问题背景:在构建一个在线编程平台的过程中,我使用了Spring AOP来增强代码沙箱(CodeSandBox)的功能。通过定义一个切面,我希望在执行代码沙箱的相关方法前后添加日志记录和其他业务逻辑。
在编写单元测试时,我发现尽管我已经按照Spring的规范配置了AOP切面,但是在测试执行过程中,并没有触发@Around注解的方法。这导致了日志记录和其他增强逻辑的缺失,从而使得测试结果与预期不符。
AOP切面类:
@Aspect
@Component
@Slf4j
public class aopCodeSand {@Around("execution(* com.jinyi.OJ_backend.judge.codeSandBox.impl.*.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Object[] args = joinPoint.getArgs();log.info("aop CodeSandBoxRequest:{}", args[0]);Object proceed = joinPoint.proceed();log.info("aop CodeSandBoxResponse:{}", proceed);return proceed;}}
springboot测试类:
@SpringBootTest
class MainApplicationTests {@Value("${codeSandBox.type:example}")private String type;@Testvoid testCodeSand() {String code = "int main(){ }";List<String> list = Arrays.asList("1 2", "3 4");String language = QuestionSubmitLanguageEnum.CPLUSPLUS.getValue();CodeSandBox codeSandBox = CodeSandFactory.getCodeSandBox(type);ExecuteCodeRequest request = ExecuteCodeRequest.builder().code(code).inputList(list).language(language).build();codeSandBox.executeCode(request);}}在使用Spring AOP时,如果AOP的切面没有按预期工作,可能有几个原因:
切面类没有被Spring容器管理:确保你的
aopCodeSand类被标记为@Component或@Service等,以便Spring可以自动检测并注册它。
切点表达式不匹配:检查你的切点表达式
execution(* com.jinyi.OJ_backend.judge.codeSandBox.impl.*.*(..))是否正确匹配了你想要拦截的方法。如果方法签名或包路径不正确,AOP将不会拦截到这些方法。
日志配置问题:如果日志级别设置不正确,可能不会打印出日志信息。检查你的日志配置,确保
log.info调用的日志级别是开启的。
测试环境问题:在使用
@SpringBootTest时,确保你的测试类能够加载Spring的上下文,并且AOP代理是开启的。可以通过在测试类上添加@AutoConfigureMockMvc来确保Spring MVC的配置也被加载。
切面类未被正确加载:有时候,即使类被标记为
@Component,Spring容器也可能没有正确加载它。这可能是因为类路径问题或其他Spring配置问题。
Spring AOP的代理问题:确保你的
CodeSandBox实现类是Spring管理的Bean,并且它的方法被Spring代理。
测试方法的执行:检查
testCodeSand方法是否真正被执行了。有时候,可能是因为测试配置问题导致测试方法没有运行。
Spring AOP的兼容性问题:如果你使用的是Spring Boot,确保你的Spring版本和Spring AOP版本是兼容的。
为了进一步调试,可以尝试以下步骤:
- 确保你的
aopCodeSand类在Spring容器启动时被正确加载。- 在
aopCodeSand类中添加一个@PostConstruct注解的方法,打印一条消息,以确保它被初始化。- 使用
@Profile注解指定测试环境,确保在测试时加载特定的配置。- 使用Spring的AOP代理工具来检查代理链,确保你的切面被应用到了目标对象上。
之后我再AOP切面类加入了 @PostConstruct注解的方法,打印一条消息,以确保它被初始化。
加入后的AOP类:
@Aspect
@Component
@Slf4j
public class aopCodeSand {@Around("execution(* com.jinyi.OJ_backend.judge.codeSandBox.impl.*.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Object[] args = joinPoint.getArgs();log.info("aop CodeSandBoxRequest:{}", args[0]);Object proceed = joinPoint.proceed();log.info("aop CodeSandBoxResponse:{}", proceed);return proceed;}@PostConstructpublic void init() {log.info("aopCodeSand Bean is initialized.");}}
之后的执行结果:
13:19:49.512 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate]
13:19:49.528 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)]
13:19:49.599 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.jinyi.OJ_backend.MainApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper]
13:19:49.620 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.jinyi.OJ_backend.MainApplicationTests], using SpringBootContextLoader
13:19:49.627 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.jinyi.OJ_backend.MainApplicationTests]: class path resource [com/jinyi/OJ_backend/MainApplicationTests-context.xml] does not exist
13:19:49.628 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.jinyi.OJ_backend.MainApplicationTests]: class path resource [com/jinyi/OJ_backend/MainApplicationTestsContext.groovy] does not exist
13:19:49.628 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.jinyi.OJ_backend.MainApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.
13:19:49.629 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.jinyi.OJ_backend.MainApplicationTests]: MainApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
13:19:49.757 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.jinyi.OJ_backend.MainApplicationTests]
13:19:49.882 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [D:\java_demo\jinyi-OJ-backend\target\classes\com\jinyi\OJ_backend\MainApplication.class]
13:19:49.885 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.jinyi.OJ_backend.MainApplication for test class com.jinyi.OJ_backend.MainApplicationTests
13:19:50.080 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.jinyi.OJ_backend.MainApplicationTests]: using defaults.
13:19:50.081 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.event.ApplicationEventsTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
13:19:50.109 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@339bf286, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@38be305c, org.springframework.test.context.event.ApplicationEventsTestExecutionListener@269f4bad, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@5ed731d0, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@3234f74e, org.springframework.test.context.support.DirtiesContextTestExecutionListener@7bc10d84, org.springframework.test.context.transaction.TransactionalTestExecutionListener@275fe372, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@40e10ff8, org.springframework.test.context.event.EventPublishingTestExecutionListener@557a1e2d, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@26a4842b, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@7e38a7fe, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@366ef90e, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@33e01298, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@31e75d13, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@a5b0b86]
13:19:50.117 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@36676c1a testClass = MainApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@5b408dc3 testClass = MainApplicationTests, locations = '{}', classes = '{class com.jinyi.OJ_backend.MainApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[[ImportsContextCustomizer@4d098f9b key = [org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration, org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration, org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration, org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6e005dc9, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@7c214cc0, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@8e50104, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@6c2ed0cd, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@4b3fa0b3, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@181e731e, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@78dd667e], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null].
by 程序员jinyi:https://github.com/VioletFrank 2024-06-23 13:19:50.899  INFO 8928 --- [           main] c.jinyi.OJ_backend.MainApplicationTests  : Starting MainApplicationTests using Java 11.0.23 on Voilet with PID 8928 (started by Lenovo in D:\java_demo\jinyi-OJ-backend)
2024-06-23 13:19:50.902  INFO 8928 --- [           main] c.jinyi.OJ_backend.MainApplicationTests  : The following 1 profile is active: "dev"
2024-06-23 13:19:52.504  INFO 8928 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2024-06-23 13:19:52.512  INFO 8928 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Elasticsearch repositories in DEFAULT mode.
2024-06-23 13:19:52.801  INFO 8928 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 281 ms. Found 1 Elasticsearch repository interfaces.
2024-06-23 13:19:52.807  INFO 8928 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2024-06-23 13:19:52.809  INFO 8928 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive Elasticsearch repositories in DEFAULT mode.
2024-06-23 13:19:52.819  INFO 8928 --- [           main] .RepositoryConfigurationExtensionSupport : Spring Data Reactive Elasticsearch - Could not safely identify store assignment for repository candidate interface com.jinyi.OJ_backend.esdao.PostEsDao; If you want this repository to be a Reactive Elasticsearch repository, consider annotating your entities with one of these annotations: org.springframework.data.elasticsearch.annotations.Document (preferred), or consider extending one of the following types with your repository: org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository
2024-06-23 13:19:52.819  INFO 8928 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 15 ms. Found 0 Reactive Elasticsearch repository interfaces.
2024-06-23 13:19:53.071  WARN 8928 --- [           main] o.m.s.mapper.ClassPathMapperScanner      : No MyBatis mapper was found in '[com.yupi.springbootinit.mapper]' package. Please check your configuration.
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@5df64b2a'
Parsed mapper file: 'file [D:\java_demo\jinyi-OJ-backend\target\classes\mapper\PostFavourMapper.xml]'
Parsed mapper file: 'file [D:\java_demo\jinyi-OJ-backend\target\classes\mapper\PostMapper.xml]'
Parsed mapper file: 'file [D:\java_demo\jinyi-OJ-backend\target\classes\mapper\PostThumbMapper.xml]'
Parsed mapper file: 'file [D:\java_demo\jinyi-OJ-backend\target\classes\mapper\QuestionMapper.xml]'
Parsed mapper file: 'file [D:\java_demo\jinyi-OJ-backend\target\classes\mapper\QuestionSubmitMapper.xml]'
Parsed mapper file: 'file [D:\java_demo\jinyi-OJ-backend\target\classes\mapper\UserMapper.xml]'_ _   |_  _ _|_. ___ _ |    _ 
| | |\/|_)(_| | |_\  |_)||_|_\ /               |         3.5.2 
2024-06-23 13:19:58.907  INFO 8928 --- [           main] c.j.O.j.codeSandBox.config.aopCodeSand   : aopCodeSand Bean is initialized.
2024-06-23 13:20:01.118  INFO 8928 --- [           main] pertySourcedRequestMappingHandlerMapping : Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation(String, HttpServletRequest)]
2024-06-23 13:20:02.052  INFO 8928 --- [           main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2024-06-23 13:20:02.052  INFO 8928 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2024-06-23 13:20:02.054  INFO 8928 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 2 ms
2024-06-23 13:20:02.082  INFO 8928 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Documentation plugins bootstrapped
2024-06-23 13:20:02.096  INFO 8928 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
2024-06-23 13:20:02.163  INFO 8928 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
2024-06-23 13:20:02.756  INFO 8928 --- [           main] c.jinyi.OJ_backend.MainApplicationTests  : Started MainApplicationTests in 12.566 seconds (JVM running for 14.987)
远程代码沙箱执行代码...可以看到以下信息:
-  Spring Boot测试环境正在启动:日志显示 @SpringBootTest正在启动,Spring Boot的测试上下文正在加载。
-  没有找到自定义的配置类或资源文件:Spring Boot测试加载器没有找到 @ContextConfiguration或@ContextHierarchy注解指定的配置类或资源文件。它将使用默认的SpringBootContextLoader。
-  Spring Data模块被检测到:日志显示Spring Data模块被检测到,并且进入了严格的存储库配置模式。 
-  MyBatis Mapper扫描:日志显示MyBatis的Mapper文件被解析,但没有在指定的包中找到MyBatis的Mapper。 
-  AOP切面类初始化:日志中的 c.j.O.j.codeSandBox.config.aopCodeSand : aopCodeSand Bean is initialized.表明您的AOP切面类aopCodeSand已经被Spring容器初始化。
-  Swagger文档生成:日志显示Springfox正在为API生成Swagger文档。 
-  测试类启动: Started MainApplicationTests in 12.566 seconds表明测试类MainApplicationTests已经启动。
-  测试执行:日志的最后部分 远程代码沙箱执行代码...表明测试方法正在执行。
从日志中没有看到任何错误或异常,这表明Spring Boot测试环境和应用程序已经成功启动。但是,问题是AOP没有按预期工作。由于日志中显示AOP切面类已经被初始化,但没有显示@Around方法的日志,这可能意味着:
- AOP的切点表达式可能没有正确匹配到您想要拦截的方法。
- 测试方法可能没有触发AOP的切面逻辑。
要排查测试方法是否触发了AOP的切面逻辑,于是采取以下步骤:
-  检查代理对象:确保 CodeSandBox对象是Spring代理对象。如果CodeSandBox是通过直接new出来的,它不会被Spring代理,因此AOP不会生效。
-  检查Spring配置:确保Spring配置没有问题,特别是如果使用了XML配置或自定义的 @Configuration类,确保它们被正确加载。
-  使用 @DirtiesContext注解:在测试类上使用@DirtiesContext注解,确保每次测试都会重新创建Spring应用上下文。@DirtiesContext(classMode = ClassMode.AFTER_CLASS) @SpringBootTest class MainApplicationTests {// ... }
-  检查测试执行器:确保测试类使用了正确的测试执行器。对于Spring Boot应用程序,通常使用 @SpringBootTest。
-  检查Spring AOP的配置:确保Spring AOP配置没有问题,比如 @EnableAspectJAutoProxy是否已经添加到配置类中。
-  使用断点调试:在IDE中设置断点,检查AOP方法是否真的没有被调用。这可以帮助确定是否是代码路径问题。 
-  检查测试类是否正确执行:确保测试方法被正确执行。有时候,可能是因为测试配置问题导致测试方法没有运行。 
-  检查Spring的日志级别:确保Spring的日志级别足够高,以便能够看到AOP相关的日志信息。 
到这里,我的问题已经解决了,就是因为测试类使用的对象是new出来的,绕过了springaop的管理,所以没有生效,修改后的测试类:
@SpringBootTest
//@AutoConfigureMockMvc
class MainApplicationTests {@Value("${codeSandBox.type:example}")private String type;@Resourceprivate RemoteCodeSandBox remoteCodeSandBox;@Testvoid testCodeSand() {String code = "int main(){ }";List<String> list = Arrays.asList("1 2", "3 4");String language = QuestionSubmitLanguageEnum.CPLUSPLUS.getValue();//CodeSandBox codeSandBox = CodeSandFactory.getCodeSandBox(type);ExecuteCodeRequest request = ExecuteCodeRequest.builder().code(code).inputList(list).language(language).build();remoteCodeSandBox.executeCode(request);}}运行结果:
2024-06-23 13:29:03.686  INFO 13592 --- [           main] c.j.O.j.codeSandBox.config.aopCodeSand   : aop CodeSandBoxRequest:ExecuteCodeRequest(inputList=[1 2, 3 4], code=int main(){ }, language=c++)
远程代码沙箱执行代码...
2024-06-23 13:29:03.699  INFO 13592 --- [           main] c.j.O.j.codeSandBox.config.aopCodeSand   : aop CodeSandBoxResponse:null
顺便再说一下,在Spring框架中,AOP代理是通过Spring容器来创建的,这意味着只有通过Spring容器管理的Bean才能被代理。当你使用new关键字直接实例化一个对象时,这个对象并不在Spring的控制之下,因此它不会应用任何AOP代理。以下是几个关键点来解释为什么直接使用new关键字实例化的CodeSandBox对象不会触发AOP逻辑:
-  Spring容器管理:Spring通过IoC(控制反转)容器管理Bean的生命周期和依赖关系。当一个类被标记为一个Bean(使用 @Component、@Service等注解),Spring容器会在应用程序启动时自动创建它的实例,并管理这个实例。
-  AOP代理创建:当Spring创建一个Bean的实例时,如果启用了AOP代理(通常是通过 @EnableAspectJAutoProxy注解),Spring AOP会检查是否有切面(Aspect)应用于这个Bean。如果有,Spring AOP会创建一个代理对象,而不是原始Bean的实例。这个代理对象会拦截对Bean方法的调用,并在调用前后执行切面中定义的逻辑。
-  直接实例化的问题:如果你使用 new关键字直接创建了一个对象,这个对象绕过了Spring容器,因此不会被Spring AOP代理。这意味着,即使存在应用于该对象方法的切面,这些切面也不会被应用,因为Spring AOP不知道这个对象的存在。
-  代理的类型:Spring AOP可以创建两种类型的代理:基于CGLIB的代理和基于JDK的代理。无论哪种类型,都需要Spring容器来创建代理对象,这样才能确保方法调用被正确拦截。 
-  Bean的作用域:Spring管理的Bean可以有不同的作用域(如singleton、prototype等)。当你通过Spring容器获取Bean时,Spring会根据声明的作用域来提供Bean的实例。直接使用 new关键字创建的对象不享受这种灵活性和控制。
-  自动装配:Spring容器管理的Bean可以自动装配其他Bean的依赖,这是通过注解(如 @Autowired)或XML配置来实现的。直接使用new创建的对象无法享受这种自动装配的便利。
为了确保AOP逻辑能够生效,应该通过Spring容器来获取CodeSandBox对象,例如使用@Autowired注解自动装配,或者通过ApplicationContext获取Bean的实例。这样,当调用CodeSandBox的方法时,Spring AOP代理就能够拦截这些调用并应用相关的切面逻辑。