用了这么久的@Scheduled,你知道它的实现原理吗?

这两天使用Scheduled注解来解决定时问题的时候,发现不能正常使用。所以就有了这一篇博客

@Scheduled(initialDelay = 2000,fixedDelay = 1000)
private void test(){
    System.out.println(Math.random());
}

单从源码的doc文件中可以看到这么一段

You can add the `@Scheduled` annotation to a method, along with trigger metadata. For
example, the following method is invoked every five seconds with a fixed delay,
meaning that the period is measured from the completion time of each preceding
invocation:

[source,java,indent=0]
[subs="verbatim,quotes"]
----
    @Scheduled(fixedDelay=5000)
    public void doSomething() {
        // something that should run periodically
    }
----

将源码放进我的环境运行,发现并不能生效。那就只能先看看源码来看看它究竟是怎么生效的

注解Scheduled的源码

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
    String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
    String cron() default "";
    String zone() default "";
    long fixedDelay() default -1;
    String fixedDelayString() default "";
    long fixedRate() default -1;
    String fixedRateString() default "";
    long initialDelay() default -1;
    String initialDelayString() default "";
}

然后,动态加载的代码在 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> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                                method, Scheduled.class, Schedules.class);
                        return (!scheduledMethods.isEmpty() ? scheduledMethods : 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, scheduledMethods) ->
                        scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
                if (logger.isTraceEnabled()) {
                    logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                            "': " + annotatedMethods);
                }
            }
        }
        return bean;
    }

首先,通过AopProxyUtils.ultimateTargetClass获取传入的Bean的最终类(是哪个类),然后判断当前类有没有在this.nonAnnotatedClasses中,如果没有在,则继续使用AnnotationUtils.isCandidateClass判断当前类是不是一个非抽象类或者接口,如果都满足,则调用

Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                    (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
                        Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                                method, Scheduled.class, Schedules.class);
                        return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
                    });

获取到某个方法的所有Scheduled。也就是说,一个方法,是可以同时被多次定义周期化的。也就是这样

@Scheduled(fixedDelay = 5000)
@Schedules({@Scheduled(fixedDelay = 5000),@Scheduled(fixedDelay = 3000)})
public void test(){
    logger.info("123");
}

继续分析源码,我们可以发现,在得到targetClass(目标类)的所有带有@Scheduled或者@Schedules注解的方法并放到annotatedMethods中后,如果annotatedMethods的大小为0,则将当前目标targetClass放到this.nonAnnotatedClasses中,标记这个类中没有被相关注解修饰,方便新的调用方进行判断。如果annotatedMethods的大小不为空,则

nnotatedMethods.forEach((method, scheduledMethods) ->
                        scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));

