(一)定时任务方式介绍

日常项目开发中难免会用到定时任务,如果定时任务数量少,需求变更不频繁,代码耦合度低,那皆大欢喜。否则定时任务的维护也是让人头疼的事情,下边就介绍一下我了解到的几种实现定时任务的方式,技术没有强弱,根据实际情况选择,合适的才是最好的。

  • JDK 的Timer类: 这是Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。有两种方式,一种是使任务在指定时间被执行一次,另一种是从某一指定时间开始周期性地执行任务,一般用的较少。
  • Quartz: Quartz是一个任务调度框架,简单理解就是:在某一个有规律的时间点干某件事。并且时间的触发的条件可以非常复杂,你给它一个触发条件的定义,它负责到指定时间点执行任务。
  • SpringTask : SpringTask上手比较简单,实现有两种实现方式:一种基于xml配置,一种基于注解。这里主要介绍一下这种方式。

(二)SpringBoot实现定时任务

方式一:

搭建好SpringBoot项目,在启动类上添加@EnableScheduling注解,开启对定时任务的支持,然后编写定时任务就可以了。

编写定时任务类

/**
 * @ClassName SyncAlarmSourceTask
 * @description: 定时任务
 * @author: MYY
 * @create: 2021-11-11 11:11
 * @Version 1.0
 **/
@Component
public class TestTask{

    /**
     * 日志
     */
    private Logger logger = LoggerFactory.getLogger(SyncAlarmSourceTask.class);

	//这里表示5秒执行一次,对cron表达式不熟悉的可以去百度一下
    @Scheduled(cron = "*/5 * * * * ?")
    public void execute() {
        logger.info("任务调度成功!");
    }
    
}

这种方法虽然简单,但是cron表达式写死在代码之后不能动态改变运行周期,不利于修改维护。所以建议使用第二种方式,比较优雅的实现定时任务。

方式二:

(1)、创建数据库表

