定时任务管理器工具

  • Post author:
  • Post category:其他




前言

自己造轮子是件有趣的事情,自己手写了一个定时器管理器。使用的场景是有多个后台运行的定时任务的web项目,愿景是让定时器执行过程可视化,可以在界面控制每一个定时任务,进行开关,立刻执行任务等操作。

在这里插入图片描述



功能

  • 这个容器可以非常方便的管理多个定时任务,可以动态的在内存修改配置,修改后立马生效。无需重启项目。对于某个定时任务都可以独立配置
  • 可以动态的设置任务的开关
  • 动态改变定时任务的时间间隔
  • 控制定时任务周一到周五是否执行或者不执行 (比如对于短信通知任务可以设置双休时不发送)
  • 指定未来的某天定时任务是否执行(比如可以设置节假日不执行定时任务)
  • 检测定时任务当前的执行状态
  • 如果定时任务正在执行,那么会跳过本次执行
  • 检测定时任务下次执行的时间
  • 可以手动立刻执行务 (比如需要立刻更新某些状态,可以直接手动执行)
  • 提供了任务启动后,任务发生错误时,开启和关闭任务后的回调接口(比如定时任务发生异常时可以短信通知)
  • 可以查看每个定时任务的日志记录,可以设置日志数量(队列实现,存储在内存中,设置数量不会导致内存爆炸)
  • 可以设置任务执行的有效次数,比如我只想执行3次,执行完3次后,后面就不执行了
  • 可以动态的添加定时任务,定时任务也可动态的配置

提供操作界面,需要控制层程序支持。容器提供的相关动态的修改内存的配置,即可做到上面的控制

容器可以监听器中初始化,需要调用register方法注册定时任务(继承 com.zjca.bss.timer.TimerTask 就是定时任务类,可以注册到该定时容器)



代码



TaskConfig 配置类

package com.zjca.bss.timer;

import java.util.Date;
import java.util.LinkedList;

/**
 * @Auther: ycl
 * @Date: 2019/4/15 11:31
 * @Description:定时任务配置
 */
public class TaskConfig {
    /**
     * 定时器的有效性,默认有效,当为false时,即使时间到了,也不会触发任务
     * 这个属性优先级最高,如果为false,下面属性全部不生效
     */
    private boolean enable = true;

    /**
     * 是否在定时器管理容器初始化时就执行一次任务,默认在启动时不执行
     */
    private boolean executeOnContainerBootstrap = false;

    /**
     * 启动的小时数,24小时制。0 - 23
     */
    private Integer startHour;

    /**
     * 启动的分钟数  0 - 60
     */
    private Integer startMinute;

    /**
     * 启动的秒数  0 - 60
     */
    private Integer startSecond;

    /**
     * 间隔执行时间,单位是毫秒
     */
    private long interval;

    /**
     * 一个星期7天,对应着7个boolean值,默认每天都有效,比如就星期二到星期五执行,周一和周六周日不执行就设置成
     *{false,true,true,true,true,false,false}
     */
    private boolean[] dayEnable = new boolean[]{true,true,true,true,true,true,true};

    /**
     * 最大有效次数
     * null表示无限次数
     * 比如我只想定时器任务只执行,三次,设置成3就可以了,执行完三次之后就将任务设置成不可用
     */
    private Integer effectiveNum = null;

    /**
     * 不执行的日期。这个优先级大于 dayEnable。
     * 可以动态的添加数据,添加指定日期,到了那天,定时器都不会执行
     * 过期日期自动删除
     */
    public LinkedList<Date> notExecuteDateList = new LinkedList<>();

    /**
     * 最大日志数量,定时器可以存储执行过程的一些日志,当日志数量大于设置的数量时,会删掉以前的,以队列的形式存在
     * 默认存储20条记录
     */
    private int maxLogNum = 20;

