Spring定时任务@Scheduled为什么会让切面失效

编程入门 行业动态 更新时间:2024-10-21 18:34:46

Spring定时任务@Scheduled为什么会让<a href=https://www.elefans.com/category/jswz/34/1755292.html style=切面失效"/>

Spring定时任务@Scheduled为什么会让切面失效

Spring定时任务@Scheduled为什么会让切面失效

在一些中小型项目当中经常会使用Spring自带的任务执行器,本章不描述@Scheduled的用法,主要从源码的角度讲清楚问题所在。

背景

在一个中小型项目中,有一个定时任务,用于打款到用户微信。整个项目部署是集群模式,为了让两个节点的定时任务只有一个可以正常工作,写了一个自定义注解@AllowNode并使用通知Around来鉴别当前服务器IP地址,来校验其是否应该执行定时任务。伪代码如下:

@Component
@Slf4j
@EnableScheduling
public class PayHandler {@AutowiredPayHandler payHandler;@Scheduled(cron = "0 0/1 * * * ?")@AllowNodepublic void payHandler(){RLock lock = redisson.getLock(PAY_LOCK_NAME);if(lock.tryLock()){try{payHandler.doHandler();}catch (Exception e){log.error("定时任务异常,信息:{}", e);}finally {lock.unlock();}}}@Transactional(rollbackFor = Exception.class)public void doHandler() {}
}

1、首先我解释下为什么拆分开了两个方法来处理打款业务,有的朋友们可能会有疑问。这是因为我们必须等事物提交完成或者回滚完成之后再处理锁,否则可能会出现锁释放了,但是事物没有提交的情况。

2、为什么要自己注入自己?同一个类中,方法内部调用会导致事务失效,又不想拆分成两个类,所以自己注入自己来解决事物失效的问题。

问题描述

启动项目,在payHandler第一行,打断点,发现并没有先进入切面的Around方法,导致无法判断IP地址是否和服务器一直,所以两个节点都会跑定时任务,不符合预期。

然后查看对象发现当前对象是普通对象,并不是代理对象。

然后就继续在Threads&Variables中查看调用信息,发现Spring会给我们的定时任务创建一个ScheduledMethodRunnable。

ScheduledMethodRunnable#run:84

payHandler:126, PayHandler (com.*********)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
run:84, ScheduledMethodRunnable (org.springframework.scheduling.support)
run:54, DelegatingErrorHandlingRunnable (org.springframework.scheduling.support)
run:95, ReschedulingRunnable (org.springframework.scheduling.concurrent)
call:511, Executors$RunnableAdapter (java.util.concurrent)
run$$$capture:266, FutureTask (java.util.concurrent)
run:-1, FutureTask (java.util.concurrent)- Async stack trace
<init>:151, FutureTask (java.util.concurrent)
<init>:209, ScheduledThreadPoolExecutor$ScheduledFutureTask (java.util.concurrent)
schedule:532, ScheduledThreadPoolExecutor (java.util.concurrent)
schedule:82, ReschedulingRunnable (org.springframework.scheduling.concurrent)
schedule:372, ThreadPoolTaskScheduler (org.springframework.scheduling.concurrent)
scheduleCronTask:431, ScheduledTaskRegistrar (org.springframework.scheduling.config)
scheduleTasks:369, ScheduledTaskRegistrar (org.springframework.scheduling.config)
afterPropertiesSet:349, ScheduledTaskRegistrar (org.springframework.scheduling.config)
finishRegistration:320, ScheduledAnnotationBeanPostProcessor (org.springframework.scheduling.annotation)
onApplicationEvent:239, ScheduledAnnotationBeanPostProcessor (org.springframework.scheduling.annotation)
onApplicationEvent:110, ScheduledAnnotationBeanPostProcessor (org.springframework.scheduling.annotation)
doInvokeListener:176, SimpleApplicationEventMulticaster (org.springframework.context.event)
invokeListener:169, SimpleApplicationEventMulticaster (org.springframework.context.event)
multicastEvent:143, SimpleApplicationEventMulticaster (org.springframework.context.event)
publishEvent:421, AbstractApplicationContext (org.springframework.context.support)
publishEvent:378, AbstractApplicationContext (org.springframework.context.support)
finishRefresh:938, AbstractApplicationContext (org.springframework.context.support)
refresh:586, AbstractApplicationContext (org.springframework.context.support)
refresh:145, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:767, SpringApplication (org.springframework.boot)
refreshContext:447, SpringApplication (org.springframework.boot)
run:338, SpringApplication (org.springframework.boot)
run:1356, SpringApplication (org.springframework.boot)
run:1345, SpringApplication (org.springframework.boot)
main:17, *****Application (com.****)
invoke0:-2, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
run:49, RestartLauncher (org.springframework.boot.devtools.restart)

