springboot不重启应用动态添加修改删除定时任务(以cron定时方式为例)

一、实现目标:

    动态的添加修改删除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不重启应用动态添加修改删除定时任务(以cron定时方式为例)_第1张图片

四、代码实现,采用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文件

springboot不重启应用动态添加修改删除定时任务(以cron定时方式为例)_第2张图片

springboot不重启应用动态添加修改删除定时任务(以cron定时方式为例)_第3张图片

2.此时修改task-example.json中的cron以及operation

springboot不重启应用动态添加修改删除定时任务(以cron定时方式为例)_第4张图片

3.在文件夹taskInfoFiles里面添加一个task-example2.json

springboot不重启应用动态添加修改删除定时任务(以cron定时方式为例)_第5张图片

4.删除文件夹taskInfoFiles里面的task-example2.json文件

注明:

本文为学习记录笔记,不喜勿喷。

你可能感兴趣的:(springboot定时任务,动态定时任务,动态cron任务,动态添加修改删除定时定时任务)