    public TaskConfig(int startHour, int startMinute, int startSecond, long interval) {
        this.startHour = startHour;
        this.startMinute = startMinute;
        this.startSecond = startSecond;
        this.interval = interval;
    }

    public TaskConfig(long interval) {
        this.interval = interval;
    }

    public boolean isEnable() {
        return enable;
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public boolean isExecuteOnContainerBootstrap() {
        return executeOnContainerBootstrap;
    }

    public void setExecuteOnContainerBootstrap(boolean executeOnContainerBootstrap) {
        this.executeOnContainerBootstrap = executeOnContainerBootstrap;
    }

    public long getInterval() {
        return interval;
    }

    public void setInterval(long interval) {
        this.interval = interval;
    }

    public boolean[] getDayEnable() {
        return dayEnable;
    }

    public void setDayEnable(boolean[] dayEnable) {
        this.dayEnable = dayEnable;
    }

    public Integer getEffectiveNum() {
        return effectiveNum;
    }

    public void setEffectiveNum(Integer effectiveNum) {
        this.effectiveNum = effectiveNum;
    }

    public LinkedList<Date> getNotExecuteDateList() {
        return notExecuteDateList;
    }

    public void setNotExecuteDateList(LinkedList<Date> notExecuteDateList) {
        this.notExecuteDateList = notExecuteDateList;
    }

    public int getMaxLogNum() {
        return maxLogNum;
    }

    public void setMaxLogNum(int maxLogNum) {
        this.maxLogNum = maxLogNum;
    }

    public Integer getStartHour() {
        return startHour;
    }

    public Integer getStartMinute() {
        return startMinute;
    }

    public Integer getStartSecond() {
        return startSecond;
    }
}



TimerTask 定时任务类

需要继承该类,实现

public abstract TaskRunResultDTO task(TaskConfig config) 抽象方法

在task方法中编写具体的业务逻辑,可以覆盖onStart onClose onError等回调方法

package com.zjca.bss.timer;

import com.zjca.bss.utils.DateUtils;

import java.util.*;

/**
 * @Auther: ycl
 * @Date: 2019/4/15 11:30
 * @Description:定时任务
 */
public abstract class TimerTask implements Runnable {
    /**
     * 是否正在运行
     */
    private volatile boolean running = false;
    /**
     * 任务id
     */
    private String id;

    /**
     * 任务名称
     */
    private String taskName;

    /**
     * 任务执行次数
     */
    private int taskExecuteNum;

    /**
     * 最后执行时间
     */
    private Date lastTime;

    /**
     * 下次执行时间
     */
    private Long nextExecuteTime;

    /**
     * 间隔数
     */
    private Long interval;

    /**
     * 任务配置
     */
    private TaskConfig config;


    /**
     * 执行日志
     */
    private Queue<String> logList = new LinkedList<String>();


    /**
     * @param taskName          定时任务名称
     * @param config            定时任务配置
     * @throws TaskException
     */
    public TimerTask(String taskName,TaskConfig config) throws TaskException {
        String check = checkConfig(config);
        if(check != null){
            throw new TaskException(check);
        }
        this.taskName = taskName;
        this.config = config;
        this.id = UUID.randomUUID().toString().replaceAll("-", "");
        //设置下一次启动时间
        Date date = DateUtils.createTime(null,null,null,config.getStartHour(),config.getStartMinute(),config.getStartSecond());

        interval =config.getInterval();
        if(System.currentTimeMillis() < date.getTime()){
            nextExecuteTime = date.getTime();
        }else if(System.currentTimeMillis() > date.getTime() + interval){
            //启动时间过时
            nextExecuteTime = System.currentTimeMillis() + interval;
        }else {
            nextExecuteTime = date.getTime() + interval;
        }
    }

    /**
     * 校验配置是否正确,不正确返回具体的原因,如果正确,返回Null
     * @param config
     * @return
     */
    private String checkConfig(TaskConfig config){
        return null;
    }


