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

文章探讨了Spring定时任务@Scheduled在集群环境中可能导致切面失效的问题,由于自身类的循环依赖,初始化完成后才生成代理对象,从而影响了切面的执行。
摘要由CSDN通过智能技术生成

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

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

背景

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

@Component
@Slf4j
@EnableScheduling
public class PayHandler {
    @Autowired
    PayHandler payHandler;
  
    @Scheduled(cron = "0 0/1 * * * ?")
    @AllowNode
    public 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方法创建的任务。

@Override
	public 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 methods
				annotatedMethods.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方法,也就是说他没有走代理的创建流程。

@Override
	public 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);
       }
    }
}

@Override
	public Object getEarlyBeanReference(Object bean, String beanName) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
    // 放入earlyProxyReferences
		this.earlyProxyReferences.put(cacheKey, bean);
    // 进行代理
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

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

图示

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

在这里插入图片描述

总结

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

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas & Friends

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值