任务调度-动态定时任务

503 阅读5分钟

学习背景

学习是要有目的性的,漫无目的的学习会让你成长的很慢。我的学习背景是这样的,上游系统需要将他们的商品数据推送到我们系统(400万左右的数据量),我们保存的同时,需要将这部分数据推送到ERP系统。因为数据量太大,在推送过程中难免会遇到服务连接超时等一系列的问题,遇到问题时,我们需要将推送失败的数据进行记录,后续专门处理这部分数据。

处理失败数据的方式有两种:手动推送;定时任务推送。手动推送太烦人了,所以选择定时任务推送。领导又要求定时任务可以控制好开关,还能控制推送的时间以及频率,在这里选择了动态定时任务

在这里介绍两种不同的方式,去实现动态定时任务:
1. 通过ScheduledFuture实现 2. XXL-Job

一、ScheduledFuture实现动态定时任务

1.创建定时任务实体类Cron

我在创建这里实体类的时候,设计的比较简单,具体包含下面几个字段:
       1.oid:通过雪花算法生成,不重复,无实际意义
       2.cronClass:真正的定时任务业务代码所在的全路径
       3.cron:定时任务表达式
       4.cronDesc:定时任务的描述
       5.cronStatus:定时任务的状态,用来控制定时任务是否开启,0-关闭,1-开启

2.编写维护定时任务的服务

这部分接口主要包含:新增定时任务;修改定时任务;多条件查询定时任务;按照oid去开启以及关闭定时任务。这些接口需要向外提供rest服务,之后再生成环境容易进行维护。

3.创建配置类

@Slf4j
@Configuration
public class ScheduleConfig {

    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;

    @Autowired
    ICronService cronService;
    /**
     * 存放所有启动定时任务对象,极其重要
     */
    private HashMap<String, ScheduledFuture<?>> scheduleMap = new HashMap<>();

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        return new ThreadPoolTaskScheduler();
    }

    /**
     * 初始化定时任务
     */
    @Bean
    public void initScheduleTask(){
        log.info("初始化定时任务");
        /**
         * 这部分涉及内部代码,不予展示,主要实现的功能:
         *   调用第二部编写的接口,查询出当前全部的定时任务List<Cron>
         *   去调用startCron()方法,开启定时任务
         **/
    }

    /**
     * @param crons
     * 动态设置定时任务方法
     *
     * 此方法是真正的动态实现启停和时间周期的关键,你可以针对自己的业务来调用,
     * 你对库中的动态数据修改后来调用此方法,每个Cron对象必须要包含,
     * 执行周期(cron.getCron()),启停状态(cron.getCronStatus()),执行的类(cron.getCronClass())
     */
    public void startCron(List<Cron> crons){
        try {
            //遍历所有库中动态数据,根据库中class取出所属的定时任务对象进行关闭,每次都会把之前所有的定时任务都关闭,根据新的状态重新启用一次,达到最新配置
            for (Cron cron : crons){
                ScheduledFuture<?> scheduledFuture = scheduleMap.get(cron.getCronClass());
                //一定判空否则出现空指针异常,
                if (scheduledFuture != null){
                    scheduledFuture.cancel(true);
                }
            }
            //因为下边存储的是新的定时任务对象,以前的定时任务对象已经都停用了,所以旧的数据没用清除掉,这步可以不处理,因为可以是不可重复要被覆盖
            //scheduleMap.clear();
            //遍历库中数据,之前已经把之前所有的定时任务都停用了,现在判断库中如果是启用的重新启用并读取新的数据,把开启的数据对象保存到定时任务对象中以便下次停用
            for (Cron cron : crons){
                //判断当前定时任务是否有效,COMMON_API_WRAPPER_STATIC_VALUE.VALIDFLAG.TRUE为有效标识
                if (cron.getCronStatus().equals(1)) {
                    //开启一个新的任务,库中存储的是全类名(包名加类名)通过反射成java类,读取新的时间
                    ScheduledFuture<?> future = threadPoolTaskScheduler.schedule((Runnable) Class.forName(cron.getCronClass()).newInstance(), new CronTrigger(cron.getCron()));
                    //这一步非常重要,之前直接停用,只停用掉了最后启动的定时任务,前边启用的都没办法停止,所以把每次的对象存到map中可以根据key停用自己想要停用的
                    scheduleMap.put(cron.getCronClass(),future);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.创建Runnable实现类,完成真正的业务逻辑

 public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("业务执行了" + new Date());
    }

}

5.错误总结

1.在配置类中,在开始是在初始化方法上面使用的@PostConstruct注解,想要达到的结果就是:初始化的时候,将定时任务开启。因为在初始化方法当中,需要查询全部的定时任务,既然需要查询,就得注入业务接口的Bean,在查询时,报了空指针异常。

通过查阅资料发现:@PostConstruct注释的方法,会在容器加载Servlet时候运行,并且只会运行一次。并且是在【构造方法】之后,init之前运行。

明白了:在执行@PostConstruct修饰的方法的时候,Spring容器还没有初始化Bean,所以注入的是null的,导致调用查询接口时,报空指针异常。

2.在Runable的实现类中,实现的具体的业务是:查询出所有推送失败的数据,再次进行推送。在这里也使用到了查询接口,在注入Spring的Bean的时候,还是注入不进来。

在代码中是通过threadPoolTaskScheduler.schedule(Runnable task, Trigger trigger);方法去开启的定时任务,通过查看源码发现,这个方法中创建Runable对象时,是new出来的,所以这个对象不是Spring的Bean,所以这个Runable实现类中注入不进来Spring Bean。

解决方式:获取Spring容器,从容器中拿到具体的Bean。

***Service service = SpringContextUtil.getBean(业务接口名称.class);

@Component
public class SpringContextUtil implements ApplicationContextAware{

   private static ApplicationContext applicationContext;

   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
       if (SpringContextUtil.applicationContext == null) {
           SpringContextUtil.applicationContext = applicationContext;
       }
   }

   /**
    * 获取applicationContext
    */
   public static ApplicationContext getApplicationContext() {
       return applicationContext;
   }

   /**
    * 通过name获取 Bean.
    *
    * @param name
    * @return
    */
   public static Object getBean(String name) {
       return getApplicationContext().getBean(name);
   }

   /**
    * 通过class获取Bean.
    *
    * @param clazz
    * @param <T>
    * @return
    */
   public static <T> T getBean(Class<T> clazz) {
       return getApplicationContext().getBean(clazz);
   }

   /**
    * 通过name,以及Clazz返回指定的Bean
    *
    * @param name
    * @param clazz
    * @param <T>
    * @return
    */
   public static <T> T getBean(String name, Class<T> clazz) {
       return getApplicationContext().getBean(name, clazz);
   }


   public static Object createBean(String name, Object object) {
       DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
       try {
           beanFactory.removeBeanDefinition(name);
       } catch (NoSuchBeanDefinitionException e) {

       }
       beanFactory.registerSingleton(name, object);

       return beanFactory.getBean(name);

   }

}