    @Override
    public void run() {
        //更新执行的信息
        taskExecuteNum ++;
        lastTime = new Date();
        running = true;
        //计算下次执行时间间隔
        nextExecuteTime = lastTime.getTime() +  interval;

        try {
            addLog("定时任务开始执行");
            TaskRunResultDTO result = task(config);
            addLog("定时任务开始完毕");
            after(config, result);
        }catch (Throwable e){
            addLog("定时任务执行出错");
            try {
                onError(config, e);
            }catch (Exception e1){
                addLog("onError回调出错:" + e1.toString());
            }
        }finally {
            running = false;
        }
    }

    //--------------------------主要的定时任务-------------------------

    /**
     * 要执行的定时任务
     * @param config
     * @return              TaskRunResultDTO 定时任务执行返回的结果,可以是null。但是如果有回调 after 函数,需要具体的设置值运行。
     * @throws Exception
     */
    public abstract TaskRunResultDTO task(TaskConfig config);

    //--------------------------一些回调方法,需要回调就覆盖该方法-------------------------
    /**
     * 在定时任务完成之后回调。调用 task 且没有任何错误后就会调用after
     * 需要回调就覆盖该方法
     * @param config
     */
    public void after(TaskConfig config,TaskRunResultDTO dto){}

    /**
     * 执行任务时发生错误会执行onError方法。
     * 需要回调就覆盖该方法
     * @param config
     */
    public void onError(TaskConfig config,Throwable error) {}

    /**
     * 关闭定时任务时回调
     * 需要回调就覆盖该方法
     * @param config
     */
    public void onClose(TaskConfig config){}

    /**
     * 开启定时任务时回调
     * 需要回调就覆盖该方法
     * @param config
     */
    public  void onStart(TaskConfig config){}

    /**
     * 添加日志
     */
    public void addLog(String msg){
        if(logList.size() >= config.getMaxLogNum()){
            logList.poll();
        }
        logList.offer(DateUtils.format(new Date()) + ": [" +  this.taskName + "]" + msg);
    }

    /**
     * 开启定时器任务,并不是执行
     */
    public void start(){
        config.setEnable(true);
        addLog("开启定时任务成功");
        try {
            onStart(config);
        }catch (Exception e){
            addLog("onStart回调出错:" + e.toString());
        }

    }

    /**
     * 关闭定时器任务,可以开启
     */
    public void close(){
        config.setEnable(false);
        addLog("关闭定时任务成功");
        try {
            onClose(config);
        }catch (Exception e){
            addLog("onClose回调出错:" + e.toString());
        }
    }

    /**
     * 添加不执行定时器的日期,比如手动操作,添加节假日不用执行发送审核提醒的定时器
     * @param date
     * @throws TaskException
     */
    public void addNotExecuteDate(Date date) throws TaskException {
        if(date == null){
            throw new TaskException("过时日期不能为空");
        }
        /**
         *
         */
        /*if(DateUtils.getDiffDay(date,null)<= 0){
            throw new TaskException("过时日期不能添加,只能添加今天之后的日期");
        }*/
        config.notExecuteDateList.add(date);
        addLog("添加不执行的日期成功,日期为:" + DateUtils.format(date,DateUtils.FORMAT_DATE_ONLY));
    }

