SchedulingConfigurer实现动态定时,导致ApplicationRunner无效解决
作者:暴躁码农
SchedulingConfigurer实现动态定时,导致ApplicationRunner无效
问题描述
当通过SchedulingConfigurer接口实现动态定时任务后,发现ApplicationRunner接口实现的逻辑不生效了,断点不进,说明ApplicationRunner接口实现的方法并没有执行。
问题解释
SchedulingConfigurer接口是使用Spring实现动态定时任务必然的一步,而ApplicationRunner接口为的是在容器(服务)启动完成后,进行一些操作,同样效果的还有接口CommandLineRunner,那么是因为啥导致实现SchedulingConfigurer接口后ApplicationRunner和CommandLineRunner的接口实现就不生效了呢?
原因剖析
导致ApplicationRunner和CommandLineRunner接口失效的原因还要看他俩的实现原理。
首先我们要明确一个概念,那就是虽然它俩名字含有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); } } }
所以存在猜测,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); } ); } }
其实原因很简单,就是因为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; } })); } }
SpringBoot的ApplicationRunner问题
在开发中可能会有这样的情景。需要在容器启动的时候执行一些内容。比如读取配置文件,数据库连接之类的。
SpringBoot给我们提供了两个接口来帮助我们实现这种需求。
这两个接口分别为CommandLineRunner和ApplicationRunner。他们的执行时机为容器启动完成的时候。
这两个接口中有一个run方法,我们只需要实现这个方法即可。
这两个接口的不同之处在于:ApplicationRunner中run方法的参数为ApplicationArguments,而CommandLineRunner接口中run方法的参数为String数组。
目前我在项目中用的是ApplicationRunner。是这么实现的:
package com.jdddemo.demo.controller; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component public class JDDRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println(args); System.out.println("这个是测试ApplicationRunner接口"); } }
执行结果如下:
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。