发现这个target对象也不是代理对象,说明组装这个Runnable的时候对象还不是代理对象。

public class ScheduledMethodRunnable implements Runnable {private final Object target;private final Method method;/*** Create a {@code ScheduledMethodRunnable} for the given target instance,* calling the specified method.* @param target the target instance to call the method on* @param method the target method to call*/public ScheduledMethodRunnable(Object target, Method method) {this.target = target;this.method = method;}。。。。。。

分析

首先PayHandler这个类,如果在实例化完成之后进行的Runnable组装,肯定不会出现是非代理对象的情况,因为切面会切到@AllowNode注解,一定会为这个类创建代理对象的。

所以我得知道Runable的组装时机。所以我在ScheduledMethodRunnable类的构造函数打断点,希望能发现点什么。

果不其然,我发现在PayHandler这个类initializeBean的时候执行ScheduledAnnotationBeanPostProcessor类的postProcessAfterInitialization方法创建的任务。

@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||bean instanceof ScheduledExecutorService) {// Ignore AOP infrastructure such as scoped proxies.return bean;}Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);if (!this.nonAnnotatedClasses.contains(targetClass) &&AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);});if (annotatedMethods.isEmpty()) {this.nonAnnotatedClasses.add(targetClass);if (logger.isTraceEnabled()) {logger.trace("No @Scheduled annotations found on bean class: " + targetClass);}}else {// Non-empty set of methodsannotatedMethods.forEach((method, scheduledAnnotations) ->scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));if (logger.isTraceEnabled()) {logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +"': " + annotatedMethods);}}}return bean;}

下面的代码中processScheduled,就是创建task

annotatedMethods.forEach((method, scheduledAnnotations) ->scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));

到这里我们就知道了,我们的定时任务PayHandler的创建时机是在initializeBean(PayHandler)的最后一步的时候,当然AOP代理对象的创建也是在这一步。

到这里我开始怀疑ScheduledAnnotationBeanPostProcessor和AbstractAutoProxyCreator的执行顺序了,但是我查看了Order,两者是一样的,所以AbstractAutoProxyCreator还是会先执行,我的猜想错了。我还是不死心的打条件断点来验证,但确实每次都是先创建代理对象。

Bean的是实例化过程在前面的章节有描述,不清楚的可以回过头去再熟悉一下。

一开始只顾着找两个BeanPostProcessor的顺序,并没有发现此时的PayHandler并没有进入到wrapIfNecessary方法,也就是说他没有走代理的创建流程。

@Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;}

也就是this.earlyProxyReferences.remove(cacheKey) != bean条件不成立,直接返回了普通bean。

private final Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);

earlyProxyReferences除了在此处使用之外,整个Spring中还有一处:

boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

这里是解决循环依赖的时候往singletonFactories中放入ObjectFacotry对象。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}@Overridepublic Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);// 放入earlyProxyReferencesthis.earlyProxyReferences.put(cacheKey, bean);// 进行代理return wrapIfNecessary(bean, beanName, cacheKey);}

也就是说因为PayHandler存在自己依赖自己的循环依赖,导致的Scheduled拿到的是普通对象,但是PayHandler注入的属性payHandler却是代理对象。

图示

我讲用流程图的方式讲清楚这里面的逻辑。

总结

到此你应该知道了,其实就是@Scheduled的实现是在initializeBean方法中的最后一步进行的任务组装,但是如果出现自己依赖自己的时候,此对象的代理对象生成是在initializeBean完成之后进行的代理对象替换。

更多推荐

Spring定时任务@Scheduled为什么会让切面失效

本文发布于:2023-11-15 01:06:41,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1590964.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:切面   会让   Spring   Scheduled

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!