    /**
     * 批量添加不执行定时器的日期
     * @param dates
     * @throws TaskException
     */
    public void addNotExecuteDate(Date[] dates) throws TaskException {
        if(dates == null || dates.length == 0){
            throw new TaskException("过时日期不能为空");
        }
        for(Date date:dates){
            addNotExecuteDate(date);
        }
    }
	/**
     * 更新下次执行时间
     * @param time  如果为null 默认+1天
     */
    public void updateNextExecuteTime(Long time){
        if(time == null){
            time = 86400000L;//24小时的毫秒时间
        }
        nextExecuteTime += time;
        nextExecuteTimeStr = DateUtils.format(new Date(nextExecuteTime));
    }
    /**
 
     * 立刻启动定时任务。
     * 启动定时任务并不是多线程启动,而是调用这个方法的线程去执行这个定时任务
     * 立刻执行定时任务不会受到配置的影响,也不会影响下一次定时任务的执行。
     * 如果需要自定义,请重写该方法
     */
    public TaskRunResultDTO executeImmediately() {
        addLog("手动调用,立刻执行!");
        TaskRunResultDTO resultDTO = null;
        try {
            running = true;
            resultDTO = task(config);
        } catch (Exception e) {
            addLog("手动调用执行失败!详情:" + e.toString());
        }finally {
            running = false;
        }
        return resultDTO;
    }


    public boolean isRunning() {
        return running;
    }

    public String getId() {
        return id;
    }

    public String getTaskName() {
        return taskName;
    }

    public int getTaskExecuteNum() {
        return taskExecuteNum;
    }

    public Date getLastTime() {
        return lastTime;
    }

    public Long getNextExecuteTime() {
        return nextExecuteTime;
    }

    public Long getInterval() {
        return interval;
    }

    public TaskConfig getConfig() {
        return config;
    }

    public Queue<String> getLogList() {
        return logList;
    }
}



TimerTaskContainer 定任务容器类

继承了TimerTask类的定时任务 注册到该容器即可管理容器内的任务

package com.zjca.bss.timer;

import com.zjca.bss.utils.DateUtils;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Auther: ycl
 * @Date: 2019/4/15 11:51
 * @Description:定时器容器
 * 这个容器可以非常方便的管理多个定时任务,可以动态的在内存修改配置,修改后立马生效。无需重启项目。对于某个定时任务可以管理以下配置
 * 并且可以动态的设置任务的开关
 * 动态改变定时任务的时间间隔
 * 控制定时任务周一到周五是否执行或者不执行 (比如对于短信通知任务可以设置双休时不发送)
 * 指定未来的某天定时任务是否执行,(比如可以设置节假日不执行定时任务)
 * 检测定时任务当前的执行状态
 * 检测定时任务下次执行的时间
 * 可以手动立刻执行务 (比如需要立刻更新某些状态,可以直接手动执行)
 * 提供了任务启动后,任务发生错误时,开启和关闭任务后的回调接口(比如定时任务发生异常时可以短信通知)
 * 可以查看每个定时任务的日志记录,可以设置日志数量(队列实现,存储在内存中,设置数量不会导致内存爆炸)
 * 可以设置任务执行的有效次数,比如我只想执行3次,执行完3次后,后面就不执行了
 * 可以动态的添加定时任务,定时任务也可动态的配置
 *
 *
 * 提供操作界面,需要控制层程序支持。容器提供的相关动态的修改内存的配置,即可做到上面的控制
 * 容器可以监听器中初始化,需要调用register方法注册定时任务(继承 com.zjca.bss.timer.TimerTask 就是定时任务类,可以注册到该定时容器)
 */
public class TimerTaskContainer{
    private static TimerTaskContainer instance;
    private Timer timer;
    /**
     * 索引map通过任务id快速找到任务对象
     */
    Map<String,TimerTask> indexMap = new HashMap<String,TimerTask>();

    /**
     * 任务列表
     */
    List<TimerTask> taskList = new ArrayList<TimerTask>();

    /**
     * 执行定时任务的线程池
     */
    ExecutorService executorService;

    private TimerTaskContainer(){}
    public static TimerTaskContainer getInstance(){
        if(instance == null){
            synchronized (TimerTaskContainer.class){
                if(instance == null){
                    instance = new TimerTaskContainer();
                }
            }
        }
        return instance;
    }

    //注册定时任务
    public void register(TimerTask task) {
        taskList.add(task);
        indexMap.put(task.getId(),task);
        if(task.getConfig().isExecuteOnContainerBootstrap()){
            task.executeImmediately();
        }
    }

