在一些中小型项目当中经常会使用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完成之后进行的代理对象替换。