我之前用过 quartz
实现动态定时任务,但是这次使用 springboot 内置的定时任务来实现动态定时任务,如果想要用 quartz
实现动态定时任务,请阅读
我在项目里是做成了一个自定义的 task-spring-boot-starter,方便其他地方使用,引入依赖
io.github.chichengyu
task-spring-boot-starter
1.2.7.RELEASE
即可使用。下面进入正题
正题
首先创建新的springboot工作,导包
tk.mybatis
mapper-spring-boot-starter
2.1.5
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-web
com.alibaba
fastjson
1.2.71
org.projectlombok
lombok
true
application.yml
配置
server:
port: 8888
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/task?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT
username: root
password: root
mybatis:
type-aliases-package: com.example.demo.pojo
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
cache-enabled: true #使全局的映射器启用或禁用缓存。
lazy-loading-enabled: true #全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。
aggressive-lazy-loading: true #当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。
jdbc-type-for-null: null #设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型
第一步先创建一个工具类 SpringContextUtils
package com.example.demo;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author xiaochi
* @date 2022/6/11 11:02
* @desc SpringContextUtils
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
public static Object getBean(String name){
return applicationContext.getBean(name);
}
public static T getBean(Class clazz){
return applicationContext.getBean(clazz);
}
public static T getBean(String name,Class requiredType){
return applicationContext.getBean(name,requiredType);
}
public static boolean containsBean(String name){
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class> getType(String name){
return applicationContext.getType(name);
}
public static Map getBeansOfType(Class clazz){
return applicationContext.getBeansOfType(clazz);
}
}
创建定时任务管理工具类 SchudleManager
package com.example.demo.task;
import com.alibaba.fastjson.JSON;
import com.example.demo.dao.ScheduleJobLogDao;
import com.example.demo.pojo.ScheduleJob;
import com.example.demo.pojo.ScheduleJobLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.task.TaskSchedulerBuilder;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* @author xiaochi
* @date 2022/6/11 16:52
* @desc SchudleManager
*/
@Slf4j
@Component
public class SchudleManager implements DisposableBean {
// 正在运行的定时任务
private ConcurrentHashMap taskContainer = new ConcurrentHashMap<>();
private TaskScheduler taskScheduler;
@Autowired
private ScheduleJobLogDao scheduleJobLogDao;
@PostConstruct
public void init(){
ThreadPoolTaskScheduler taskScheduler = new TaskSchedulerBuilder()
.poolSize(10)
.threadNamePrefix("job_")
.build();
taskScheduler.setRemoveOnCancelPolicy(true);//是否将取消后的任务从队列删除
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.initialize();
this.taskScheduler = taskScheduler;
}
/**
* 刷新所有定时任务
*/
public void refresh(List taskPojoList) throws Exception {
if (!taskPojoList.isEmpty()){
destroy();
for (ScheduleJob pojo : taskPojoList){
addCronTask(pojo);
}
}
}
/**
* 创建并启动定时任务
* @param pojo
*/
public void addCronTask(ScheduleJob pojo){
cancel(pojo.getJobId());
/*CronTask cronTask = new CronTask(() -> {
System.out.println("执行定时器【"+ pojo.getName() +"】任务:" + LocalDateTime.now().toLocalTime());
},pojo.getCron());*/
CronTask cronTask = new CronTask(() -> execute(pojo),pojo.getCronExpression());
ScheduledRealTaskFuture realTaskFuture = new ScheduledRealTaskFuture();
realTaskFuture.future = taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
taskContainer.put(pojo.getJobId(),realTaskFuture);
}
/**
* 更新定时任务
*/
public void updateCronTask(ScheduleJob pojo) throws Exception {
cancel(pojo.getJobId());
addCronTask(pojo);
}
/**
* 立即执行定时任务
* @param scheduleJob
*/
public void runNow(ScheduleJob scheduleJob){
if (scheduleJob != null){
execute(scheduleJob);
}
}
/**
* 取消定时任务
* @param jobId
*/
public void cancel(Long jobId){
if (taskContainer.containsKey(jobId)){
ScheduledRealTaskFuture taskFuture = taskContainer.get(jobId);
if (taskFuture != null){
taskFuture.cancel();
}
taskContainer.remove(jobId);
}
}
/**
* 销毁的时候停止定时任务
*/
@Override
public void destroy(){
for (ScheduledRealTaskFuture future : taskContainer.values()){
future.cancel();
}
taskContainer.clear();// 清空容器
}
/**
* 取消任务的 ScheduledFuture
*/
private static class ScheduledRealTaskFuture{
public volatile ScheduledFuture> future;
/**
* 取消定时任务
*/
public void cancel() {
ScheduledFuture> future = this.future;
if (future != null) {
future.cancel(true);
}
}
}
/**
* 定时任务执行且日志入库
* @param scheduleJob
*/
private void execute(ScheduleJob scheduleJob){
//数据库保存执行记录
ScheduleJobLog jobLog = ScheduleJobLog.builder().jobId(scheduleJob.getJobId()).beanName(scheduleJob.getBeanName()).params(scheduleJob.getParams()).createTime(new Date()).build();
// 任务开始执行时间
long startTime = System.currentTimeMillis();
try {
log.info("定时任务[{}]准备执行", scheduleJob.getJobId());
Object target = SpringContextUtils.getBean(scheduleJob.getBeanName());
Method method = target.getClass().getDeclaredMethod("run", String.class);
R> result = (R>)method.invoke(target, JSON.toJSONString(scheduleJob));
// 任务执行时长
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes((int) times);
jobLog.setStatus(0);
if (null != result) {
jobLog.setStatus(result.getCode());
jobLog.setMessage(result.getMsg());
}
log.info("定时任务[{}]执行完毕,总共耗时:{}毫秒",scheduleJob.getJobId(),times);
}catch (Exception e){
log.error("定时任务[{}]执行失败,异常信息:{}",scheduleJob.getJobId(),e);
// 任务执行时长
long times = System.currentTimeMillis() - startTime;
// 任务状态 0:成功 1:失败 记录数据库
jobLog.setTimes((int)times);
jobLog.setStatus(1);
jobLog.setError(e.toString());
}finally {
// 最终记录到数据库
scheduleJobLogDao.insertSelective(jobLog);
}
}
}
这时候再继续创建定时任务接口,所有定时任务都要实现该接口 ITask
package com.example.demo.task;
/** 定时任务接口,所有定时任务都要实现该接口
* @author xiaochi
* @date 2022/6/11 16:33
* @desc ITask
*/
public interface ITask {
/**
* 执行定时任务接口
* @param params 参数,多参数使用JSON数据
* @return R
*/
R run(String params);
}
接着创建定时任务配置类,主要就是项目启动的时候去加载数据库的定时任务,配置类 SchedulingConfig
package com.example.demo.task.config;
import com.example.demo.job.dao.ScheduleJobDao;
import com.example.demo.job.pojo.ScheduleJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.List;
/** 定时任务配置(启动时执行,跟 CommandLineRunner 效果一样)
* @author xiaochi
* @date 2022/6/11 9:19
* @desc SchedulingConfig
*/
@Slf4j
@Configuration
@EnableScheduling // 必须开启定时任务,才能执行
public class SchedulingConfig implements SchedulingConfigurer {
@Autowired
private ScheduleJobDao scheduleJobDao;
@Autowired
private SchudleManager schudleManager;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
log.info("[定时任务将在30s后执行] after 30s .");
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.schedule(() -> {
log.info("[定时任务开始执行] start running");
try {
ScheduleJob job = ScheduleJob.builder().status(0).build();
schudleManager.refresh(scheduleJobDao.select(job));
} catch (Exception e) {
log.error("[加载定时任务失败],异常信息:{}",e);
}
},30, TimeUnit.SECONDS);
executorService.shutdown();
/*List taskPojoList = schudleTaskMapper.selectAll();
//taskRegistrar.addFixedDelayTask(() -> System.out.println(1),0);
for (SchudleTaskPojo pojo : taskPojoList){
taskRegistrar.addTriggerTask(() -> {
System.out.println("执行定时器【"+ pojo.getName() +"】任务:" + LocalDateTime.now().toLocalTime());
},triggerContext -> {
System.out.println(1);
return new CronTrigger(pojo.getCron()).nextExecutionTime(triggerContext);
});
}*/
}
}
接下来就是创建测试类进行测试了 TaskControlelr
,还有测试的定时任务bean(需要实现 ITask
)
测试的定时任务bean(需要实现 ITask
) TestTask
package com.example.demo.task;
import org.springframework.stereotype.Component;
/**
* @author xiaochi
* @date 2022/6/11 16:37
* @desc TestTask
*/
@Component("testTask")// 该名称必须与我们自定义的定时器表中的 Bean 名称一致
public class TestTask implements ITask {
/**
* 执行定时任务接口
* @param params 参数,多参数使用JSON数据
* @return R
*/
@Override
public R run(String params) {
System.out.println("测试定时任务执行了,参数:"+params);
return R.ok();
}
}
测试 TaskControlelr
package com.example.demo.task.controller;
import com.example.demo.dao.ScheduleJobDao;
import com.example.demo.pojo.ScheduleJob;
import com.example.demo.task.SchudleManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @author xiaochi
* @date 2022/6/11 9:34
* @desc TaskControlelr
*/
@RestController
@RequestMapping("/task")
public class TaskControlelr {
@Autowired
private ScheduleJobDao scheduleJobDao;
@Autowired
private SchudleManager schudleManager;
/**
* 刷新定时任务
* @return
*/
@GetMapping("/refresh")
public String refresh() throws Exception {
schudleManager.refresh(scheduleJobDao.selectAll());
return "ok";
}
/**
* 立即执行定时任务
* @return
*/
@GetMapping("/now/{id}")
public String now(@PathVariable("id") Long id) throws Exception {
ScheduleJob scheduleJob = scheduleJobDao.selectOne(ScheduleJob.builder().jobId(id).build());
schudleManager.runNow(scheduleJob);
return "ok";
}
/**
* 添加定时任务
* @return
*/
@GetMapping("/add")
public String add(){
ScheduleJob scheduleJob = new ScheduleJob();
scheduleJob.setBeanName("testTask");// TestTask 类的 BeanName名称
scheduleJob.setJobId(100L);
scheduleJob.setStatus(0);
scheduleJob.setParams("我是测试定时器");
scheduleJob.setCronExpression("*/2 * * * * ?");
scheduleJob.setRemark("测试");
scheduleJob.setCreateTime(new Date());
scheduleJobDao.insertSelective(scheduleJob);// 插入数据库
// 同时添加定时任务
schudleManager.addCronTask(scheduleJob);
return "ok";
}
/**
* 暂停定时任务
* @return
*/
@GetMapping("/suspend/{id}")
public String suspend(@PathVariable("id") Long id) throws Exception {
scheduleJobDao.updateByPrimaryKeySelective(ScheduleJob.builder().jobId(id).status(1).build());
schudleManager.cancel(id);
return "ok";
}
/**
* 更新定时任务
* @return
*/
@GetMapping("/update/{id}")
public String update(@PathVariable("id") Long id) throws Exception {
ScheduleJob scheduleJob = scheduleJobDao.selectOne(ScheduleJob.builder().jobId(id).build());
scheduleJob.setStatus(0);
scheduleJobDao.updateByPrimaryKeySelective(scheduleJob);
schudleManager.updateCronTask(scheduleJob);
return "ok";
}
/**
* 取消定时任务
* @return
*/
@GetMapping("/cancel/{id}")
public String cancel(@PathVariable("id") Long id){
schudleManager.cancel(id);
return "ok";
}
}
提供一下用到的两张表与实体类、R类,dao自己需要创建 ScheduleJobDao
与 ScheduleJobLogDao
,这里我就不提供了
数据库定时任务的表 schedule_job
与 schedule_job_log
DROP TABLE IF EXISTS `schedule_job`;
CREATE TABLE `schedule_job` (
`job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`bean_name` varchar(200) DEFAULT NULL COMMENT 'spring bean名称',
`params` text COMMENT '参数',
`cron_expression` varchar(100) DEFAULT NULL COMMENT 'cron表达式',
`status` tinyint(4) DEFAULT NULL COMMENT '任务状态 0:正常 1:暂停',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`job_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='定时任务';
DROP TABLE IF EXISTS `schedule_job_log`;
CREATE TABLE `schedule_job_log` (
`log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务日志id',
`job_id` bigint(20) NOT NULL COMMENT '任务id',
`bean_name` varchar(200) DEFAULT NULL COMMENT 'spring bean名称',
`params` text COMMENT '参数',
`status` tinyint(4) NOT NULL COMMENT '任务状态 0:成功 1:失败',
`message` text COMMENT '正常信息',
`error` text COMMENT '失败信息',
`times` int(11) NOT NULL COMMENT '耗时(单位:毫秒)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`log_id`),
KEY `job_id` (`job_id`),
KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='定时任务日志';
实体 ScheduleJob
package com.example.demo.pojo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
/** 定时任务
* @author xiaochi
* @date 2022/6/11 9:37
* @desc SchudleTaskPojo
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "schedule_job")
public class ScheduleJob implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long jobId;// 任务id
private String beanName;//spring bean名称
private String params;//参数
private String cronExpression;// cron表达式
private Integer status;// 任务状态
private String remark;// 备注
private Date createTime;// 创建时间
}
定时任务日志实体类 ScheduleJobLog
package com.example.demo.pojo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
/** 定时任务日志
* @author xiaochi
* @date 2022/6/11 11:37
* @desc ScheduleJobLog
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "schedule_job_log")
public class ScheduleJobLog implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long logId;// 日志主建id
private Long jobId;// 任务id
private String beanName;// spring bean名称
private String params;// 参数
private Integer status;// 任务状态0:成功1:失败
private String message;// 正常信息
private String error;// 失败信息
private Integer times;// 耗时(单位:毫秒)
private Date createTime;// 创建时间
}
R类
package com.example.demo.task;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.io.Serializable;
/**
* @author xiaochi
* @date 2022/5/13 14:13
* @desc R
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R implements Serializable {
private static final long serialVersionUID = 7735505903525411467L;
private static final int SUCCESS_CODE = 0;// 成功
private static final int ERROR_CODE = 1;// 失败
private int code;//状态码
private String msg;//消息
private T data;//返回数据
private R(int code){
this.code = code;
}
private R(int code, T data){
this.code = code;
this.data = data;
}
private R(int code, String msg){
this.code = code;
this.msg = msg;
}
private R(int code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
}
public static R ok(){
return new R(SUCCESS_CODE,"success");
}
public static R ok(String msg){
return new R(SUCCESS_CODE,msg);
}
public static R ok(T data){
return new R(SUCCESS_CODE,data);
}
public static R ok(String msg, T data){
return new R(SUCCESS_CODE,msg,data);
}
public static R error(){
return new R(ERROR_CODE,"error");
}
public static R error(String msg){
return new R(ERROR_CODE,msg);
}
public static R error(int code, String msg){
return new R(code,msg);
}
public int getCode(){
return code;
}
public String getMsg(){
return msg;
}
public T getData(){
return data;
}
}