    /**
     * 启动定时器容器
     */
    public void bootstrap(){
        if(taskList == null || taskList.size() == 0){
            return;
        }

        /**
         * 定时器管理器是一个定时任务线程
         * 遍历每一个用户定义的任务类,读取配置信息,如果满足执行条件,就开启新线程,去执行这个任务
         */
        timer = new Timer();
        executorService = Executors.newFixedThreadPool(5);
        timer.schedule(new java.util.TimerTask() {
            public void run() {
                taskListLoop:
                for(TimerTask task:taskList){
                    if(task == null){
                        continue ;
                    }
                    TaskConfig config = task.getConfig();
                    //判断是否到执行的时间点了。比如定时任务的下次执行时间是昨天早上8点,但是昨天设置了不执行,这个时间没有更新,今天早上8点要执行了,依然满足这个条件。今天会继续执行
                    if(task.getNextExecuteTime() > System.currentTimeMillis()){
                        continue ;
                    }
                    //定时器总的开关
                    if(!config.isEnable()){
                        continue;
                    }
                     //指定了星期不执行
                    if(!config.getDayEnable()[DateUtils.getDayOfWeek(null) - 1]){
                        //下次执行时间加1天
                        task.updateNextExecuteTime(null);
                        continue;
                    }

                    //是否指定了今天不执行
                   List<Date> list = config.getNotExecuteDateList();
                    if(list != null || list.size() > 0){
                        Iterator<Date> iterator = list.listIterator();
                        while (iterator.hasNext()){
                            Date date = iterator.next();
                            int d = DateUtils.getDiffDay(date,null);
                            if(d == 0){
                                //当天不执行,下次执行时间加到1天后
                                task.updateNextExecuteTime(null);
                                continue taskListLoop;
                            }else if(d < 0){
                                //过时,删掉
                                iterator.remove();
                            }
                        }
                    }

                    //是否执行完了最大次数
                    if(config.getEffectiveNum() != null && task.getTaskExecuteNum() >= config.getEffectiveNum()){
                        //将定时器设置成不可用
                        config.setEnable(false);
                        task.addLog("有效次数执行完毕,本次执行跳过");
                        continue ;
                    }

                    //如果正在运行,跳过本次执行
                    if(task.isRunning()){
                        task.addLog("检测到任务正在执行,本次跳过");
                        continue ;
                    }
                    //执行定时任务
                    executorService.execute(task);
                }
            }
        }, new Date(),1000);
    }

    /**
     * 关闭定时容器
     * 释放定时线程和任务执行线程,正在执行的任务会尝试等它执行完毕之后再关掉该线程
     * 关闭之后还能启动哦
     */
    public void close(){
        if(timer != null){
            timer.cancel();
        }
        if(executorService != null){
            executorService.shutdown();
        }
    }

    public Map<String, TimerTask> getIndexMap() {
        return indexMap;
    }

    public List<TimerTask> getTaskList() {
        return taskList;
    }
}



TaskException 定时器任务异常类

package com.zjca.bss.timer;

/**
 * @Auther: ycl
 * @Date: 2019/4/15 11:32
 * @Description:
 */
public class TaskException extends RuntimeException {
    /**
     * Constructs a new exception with the specified detail message.  The
     * cause is not initialized, and may subsequently be initialized by
     * a call to {@link #initCause}.
     *
     * @param message the detail message. The detail message is saved for
     *                later retrieval by the {@link #getMessage()} method.
     */
    public TaskException(String message) {
        super(message);
    }

