前言
自己造轮子是件有趣的事情,自己手写了一个定时器管理器。使用的场景是有多个后台运行的定时任务的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 类