使用Spring AOP @Around调用ExecutorService submit时触发NullPointerException
Spring AOP切面引发ExecutorService.submit()空指针异常问题解析
问题背景
我们实现了一个用于记录方法耗时的Spring AOP切面,但调用ExecutorService.submit()方法时抛出NullPointerException,经排查是切面向ExecutorService传入了null值。需要解决以下问题:
- 遗漏了哪些配置?
- 为何会传入null?
- 如何处理该问题或让目标方法跳过切面?
补充代码与依赖
依赖配置
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.3</version> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
业务代码
@Log4j2 @Service @RequiredArgsConstructor public class MessageProcessorService { @Qualifier("asyncpoolexecutor") private final ThreadPoolTaskExecutor threadPoolTaskExecutor; private final ParsingAndValidationSteps steps; public void processMessage() throws Exception { try { Future<String> populateClientIdTask = threadPoolTaskExecutor.submit(steps.populateClientIdTask); populateClientIdTask.get(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } }
@Service @Scope(value = "prototype") public class ParsingAndValidationSteps { @Autowired ApplicationContext ctx; public Callable<String> populateClientIdTask = () -> ctx.getBean(ClientServiceBean.class).getClientId(); }
@Service public class ClientServiceBean { public String getClientId() { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } return String.valueOf(System.currentTimeMillis()); } }
AOP切面代码
@Around("execution(* com.*.*.*.*(..))") public Object allMethodsInApplicationPackage(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long start = System.currentTimeMillis(); Object proceed = proceedingJoinPoint.proceed(); logger.info("[Metrics] {} executed in {} ms", proceedingJoinPoint.getSignature(), System.currentTimeMillis() - start); return proceed; }
异常堆栈
java.lang.NullPointerException at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:143) at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:410) at com.parsingandvalidationservice.service.MessageProcessorService.processMessage(MessageProcessorService.java:75) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) at com.aspect.Metrics.allMethodsInApplicationPackage(Metrics.java:36) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:727) at com.parsingandvalidationservice.service.MessageProcessorService$$SpringCGLIB$$0.processMessage(<generated>) at com.parsingandvalidationservice.listener.ParsingValidationServiceListenerDelegator.delegate(ParsingValidationServiceListenerDelegator.java:57) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) at com.aspect.Metrics.allMethodsInApplicationPackage(Metrics.java:36) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:727) at com.parsingandvalidationservice.listener.ParsingValidationServiceListenerDelegator$$SpringCGLIB$$0.delegate(<generated>) at com.parsingandvalidationservice.listener.ParsingValidationServiceListener.listen(ParsingValidationServiceListener.java:50) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) at com.aspect.Metrics.allMethodsInApplicationPackage(Metrics.java:36) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:727) at com.parsingandvalidationservice.listener.ParsingValidationServiceListener$$SpringCGLIB$$0.listen(<generated>) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:169) at org.springframework.kafka.listener.adapter.KotlinAwareInvocableHandlerMethod.doInvoke(KotlinAwareInvocableHandlerMethod.java:45) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:119) at org.springframework.kafka.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:78) at org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:474) at org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.invoke(MessagingMessageListenerAdapter.java:421) at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:85) at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:50) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeOnMessage(KafkaMessageListenerContainer.java:2889) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeOnMessage(KafkaMessageListenerContainer.java:2867) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeRecordListener(KafkaMessageListenerContainer.java:2779) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeWithRecords(KafkaMessageListenerContainer.java:2616) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:2505) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:2151) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeIfHaveRecords(KafkaMessageListenerContainer.java:1527) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1465) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1334) at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1804) at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java) at java.base/java.lang.Thread.run(Thread.java:1583)
问题原因
- 切面拦截范围过度宽泛:当前切面使用
execution(* com.*.*.*.*(..))作为切点,会拦截com包下所有类的所有方法,包括Callable接口的call()方法。当ThreadPoolTaskExecutor.submit()传入Callable对象时,切面会尝试拦截其call()方法的执行。 - Prototype Bean的初始化顺序问题:
ParsingAndValidationSteps是prototype作用域Bean,其中的populateClientIdTask字段在Bean实例化时就已初始化。此时Spring尚未完成ApplicationContext ctx的Autowired注入,导致ctx为null。当切面拦截call()方法执行时,ctx.getBean()抛出NPE,切面捕获异常后返回null,最终将null传入submit()方法,触发AbstractExecutorService中的空指针检查。
解决方案
方案1:缩小切面拦截范围
修改切点表达式,仅拦截业务服务类的方法,避免拦截JDK接口或框架类的方法。示例:
@Around("execution(* com.parsingandvalidationservice.service.*.*(..)) || execution(* com.parsingandvalidationservice.listener.*.*(..))") public Object allMethodsInApplicationPackage(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long start = System.currentTimeMillis(); Object proceed = proceedingJoinPoint.proceed(); logger.info("[Metrics] {} executed in {} ms", proceedingJoinPoint.getSignature(), System.currentTimeMillis() - start); return proceed; }
方案2:标记方法跳过切面
自定义一个注解,用于标记不需要被切面拦截的方法:
- 定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SkipAspect { }
- 修改切面切点,排除带注解的方法:
@Around("execution(* com.*.*.*.*(..)) && !@annotation(com.yourpackage.SkipAspect)") public Object allMethodsInApplicationPackage(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 原有逻辑 }
- 调整
ParsingAndValidationSteps的写法,将Callable改为方法并添加注解:
@Service @Scope(value = "prototype") public class ParsingAndValidationSteps { @Autowired ApplicationContext ctx; @SkipAspect public Callable<String> populateClientIdTask() { return () -> ctx.getBean(ClientServiceBean.class).getClientId(); } }
- 在
MessageProcessorService中调用方法获取Callable:
Future<String> populateClientIdTask = threadPoolTaskExecutor.submit(steps.populateClientIdTask());
方案3:延迟Callable初始化
利用@PostConstruct注解,在Bean完成Autowired注入后再初始化Callable:
@Service @Scope(value = "prototype") public class ParsingAndValidationSteps { @Autowired ApplicationContext ctx; private Callable<String> populateClientIdTask; @PostConstruct public void init() { populateClientIdTask = () -> ctx.getBean(ClientServiceBean.class).getClientId(); } public Callable<String> getPopulateClientIdTask() { return populateClientIdTask; } }
在MessageProcessorService中调用get方法获取Callable:
Future<String> populateClientIdTask = threadPoolTaskExecutor.submit(steps.getPopulateClientIdTask());
内容的提问来源于stack exchange,提问作者Mohit
相关产品推荐
相关产品推荐