    /**
     * Constructs a new exception with the specified detail message and
     * cause.  <p>Note that the detail message associated with
     * {@code cause} is <i>not</i> automatically incorporated in
     * this exception's detail message.
     *
     * @param message the detail message (which is saved for later retrieval
     *                by the {@link #getMessage()} method).
     * @param cause   the cause (which is saved for later retrieval by the
     *                {@link #getCause()} method).  (A <tt>null</tt> value is
     *                permitted, and indicates that the cause is nonexistent or
     *                unknown.)
     * @since 1.4
     */
    public TaskException(String message, Throwable cause) {
        super(message, cause);
    }
}



TaskRunResultDTO 回调返回结果封装类

package com.zjca.bss.timer;

/**
 * @Auther: ycl
 * @Date: 2019/4/15 15:44
 * @Description:
 */
public class TaskRunResultDTO {
    private Integer code;
    private String msg;
    private Object value;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }
}



初始化定时器容器

在监听器中配置并注册定时任务到定时管理容器中

package com.zjca.bss.listener;

import com.zjca.bss.task.OverdueCertCheckTask;
import com.zjca.bss.task.WaitAuditRegNotifyTask;
import com.zjca.bss.task.AlipayDataSyncTask;
import com.zjca.bss.timer.TaskConfig;
import com.zjca.bss.timer.TimerTaskContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * @Auther: ycl
 * @Date: 2019/4/16 14:02
 * @Description: 定时任务监听器
 */
public class TimerTaskListener implements ServletContextListener {
    private final static Logger logger = LoggerFactory.getLogger(TimerTaskListener.class);

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        TimerTaskContainer timerTaskContainer = TimerTaskContainer.getInstance();
        //----------------未审核申请表通知 定时器任务----------------
        //每天早上8点,0分,0秒(24小时执行一次)
        TaskConfig waitAuditRegNotifyTaskConfig = new TaskConfig(8, 0, 0, 24 * 3600 * 1000);
        //设置周1到周5执行,周六,周日不执行
        waitAuditRegNotifyTaskConfig.setDayEnable(new boolean[]{true, true, true, true, true, false, false});
        //注册未审核申请表通知定时器
        timerTaskContainer.register(new WaitAuditRegNotifyTask("未审核申请表通知", waitAuditRegNotifyTaskConfig));

        //----------------过期证书检测定时器任务----------------
        //每天凌晨2点对所有正常证书进行循环迭代。
        TaskConfig overdueCertCheckTaskConfig = new TaskConfig(2, 0, 0, 24 * 3600 * 1000);
        //注册过期证书检测定时任务
        timerTaskContainer.register(new OverdueCertCheckTask("过期证书检测", overdueCertCheckTaskConfig));

        //----------------支付宝数据同步定时器任务----------------
        //5分钟执行一次
        TaskConfig alipayDataSyncTaskConfig = new TaskConfig(5 * 60 * 1000);
        timerTaskContainer.register(new AlipayDataSyncTask("支付宝数据检测", alipayDataSyncTaskConfig));

        //启动定时器任务管理器
        timerTaskContainer.bootstrap();
        logger.info(">>>>>>>>>>>>>定时器管理器启动成功>>>>>>>>>>>>>>>>");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        TimerTaskContainer.getInstance().close();
    }
}



原理

只有一个定时线程每一秒遍历定时任务集合,判断是否达到执行条件,达到执行条件就会启用一个线程执行该定时任务

所以定时任务配置被改变后容器都能时时检测到,根据不同的条件去判断是否要执行这个定时任务。

定时任务类对象有一些方法可以操作定时器的开关、立即执行等操作,其他一些功能还需要完善,无非就是修改变量数据。

定时任务注册时,会生成一个UUID作为该任务的ID,任务名称在创建任务对象时需要指定。任务存储在容器的这两个变量中

/**
 * 索引map通过任务id快速找到任务对象
 */
Map<String,TimerTask> indexMap = new HashMap<String,TimerTask>();

/**
 * 任务列表
 */
List<TimerTask> taskList = new ArrayList<TimerTask>();

后续步骤,就是自己写一个控制层方法,通过定期器容器拿数据,动态的设置配置就OK了,具体支持哪些配置看 TaskConfig 类



版权声明:本文为qq_32808649原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。