由于通过SchedulingConfigurer实现动态定时,导致的ApplicationRunner无效解决办法

问题描述

  • 当通过SchedulingConfigurer接口实现动态定时任务后,发现ApplicationRunner接口实现的逻辑不生效了,断点不进,说明ApplicationRunner接口实现的方法并没有执行。

问题解释

  • SchedulingConfigurer接口是使用Spring实现动态定时任务必然的一步,而ApplicationRunner接口为的是在容器(服务)启动完成后,进行一些操作,同样效果的还有接口CommandLineRunner,那么是因为啥导致实现SchedulingConfigurer接口后ApplicationRunner和CommandLineRunner的接口实现就不生效了呢?

原因剖析

  • 导致ApplicationRunner和CommandLineRunner接口失效的原因还要看他俩的实现原理。
  1. 首先我们要明确一个概念,那就是虽然它俩名字含有Runner也有run方法,但它并不是Runnable,先看下源码
package org.springframework.boot;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

/**
 * Interface used to indicate that a bean should <em>run</em> when it is contained within
 * a {@link SpringApplication}. Multiple {@link ApplicationRunner} beans can be defined
 * within the same application context and can be ordered using the {@link Ordered}
 * interface or {@link Order @Order} annotation.
 *
 * @author Phillip Webb
 * @since 1.3.0
 * @see CommandLineRunner
 */
@FunctionalInterface
public interface ApplicationRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming application arguments
	 * @throws Exception on error
	 */
	void run(ApplicationArguments args) throws Exception;

}

@FunctionalInterface注解说明了ApplicationRunner是个函数式接口,不了解的童鞋看下java8
而CommandLineRunner源码同样也是这样,源码如下

package org.springframework.boot;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

/**
 * Interface used to indicate that a bean should <em>run</em> when it is contained within
 * a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined
 * within the same application context and can be ordered using the {@link Ordered}
 * interface or {@link Order @Order} annotation.
 * <p>
 * If you need access to {@link ApplicationArguments} instead of the raw String array
 * consider using {@link ApplicationRunner}.
 *
 * @author Dave Syer
 * @see ApplicationRunner
 */
@FunctionalInterface
public interface CommandLineRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming main method arguments
	 * @throws Exception on error
	 */
	void run(String... args) throws Exception;

}

所以ApplicationRunner与CommandLineRunner除了名字以外的唯一区别就是入参不同 那么它俩的实现方法在什么时候执行的呢?简单说就是在ApplicationContext.run()方法中,会调用callRunners方法。
该方法获取所有实现了ApplicationRunner和CommandLineRunner的接口bean,然后依次执行对应的run方法,并且是在同一个线程中执行。因此如果有某个实现了ApplicationRunner接口的bean的run方法一直循环不返回的话,后续的代码将不会被执行。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
		List<Object> runners = new ArrayList<>();
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
		AnnotationAwareOrderComparator.sort(runners);
		for (Object runner : new LinkedHashSet<>(runners)) {
			if (runner instanceof ApplicationRunner) {
				callRunner((ApplicationRunner) runner, args);
			}
			if (runner instanceof CommandLineRunner) {
				callRunner((CommandLineRunner) runner, args);
			}
		}
	}
  1. 所以存在猜测,SchedulingConfigurer的实现方式影响了这俩,看看SchedulingConfigurer的实现方法
@Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        for(SysTimedTaskEntity timedTaskEntity : timedTaskDao.selectAll()){
            Class<?> clazz;
            Object task;
            try {
                clazz = Class.forName(timedTaskEntity.getTaskPath());
                task = context.getBean(clazz);
            } catch (ClassNotFoundException e) {
                throw new IllegalArgumentException("sys_timed_task表数据" + timedTaskEntity.getTaskPath() + "有误", e);
            } catch (BeansException e) {
                throw new IllegalArgumentException(timedTaskEntity.getTaskPath() + "未纳入到spring管理", e);
            }
            Assert.isAssignable(ScheduledOfTask.class, task.getClass(), "定时任务类必须实现ScheduledOfTask接口");
            // 可以通过改变数据库数据进而实现动态改变执行周期
            taskRegistrar.addTriggerTask(((Runnable) task),
                    triggerContext -> {
                        String cronExpression = timedTaskEntity.getTaskCron();
                        return new CronTrigger(cronExpression).nextExecutionTime(triggerContext);
                    }
            );
        }
    }
  1. 其实原因很简单,就是因为SchedulingConfigurer使用的是单线程的方式,taskRegistrar.addTriggerTask添加完会阻塞,导致后面的ApplicationRunner和CommandLineRunner无法执行。

解决办法

1.只需要修改下SchedulingConfigurer实现

@Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        for(SysTimedTaskEntity timedTaskEntity : timedTaskDao.selectAll()){
            Class<?> clazz;
            Object task;
            try {
                clazz = Class.forName(timedTaskEntity.getTaskPath());
                task = context.getBean(clazz);
            } catch (ClassNotFoundException e) {
                throw new IllegalArgumentException("sys_timed_task表数据" + timedTaskEntity.getTaskPath() + "有误", e);
            } catch (BeansException e) {
                throw new IllegalArgumentException(timedTaskEntity.getTaskPath() + "未纳入到spring管理", e);
            }
            Assert.isAssignable(ScheduledOfTask.class, task.getClass(), "定时任务类必须实现ScheduledOfTask接口");
            // 可以通过改变数据库数据进而实现动态改变执行周期
            taskRegistrar.addTriggerTask(((Runnable) task),
                    triggerContext -> {
                        String cronExpression = timedTaskEntity.getTaskCron();
                        return new CronTrigger(cronExpression).nextExecutionTime(triggerContext);
                    }
            );
            /** 解决办法如下 */ 
            // 手动创建线程池,防止SchedulingConfigurer导致系统线程阻塞
            taskRegistrar.setScheduler(new ScheduledThreadPoolExecutor(10, new ThreadFactory() {
                int counter = 0;
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r,"数据统计-Thread-"+counter);
                    counter++;
                    return thread;
                }
            }));
        }
    }
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

暴躁码农

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

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

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

打赏作者

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

抵扣说明:

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

余额充值