将单独处理每个周期性任务。下面来看看究竟是怎么处理的

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
    // 可以看见,scheduled method的运行必须在Bean环境中,所以用@Schedules或者@Scheduled的方法必须在一个bean类里面
    try {
        Runnable runnable = createRunnable(bean, method);
        boolean processedSchedule = false;
        String errorMessage =
            "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
        Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
        // Determine initial delay 定义开始时间
        long initialDelay = scheduled.initialDelay();
        String initialDelayString = scheduled.initialDelayString();
        // initialDelay 和 initialDelayString 只能同时定义一个
        if (StringUtils.hasText(initialDelayString)) {
            Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
            if (this.embeddedValueResolver != null) {
                initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
            }
            if (StringUtils.hasLength(initialDelayString)) {
                try {
                    initialDelay = parseDelayAsLong(initialDelayString);
                }
                catch (RuntimeException ex) {
                    throw new IllegalArgumentException(
                        "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
                }
            }
        }
        // Check cron expression
        String cron = scheduled.cron();
        if (StringUtils.hasText(cron)) {
            String zone = scheduled.zone();
            if (this.embeddedValueResolver != null) {
                // 调用  this.embeddedValueResolver.resolveStringValue 解析cron
                cron = this.embeddedValueResolver.resolveStringValue(cron);
                zone = this.embeddedValueResolver.resolveStringValue(zone);
            }
            if (StringUtils.hasLength(cron)) {
                // 如果在initialDelay定义的情况下,cron是不生效的
                Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
                processedSchedule = true;
                // String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
                // public static final String CRON_DISABLED = "-";
                // 如果cron不等于 '-'
                if (!Scheduled.CRON_DISABLED.equals(cron)) {
                    TimeZone timeZone;
                    // 解析timeZone
                    if (StringUtils.hasText(zone)) {
                        timeZone = StringUtils.parseTimeZoneString(zone);
                    }
                    else {
                        timeZone = TimeZone.getDefault();
                    }
                    // 使用 new CronTrigger(cron, timeZone) 创建定时触发器
                    // 使用 new CronTask(runnable, new CronTrigger(cron, timeZone)) 创建一个定时任务,定时触发器会触发runnable
                    // 调用 this.registrar.scheduleCronTask 注册任务到当前环境中
                    // tasks是一个集合,避免重复注册相同的任务
                    tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
                }
            }
        }

        // At this point we don't need to differentiate between initial delay set or not anymore
        if (initialDelay < 0) {
            initialDelay = 0;
        }

        // Check fixed delay
        long fixedDelay = scheduled.fixedDelay();
        if (fixedDelay >= 0) {
            // 如果当前任务没有被加入到tasks
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            // 使用 new FixedDelayTask(runnable, fixedDelay, initialDelay) 来注册延迟任务
            tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
        }
        String fixedDelayString = scheduled.fixedDelayString();
        if (StringUtils.hasText(fixedDelayString)) {
            // 如果没有传fixedDelay,但是传了fixedDelayString,可以使用它的值
            if (this.embeddedValueResolver != null) {
                fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
            }
            if (StringUtils.hasLength(fixedDelayString)) {
                Assert.isTrue(!processedSchedule, errorMessage);
                processedSchedule = true;
                try {
                    fixedDelay = parseDelayAsLong(fixedDelayString);
                }
                catch (RuntimeException ex) {
                    throw new IllegalArgumentException(
                        "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
                }
                tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
            }
        }

        // Check fixed rate
        // 如果上面的都没满足,则判断fixedDate和fixedDateString的值
        long fixedRate = scheduled.fixedRate();
        if (fixedRate >= 0) { 
            Assert.isTrue(!processedSchedule, errorMessage);
            processedSchedule = true;
            tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
        }
        String fixedRateString = scheduled.fixedRateString();
        if (StringUtils.hasText(fixedRateString)) {
            if (this.embeddedValueResolver != null) {
                fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
            }
            if (StringUtils.hasLength(fixedRateString)) {
                Assert.isTrue(!processedSchedule, errorMessage);
                processedSchedule = true;
                try {
                    fixedRate = parseDelayAsLong(fixedRateString);
                }
                catch (RuntimeException ex) {
                    throw new IllegalArgumentException(
                        "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
                }
                tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
            }
        }

        // Check whether we had any attribute set
        Assert.isTrue(processedSchedule, errorMessage);

        // Finally register the scheduled tasks
        // 同步的注册任务 加入缓存,方便使用
        synchronized (this.scheduledTasks) {
            Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
            regTasks.addAll(tasks);
        }
    }
    catch (IllegalArgumentException ex) {
        throw new IllegalStateException(
            "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
    }
}

下面为某个类里面的方法是创建Runnabled的方法,传入方法和目标类就可以得到

protected Runnable createRunnable(Object target, Method method) {
    Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
    Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
    return new ScheduledMethodRunnable(target, invocableMethod);
}

下面是scheduleCronTask方法的定义,可以看见这里会对task去重

@Nullable
public ScheduledTask scheduleCronTask(CronTask task) {
    ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
    boolean newTask = false;
    if (scheduledTask == null) {
        scheduledTask = new ScheduledTask(task);
        newTask = true;
    }
    if (this.taskScheduler != null) {
        scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
    }
    else {
        addCronTask(task);
        this.unresolvedTasks.put(task, scheduledTask);
    }
    return (newTask ? scheduledTask : null);
}

通过processScheduled方法会将某个被@Scheduled或者@Schedules注解修饰的方法注册进全局的scheduledTask环境中。

也就是说,方法postProcessAfterInitialization会将整个bean中的所有被@Scheduled或者@Schedules注解修饰的方法都注册进全局定时执行环境。

哪些地方调用了postProcessAfterInitialization

视线移到抽象类AbstractAutowireCapableBeanFactory的applyBeanPostProcessorsAfterInitialization方法中,这里是源码中唯一调用postProcessAfterInitialization的地方,也就是说,所有的周期任务都是在这里被注入到环境中的(其实不只是被@Scheduled或者@Schedules修饰的周期性任务)

@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
    throws BeansException {
    Object result = existingBean;
    for (BeanPostProcessor processor : getBeanPostProcessors()) {
        Object current = processor.postProcessAfterInitialization(result, beanName);
        if (current == null) {
            return result;
        }
        result = current;
    }
    return result;
}

那么既然来了,我们还是分析一下,这里究竟做了什么。

首先,通过调用getBeanPostProcessors()获取到了所有的BeanPostProcessor,这个类可以理解为是各种Bean的加载器。而我们的ScheduledAnnotationBeanPostProcessor就是其中之一。根据调用栈可以发现,最终追溯到了AbstractBeanFactory.beanPostProcessors(List<BeanPostProcessor>类型),下面两个方法是添加函数

@Override
public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
    Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null");
    // Remove from old position, if any
    this.beanPostProcessors.remove(beanPostProcessor);
    // Add to end of list
    this.beanPostProcessors.add(beanPostProcessor);
}

public void addBeanPostProcessors(Collection<? extends BeanPostProcessor> beanPostProcessors) {
    this.beanPostProcessors.removeAll(beanPostProcessors);
    this.beanPostProcessors.addAll(beanPostProcessors);
}

可以看出来,beanPostProcessors中没有重复的Processors。我们把上面那个函数定义为方法一,后面那个方法定义为方法二,方法二在下面这个方法中被调用了,整个调用栈如下:

private static void registerBeanPostProcessors(
    ConfigurableListableBeanFactory beanFactory, List<BeanPostProcessor> postProcessors) {
    if (beanFactory instanceof AbstractBeanFactory) {
        // Bulk addition is more efficient against our CopyOnWriteArrayList there
        ((AbstractBeanFactory) beanFactory).addBeanPostProcessors(postProcessors);
    }
    else {
        for (BeanPostProcessor postProcessor : postProcessors) {
            beanFactory.addBeanPostProcessor(postProcessor);
        }
    }
}
file

这四次调用都出现在同一个函数registerBeanPostProcessors中,那么我们可以假设,这里的调用顺序,就是Bean加载的先后顺序(做java开发的应该都知道,如果代码写得不当,定义了错误的Bean加载顺序回导致注入无法完成,从而造成代码无法运行的问题)。那么,Bean的注册顺序就是

priorityOrderedPostProcessors > orderedPostProcessors > nonOrderedPostProcessors > internalPostProcessors

registerBeanPostProcessors的源码分析与标题没有什么关系,这里就不做分析了。留着下次分析Bean的时候再仔细分析。

从调用方分析的路走不通,我们可以尝试从最远头出发

我们都知道,使用@Scheduled或者@Schedules之前,必须要在全局加上@EnableScheduling的注解。那么我们就可以从这个注解入手进行分析。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}

可惜的是,源码中并没有对注解@EnableScheduling进行解析的代码。可是这是为什么呢?我们注意到,修饰这个注解的有一个我们从来没有见过的注解@Import,会不会是@Import

其中,@Import的源码如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}

