一、实现目标:
动态的添加修改删除cron定时任务,不需要重启应用,包括动态修改某个定时任务的cron定时时间以及任务内容的变化
二、实现思路:
1.设计存储任务信息的文件,此处采用json文件存储,认定task-xxx.json名称的文件为存储任务信息的文件,此处只做简单的日志打印
{
"cron" : "0 17 15 * * ?",
"taskInfo" : {
"taskName" : "myTask1",
"operation" : "创建定时任务"
}
}
2. 监控任务信息文件的变化,此处采用FileAlterationMonitor方式监控(这种方式比线程通知的方式要简单)
此种方式的缺点在于检测文件删除的时候会直接删除文件,这样就无法读取到文件内容
https://blog.csdn.net/qq_37549757/article/details/93882858
3.添加修改删除定时任务,采用ThreadPoolTaskScheduler方式去添加修改删除定时任务
https://blog.csdn.net/qq_37549757/article/details/93876875
下面的例子是在创建定时任务trigger的时候使用的cronTrigger定时器注册,实际当中可以按照需求来选择定时器
因为采用FileAlterationMonitor监控的时候,当发生的变化为删除文件时无法读取到文件的内容(组件会直接删除文件),所以说在存储定时任务的时候,key可以带上文件名(因为删除操作的时候只能拿到文件的一部分信息,拿不到内容),并在定时任务处理工具类中要添加以文件名删除任务的方法,并使用该方法去删除定时任务去解决此问题,如果任务名不存储在文件内容里则不需要在存储任务列表的时候key加上文件名,此处例子key=fileName_taskName
三、项目结构
1.model包:json文件bean模型
2.util包:
json文件转bean工具;
文件变化观察者工具;
定时任务处理工具(添加修改删除定时任务);
3.listener包:文件监听器去处理文件变化
4.config包:
启动项目时启动文件观察者及相关的配置
启动项目时为文件夹下面已经存在的任务信息文件添加定时任务
四、代码实现,采用springboot方式实现
1.项目需要额外依赖于commons-io(文件监听需要)和fastjson包(json转bean需要),当然springboot的start包是最基本的也需要
com.alibaba
fastjson
1.2.54
commons-io
commons-io
2.5
2.json文件对应的模型文件
package com.holidaylee.model;
/**
* @author : HolidayLee
* @description : 任务例子模型
*/
public class TaskExampleModel {
/**
* 定时时间表达式
*/
private String cron;
/**
* 任务信息
*/
private TaskInfoModel taskInfo;
public String getCron() {
return cron;
}
public void setCron(String cron) {
this.cron = cron;
}
public TaskInfoModel getTaskInfo() {
return taskInfo;
}
public void setTaskInfo(TaskInfoModel taskInfo) {
this.taskInfo = taskInfo;
}
}
package com.holidaylee.model;
/**
* @author : HolidayLee
* @description : 任务信息模型
*/
public class TaskInfoModel {
/**
* 任务名
*/
private String taskName;
/**
* 操作内容
*/
private String operation;
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
}
3.json文件转bean处理工具
package com.holidaylee.util;
import com.alibaba.fastjson.JSONObject;
import com.holidaylee.model.TaskExampleModel;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
/**
* @author : HolidayLee
* @description : 将任务信息json文件转换为模型
*/
@Component
public class ReadJsonTaskInfoToBeanUtils {
public TaskExampleModel convertFileToBean(File taskFile) {
TaskExampleModel task = null;
if (taskFile.isFile()) {
try {
String json = FileUtils.readFileToString(taskFile, "utf-8");
task = JSONObject.parseObject(json, TaskExampleModel.class);
} catch (IOException e) {
e.printStackTrace();
}
}
return task;
}
}
4.获取文件变化观察者工具
package com.holidaylee.util;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.concurrent.TimeUnit;
/**
* @author : HolidayLee
* @description : 获取指定目录下的指定前后缀目录的文件观察者
*/
@Component
public class FileListenerMonitorUtils {
/**
* 生成monitor,一intervalSeconds监听directory文件夹下面的以suffix结束,prefix开头的文件,并交由fileListener处理变化
*
* @param directory 监视的文件夹
* @param intervalSeconds 轮训时间
* @param suffix 监视文件的后缀
* @param prefix 监视文件的前缀
* @param fileListener 文件监听处理器
* @return 文件观察者
*/
public FileAlterationMonitor getMonitor(File directory, Long intervalSeconds, String prefix, String suffix, FileAlterationListenerAdaptor fileListener) {
long interval = TimeUnit.SECONDS.toMillis(intervalSeconds);
IOFileFilter directories = FileFilterUtils.and(FileFilterUtils.directoryFileFilter());
IOFileFilter suffixFilter = FileFilterUtils.suffixFileFilter(suffix);
IOFileFilter prefixFilter = FileFilterUtils.prefixFileFilter(prefix);
IOFileFilter files = FileFilterUtils.and(FileFilterUtils.fileFileFilter(),
suffixFilter, prefixFilter);
IOFileFilter filter = FileFilterUtils.or(directories, files);
FileAlterationObserver observer = new FileAlterationObserver(directory, filter);
observer.addListener(fileListener);
FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
return monitor;
}
}
5.定时任务处理工具(添加修改删除定时任务)
package com.holidaylee.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* @author : HolidayLee
* @description : 注册定时任务
*/
@Component
public class ScheduleTaskUtils {
private final static Logger logger = LoggerFactory.getLogger(ScheduleTaskUtils.class);
private Map> futuresMap;
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
/**
* 初始化任务列表
*/
@PostConstruct
public void init() {
futuresMap = new ConcurrentHashMap<>();
}
/**
* 创建ThreadPoolTaskScheduler线程池
*/
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(8);
return threadPoolTaskScheduler;
}
/**
* 添加定时任务,如果任务名重复则抛出异常
*
* @param task 任务
* @param trigger 定时器
* @param key 任务名
* @return 是否操作成功(成功 : true ; 失败 : false)
*/
public boolean addTask(Runnable task, Trigger trigger, String key) {
ScheduledFuture> future = threadPoolTaskScheduler.schedule(task, trigger);
ScheduledFuture oldScheduledFuture = futuresMap.put(key, future);
if (oldScheduledFuture == null) {
logger.info("添加定时任务成功:" + key);
return true;
} else {
throw new RuntimeException("添加任务key名: " + key + "重复");
}
}
/**
* 移除定时任务
*
* @param key 任务名
* @return 是否操作成功(成功 : true ; 失败 : false)
*/
public boolean removeTask(String key) {
ScheduledFuture toBeRemovedFuture = futuresMap.remove(key);
if (toBeRemovedFuture != null) {
toBeRemovedFuture.cancel(true);
return true;
} else {
return false;
}
}
/**
* 删除指定文件名开头的定时任务(任务名以文件名开头的)
*
* @param fileName 文件名
* @return 是否操作成功(成功 : true ; 失败 : false)
*/
public boolean removeTaskByFileName(String fileName) {
for (String key : futuresMap.keySet()) {
if (key.startsWith(fileName)) {
futuresMap.get(key).cancel(true);
futuresMap.remove(key);
logger.info("删除定时任务成功:" + key);
return true;
}
}
return false;
}
/**
* 更新定时任务
* 有可能会出现:1、旧的任务不存在,此时直接添加新任务;
* 2、旧的任务存在,先删除旧的任务,再添加新的任务
*
* @param task 任务
* @param trigger 定时器
* @param key 任务名称
* @return 是否操作成功(成功 : true ; 失败 : false)
*/
public boolean updateTask(Runnable task, Trigger trigger, String key) {
ScheduledFuture toBeRemovedFuture = futuresMap.remove(key);
// 存在则删除旧的任务
if (toBeRemovedFuture != null) {
toBeRemovedFuture.cancel(true);
logger.info("取消定时任务成功:" + key);
}
return addTask(task, trigger, key);
}
}
6.文件监听器
package com.holidaylee.listener;
import com.holidaylee.model.TaskExampleModel;
import com.holidaylee.util.ReadJsonTaskInfoToBeanUtils;
import com.holidaylee.util.ScheduleTaskUtils;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.File;
/**
* @author : HolidayLee
* @description : 处理任务信息文件发生的变化
*/
@Component
public class TaskFileListener extends FileAlterationListenerAdaptor {
private final static Logger logger = LoggerFactory.getLogger(TaskFileListener.class);
private final String TASK_ADD = "add";
private final String TASK_UPDATE = "update";
private final String TASK_REMOVE = "remove";
@Resource
private ReadJsonTaskInfoToBeanUtils readJsonTaskInfoToBeanUtils;
@Resource
private ScheduleTaskUtils scheduleTaskUtils;
@Override
public void onStart(FileAlterationObserver observer) {
//logger.info("观察文件变化任务开始");
}
@Override
public void onDirectoryCreate(File directory) {
logger.info("创建文件夹:" + directory);
}
@Override
public void onDirectoryChange(File directory) {
logger.info("修改文件夹:" + directory);
}
@Override
public void onDirectoryDelete(File directory) {
logger.info("删除文件夹:" + directory);
}
@Override
public void onFileCreate(File file) {
try {
logger.info("创建文件:" + file);
if (dealTaskFileChange(file, TASK_ADD)) {
logger.info("任务信息文件定时任务添加成功:" + file);
} else {
logger.info("任务信息文件定时任务添加成功:" + file);
}
} catch (Exception e) {
logger.error("任务文件处理失败", e);
}
}
@Override
public void onFileChange(File file) {
try {
logger.info("修改文件:" + file);
if (dealTaskFileChange(file, TASK_UPDATE)) {
logger.info("任务信息文件定时任务更新成功:" + file);
} else {
logger.info("任务信息文件定时任务更新成功:" + file);
}
} catch (Exception e) {
logger.error("任务文件处理失败", e);
}
}
@Override
public void onFileDelete(File file) {
try {
logger.info("删除文件:" + file);
if (dealTaskFileChange(file, TASK_REMOVE)) {
logger.info("任务信息文件定时任务删除成功:" + file);
} else {
logger.info("任务信息文件定时任务删除成功:" + file);
}
} catch (Exception e) {
logger.error("任务文件处理失败", e);
}
}
@Override
public void onStop(FileAlterationObserver observer) {
//logger.info("观察文件变化任务结束");
}
/**
* 处理任务信息文件的变化:增加修改删除
*
* @param file 任务信息文件
* @param changeType 修改类型
* @return 是否处理成功
*/
private boolean dealTaskFileChange(File file, String changeType) {
boolean result = false;
// 判断文件变化类型并做相应处理
if (TASK_REMOVE.equals(changeType)) {
// 删除任务只需要需要用文件名去删除,因为删除文件后读取不到文件的内容
result = scheduleTaskUtils.removeTaskByFileName(file.getName());
} else {
// 更新或删除文件时将json文件转换为模型
TaskExampleModel taskModel = readJsonTaskInfoToBeanUtils.convertFileToBean(file);
Trigger cronTrigger = new CronTrigger(taskModel.getCron());
Runnable task = new Runnable() {
@Override
public void run() {
logger.info(String.format("开始执行定时任务-->key[%s]:cron[%s]:operation[%s]", taskModel.getTaskInfo().getTaskName(), taskModel.getCron(), taskModel.getTaskInfo().getOperation()));
}
};
// 添加定时任务
if (TASK_ADD.equals(changeType)) {
result = scheduleTaskUtils.addTask(task, cronTrigger, file.getName() + "_" + taskModel.getTaskInfo().getTaskName());
}
// 更新定时任务(包括时间以及任务内容的更新)
if (TASK_UPDATE.equals(changeType)) {
logger.info("开始进行任务信息文件定时任务更新:" + file);
result = scheduleTaskUtils.updateTask(task, cronTrigger, file.getName() + "_" + taskModel.getTaskInfo().getTaskName());
}
}
return result;
}
}
7.项目启动时启动文件观察者
package com.holidaylee.config;
import com.holidaylee.listener.TaskFileListener;
import com.holidaylee.util.FileListenerMonitorUtils;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
/**
* @author : HolidayLee
* @description : 给任务信息文件添加文件观察者
*/
@Component
public class TaskFileMonitorConfig {
private final static Logger logger = LoggerFactory.getLogger(TaskFileMonitorConfig.class);
/**
* 任务信息文件存放目录
*/
public final static String taskInfoFileDir = "taskInfoFiles" + File.separatorChar;
/**
* 任务信息文件前缀
*/
public final static String taskFilePrefix = "task-";
/**
* 任务信息文件后缀
*/
public final static String taskFileSuffix = ".json";
/**
* 观察任务信息文件目录下的文件变化的轮询时间
*/
private final Long intervalSeconds = 5L;
@Resource
private FileListenerMonitorUtils fileListenerMonitorUtils;
@Resource
private TaskFileListener taskFileListener;
@PostConstruct
private void register() {
logger.info(String.format("开始为文件目录{%s}下的{%s}文件添加文件观察者", taskInfoFileDir, taskFilePrefix + "xxx" + taskFileSuffix));
File dir = new File(taskInfoFileDir);
if (dir.isDirectory()) {
try {
// 获取观察者
FileAlterationMonitor monitor = fileListenerMonitorUtils.getMonitor(dir, intervalSeconds, taskFilePrefix, taskFileSuffix, taskFileListener);
// 启动观察者
monitor.start();
logger.info("文件观察者添加并启动成功");
} catch (Exception e) {
logger.error("启动文件观察者失败", e);
}
} else {
throw new RuntimeException("非目录或目录不存在");
}
}
}
8.给项目启动时已经存在的任务文件添加定时任务
package com.holidaylee.config;
import com.holidaylee.model.TaskExampleModel;
import com.holidaylee.util.ReadJsonTaskInfoToBeanUtils;
import com.holidaylee.util.ScheduleTaskUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* @author : HolidayLee
* @description : 应用启动前给所有的任务信息文件添加定时任务
*/
@Component
public class TaskFileRegisterConfig {
private final static Logger logger = LoggerFactory.getLogger(TaskFileRegisterConfig.class);
@Resource
private ReadJsonTaskInfoToBeanUtils readJsonTaskInfoToBeanUtils;
@Resource
private ScheduleTaskUtils scheduleTaskUtils;
@PostConstruct
private void register() {
logger.info(String.format("开始为目录{%s}下的任务信息文件添加定时任务", TaskFileMonitorConfig.taskInfoFileDir));
File dir = new File(TaskFileMonitorConfig.taskInfoFileDir);
if (dir.isDirectory()) {
// 1.扫描任务信息文件,即taskInfoFileDir目录下以taskFilePrefix开头taskFileSuffix结尾的文件
List taskFileList = new ArrayList<>();
getTaskInfoFileList(dir, taskFileList);
// 2.给所有的任务文件注册定时任务
logger.info(String.format("从目录{%s}中获取到的文件列表为%s", dir, taskFileList));
for (File taskFile : taskFileList) {
TaskExampleModel taskModel = readJsonTaskInfoToBeanUtils.convertFileToBean(taskFile);
boolean result = registerTask(taskModel,taskFile.getName());
if (result) {
logger.info("任务信息文件定时任务添加成功:" + taskFile);
} else {
logger.warn("任务信息文件定时任务添加失败:" + taskFile);
}
}
} else {
throw new RuntimeException("非目录或目录不存在");
}
logger.info(String.format("目录{%s}下的所有任务信息文件添加定时任务成功", dir));
}
/**
* 递归获取指定目录下的所有任务信息文件,即以TaskFileMonitorConfig.taskFilePrefix开头以TaskFileMonitorConfig.taskFileSuffix结束的文件
* 此处不采用直接new ArrayList返回的原因在于无法确定递归的出口
*
* @param dir 任务文件存放目录
* @param taskFileList 任务列表
*/
private void getTaskInfoFileList(File dir, List taskFileList) {
if (!dir.isDirectory()) {
throw new RuntimeException("非目录或目录不存在");
}
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) {
// file为目录则掉入子目录去查找任务信息文件
this.getTaskInfoFileList(file, taskFileList);
} else {
// 满足条件则加入任务信息文件列表
if (file.getName().startsWith(TaskFileMonitorConfig.taskFilePrefix) && file.getName().endsWith(TaskFileMonitorConfig.taskFileSuffix)) {
taskFileList.add(file);
}
}
}
}
/**
* 给读取到的任务文件信息添加定时任务,此处只打印日志,实际中根据业务需要来
*
* @param taskModel 任务信息模型
* @return 任务是否添加成功(成功 : true ; 失败 : false)
*/
private boolean registerTask(TaskExampleModel taskModel,String fileName) {
// 1.创建任务
Runnable task = new Runnable() {
@Override
public void run() {
logger.info(String.format("开始执行定时任务-->key[%s]:cron[%s]:operation[%s]", taskModel.getTaskInfo().getTaskName(), taskModel.getCron(), taskModel.getTaskInfo().getOperation()));
}
};
// 2.创建定时器
Trigger cronTrigger = new CronTrigger(taskModel.getCron());
// 3.添加定时任务
return scheduleTaskUtils.addTask(task, cronTrigger, fileName + "_" + taskModel.getTaskInfo().getTaskName());
}
}
五、查看效果(增加修改删除定时任务)
说明:Runnable中的run方法只打印了json文件中的内容(实际当中可根据业务需求去修改),所以查看效果的时候可以根据打印日志的时间去看cron的变化,operation看任务内容的变化
1.启动的时候taskInfoFiles文件夹下面存在一个任务task-example.json文件
2.此时修改task-example.json中的cron以及operation
3.在文件夹taskInfoFiles里面添加一个task-example2.json
4.删除文件夹taskInfoFiles里面的task-example2.json文件
本文为学习记录笔记,不喜勿喷。