CREATE TABLE `tbl_scheduled_task` (
  `task_id` varchar(32) NOT NULL COMMENT '主键id',
  `task_classname` varchar(255) DEFAULT NULL COMMENT '定时任务完整类名',
  `task_expression` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'cron表达式',
  `task_explain` varchar(128) DEFAULT NULL COMMENT '任务描述',
  `status` tinyint(1) DEFAULT NULL COMMENT '状态 0 正常 1 停用',
  `isdel` tinyint(1) DEFAULT '0' COMMENT '状态 0 存在 1 已删除',
  `createTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updateTime` datetime DEFAULT NULL COMMENT '上次运行时间',
  PRIMARY KEY (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='定时任务表';

INSERT INTO `tbl_scheduled_task`(`task_id`, `task_classname`, `task_expression`, `task_explain`, `status`, `isdel`, `createTime`, `updateTime`) VALUES ('23ba0825cd0e4a2aa9c305fbf63d929a', 'com.myy.task.TestTask', '*/5  * *  * * ?', '测试', 0, 0, '2021-11-11 11:11:11', '2021-11-11 16:32:20');

(2)、编写Spring工具类

package com.myy.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @ClassName SpringUtil
 * @description: 创建SpringUtils,自定义获取对象的具体方法实现
 * @author: MYY
 * @create: 2021-11-11 11:11
 * @Version 1.0
 **/
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static void set(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    /**
     * 通过字节码获取
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

    /**
     * 通过BeanName获取
     * @param beanName
     * @param <T>
     * @return
     */
    public static <T> T getBean(String beanName) {
        return (T) context.getBean(beanName);
    }
    
    /**
     * 通过beanName和字节码获取
     * @param name
     * @param beanClass
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> beanClass) {
        return context.getBean(name, beanClass);
    }
}

在主启动类中将获取到的上下文对象添加到容器中,供SpringUtils使用

@EnableScheduling //springboot注解支持
@SpringBootApplication
public class RoadblockApplication extends SpringBootServletInitializer {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(RoadblockApplication.class, args);
        //添加到容器
        SpringUtil.set(context);
    }
    
}

(3)、自定义ScheduledTask接口,方便在页面灵活调用

package com.myy.enumUtils;

import lombok.Getter;

/**
 * @ClassName StatusEnum
 * @description: 状态枚举
 * @author: MYY
 * @create: 2021-11-11 11:11
 * @Version 1.0
 **/
@Getter
public enum StatusEnum {

    ENABLE("开启",0),

    DISABLE("关闭",1);

    private String name;

    private Integer code;

    // 构造方法
    private StatusEnum(String name, Integer code) {
        this.name = name;
        this.code = code;
    }
}
package com.myy.scheduled;

import com.myy.entity.ScheduledCron;
import com.myy.enumUtils.StatusEnum;
import com.myy.service.ScheduledCronService;
import com.myy.utils.SpringUtil;

/**
 * @ClassName ScheduledTask
 * @description:
 * @author: MYY
 * @create: 2021-11-08 16:56
 * @Version 1.0
 **/
public interface ScheduledTask extends Runnable{

    /**
     * 定时任务
     */
    void execute();

    /**
     * 实现控制定时任务启用或禁用的功能
     */
    default void  run(){
        ScheduledCronService scheduledCronService = SpringUtil.getBean(ScheduledCronService.class);
        ScheduledCron scheduledCron = repositor.findByTaskClassName(this.getClass().getName());
        if(StatusEnum.DISABLE.getCode().equals(scheduledCron.getStatus()))
        {
            return ;
        }
        execute();
    }
}

所有定时任务类只需要实现这个接口,并在相应的数据库表种插入一条记录,那么在项目启动的时候,就会被自动注册到Spring的定时任务里,然后根据cron表达式去执行。

(4)、编写配置类

package com.myy.scheduled;

import com.myy.entity.ScheduledCron;
import com.myy.service.ScheduledCronService;
import com.myy.utils.SpringUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.Assert;

import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * @ClassName ScheduledConfig
 * @description: 定时任务配置类
 * @author: MYY
 * @create: 2021-11-11 11:11
 * @Version 1.0
 **/
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {

	/**
	*定时任务业务层接口
	*/
    @Autowired
    private ScheduledCronService scheduledCronService;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        for (ScheduledCron schedulecron: scheduledCronService.findAllScheduleTask())
        {
            Class<?> clazz;
            Object task;
            try {
                //返回与具有给定字符串名称的类或接口关联的Class对象
                clazz = Class.forName(schedulecron.getTaskClassName());
                //返回与给定对象类型唯一匹配的 bean 实例(如果有)
                task = SpringUtil.getBean(clazz);
            }catch (ClassNotFoundException e){
                throw new IllegalArgumentException("获取"+schedulecron.getTaskClassname()+"错误",e);
            }catch (BeansException e){
                throw new IllegalArgumentException(schedulecron.getTaskClassName()+"未交给Spring管理!",e);
            }
            //确定此Class对象表示的类或接口是否与指定的Class参数表示的类或接口相同,或者是其超类或超接口。
            // 如果是,则返回true  否则返回false 。
            // 如果此Class对象表示原始类型,则如果指定的Class参数正是此Class对象,则此方法返回true 否则返回false 。
            Assert.isAssignable(ScheduledTask.class,task.getClass());
            // 可以通过改变数据库数据进而实现动态改变执行周期
            taskRegistrar.addTriggerTask(((Runnable) task),
                triggerContext -> {
                    //获取定时触发器,这里可以每次从数据库获取最新记录,更新触发器,实现定时间隔的动态调整
                    String taskExpression= scheduledCronService.findByTaskClassName(schedulecron.getTaskClassName()).getTaskExpression();
                    return new CronTrigger(taskExpression).nextExecutionTime(triggerContext);
                }
            );
        }
    }

    @Bean
    public Executor taskExecutor() {
        //创建一个线程池,可以安排命令在给定的延迟后运行,或定期执行
        return Executors.newScheduledThreadPool(10);
    }
}

(5)、编写定时任务

package com.myy.task;

import com.myy.scheduled.ScheduledTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * @ClassName TestTask
 * @description: 测试定时任务
 * @author: MYY
 * @create: 2021-11-11 11:11
 * @Version 1.0
 **/
@Component
public class TestTask implements ScheduledTask {

    /**
     * 日志
     */
    private Logger logger = LoggerFactory.getLogger(SyncAlarmSourceTask.class);

    @Override
    public void execute() {
        logger.info("任务调度成功!");
    }
}

(6)、启动服务

项目跑起来之后就会根据数据库配置的Cron表达式执行定时任务。

spring boot 定时任务 service spring boot 定时任务 日志优雅_java

(7)、动态改变执行周期

做了个简单的页面,可以做到查看任务列表,修改任务cron表达式(也就实现了动态改变定时任务执行周期),暂停、启用定时任务、以及直接执行定时任务,这样后期正式环境维护开发就会很方便了。

spring boot 定时任务 service spring boot 定时任务 日志优雅_spring_02

/**
     * 执行定时任务
     */
    @ResponseBody
    @PostMapping(value = "/runTaskCron")
    public Result runTaskCron(String taskClassName){
        if (StringUtils.isNotEmpty(taskClassName))
        {
            try {
                ((ScheduledTask)SpringUtil.getBean(Class.forName(taskClassName))).execute();
            }catch (Exception e){
                logger.error("执行定时任务失败",e);
            }
        }
        return Result.ok("执行成功");
    }

spring boot 定时任务 service spring boot 定时任务 日志优雅_后端_03

文章仅用作记录分享,若有不当,还望指正。