其中,定义@Import注解行为的源码在类ConfigurationClassParser的collectImports方法中,来看看吧

private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
    throws IOException {

    if (visited.add(sourceClass)) {
        for (SourceClass annotation : sourceClass.getAnnotations()) {
            String annName = annotation.getMetadata().getClassName();
            if (!annName.equals(Import.class.getName())) {
                collectImports(annotation, imports, visited);
            }
        }
        imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
    }
}

这个函数是一个递归函数,会不断地查找某个注解以及修饰它的注解所有被Import注解导入的配置文件。这个函数的调用栈如下

private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
    Set<SourceClass> imports = new LinkedHashSet<>();
    Set<SourceClass> visited = new LinkedHashSet<>();
    collectImports(sourceClass, imports, visited);
    return imports;
}

// getImports在ConfigurationClassParser的doProcessConfigurationClass方法中被调用
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

// doProcessConfigurationClass在ConfigurationClassParser的processConfigurationClass方法中被调用
do {
    sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
this.configurationClasses.put(configClass, configClass);

由于调用栈实在是太深,最后会到FrameworkServlet的refresh()方法上,暂时我只能下一个结论就是,在Application的主类上面修饰的注解并不会单独写反射方法来实现,而是会通过spring提供的统一处理方式进行处理。因为在整个spring框架源码中都没有找到对该注解进行反射操作的内容。

file

总结

通过这一篇文章,我们从源码中学习了@Scheduled 和 @Schedules 这两个注解的,他们是如何解析参数,如何加入时间触发器,不过目前还欠缺时间触发器究竟是如何工作的这部分的内容,后续我会补上。

另外,我们也初次了解了,这种注解是如何被spring框架调用到的,知道了BeanFactory,也知道了ConfigurationClassParser,这给我们接下来全面研究Spring容器这一块提供了契机。

最后的最后,我代码里面的问题就是没有在主类里面加@EnableScheduling注解

炒鸡辣鸡原创文章,转载请注明来源

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269