在spring boot项目中定时任务的开发方式:
一、可通过@EnableScheduling注解和@Scheduled注解实现
二、可通过SchedulingConfigurer接口来实现
三、集成Quartz框架实现
注意:第一和第二方式不能动态添加、删除、启动、停止任务。
在满足项目需求的情况下,尽量少的依赖其它框架,避免项目过于臃肿和复杂是最基本的开发原则。
查看 spring-context 这个 jar 包中 org.springframework.scheduling.ScheduledTaskRegistrar 这个类的源代码,发现可以通过改造这个类(主要是基于TaskScheduler和CronTask两个类来实现)就能实现动态增删启停定时任务功能。
项目依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>spring-boot-project</artifactId> <groupId>com.example</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>scheduledTaskRegistrar-demo</artifactId>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency>
<dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>RELEASE</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> </dependencies> </project>
|
配置文件
server: port: 8080 spring: datasource: url: 数据库连接地址 username: 数据库名 password: 数据库密码 driver-class-name: com.mysql.jdbc.Driver
mybatis: mapper-locations: classpath:mapper/*Mapper.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
|
启动引导类
package com.scheduledtask;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication @MapperScan("com.scheduledTask.mapper") public class ScheduledTaskApplication { public static void main(String[] args) { SpringApplication.run(ScheduledTaskApplication.class, args); } }
|
@MapperScan:指定扫描的Mapper类的包的路径,简化直接在每个Mapper类上添加注解@Mapper
配置类
配置线程池
package com.scheduledtask.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration public class SchedulingConfig {
@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(4); taskScheduler.setRemoveOnCancelPolicy(true); taskScheduler.setThreadNamePrefix("ThreadPool-"); return taskScheduler; } }
|
执行类
同步处理定时任务类
package com.scheduledtask.task;
import java.util.concurrent.ScheduledFuture;
public class ScheduledTask { public volatile ScheduledFuture future;
public void cancel() { ScheduledFuture future = this.future; if (future != null) { future.cancel(true); } } }
|
定时任务注册类
package com.scheduledtask.task;
import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.config.CronTask; import org.springframework.stereotype.Component;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
@Component public class CronTaskRegistrar implements DisposableBean {
private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);
@Autowired private TaskScheduler taskScheduler;
public void addCronTask(Runnable task, String cronExpression) { addCronTask(new CronTask(task, cronExpression)); }
public void addCronTask(CronTask cronTask) { if (cronTask != null) { Runnable task = cronTask.getRunnable(); if (scheduledTasks.containsKey(task)) { removeCronTask(task); } scheduledTasks.put(task, scheduleCronTask(cronTask)); } }
public void removeCronTask(Runnable task) { ScheduledTask scheduledTask = scheduledTasks.remove(task); if (scheduledTask != null) { scheduledTask.cancel(); } }
public ScheduledTask scheduleCronTask(CronTask cronTask) { ScheduledTask scheduledTask = new ScheduledTask(); scheduledTask.future = taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger()); return scheduledTask; }
@Override public void destroy() throws Exception { for (ScheduledTask task : scheduledTasks.values()) { task.cancel(); } scheduledTasks.clear(); } }
|
此处用Map来模拟缓存,当然可以换教专业的缓存组件,如redis等等
初始化定时任务类
package com.scheduledtask.task;
import com.scheduledtask.pojo.Task; import com.scheduledtask.service.TaskService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils;
import java.util.List;
@Slf4j @Component
public class TaskRunner implements CommandLineRunner {
@Autowired private TaskService taskService;
@Autowired private CronTaskRegistrar cronTaskRegistrar;
@Override public void run(String... args) { new Thread() { public void run() { List<Task> taskList = taskService.getTaskListByStatus(1);
if (!CollectionUtils.isEmpty(taskList)) { for (Task task : taskList) { SchedulingRunnable runnable = new SchedulingRunnable(task.getBeanName(), task.getMethodName(), task.getMethodParams()); cronTaskRegistrar.addCronTask(runnable, task.getCronExpression()); } log.info("定时任务加载完毕......"); } } }.start(); } }
|
注意:Spring Boot中提供了CommandLineRunner(实现启动初始化功能)和ApplicationRunner(引导类)两个接口来实现容器启动
CommandLineRunner的执行是整个应用启动的一部分,避免CommandLineRunner启动中抛出异常(java.lang.IllegalStateException: Failed to execute CommandLineRunner),直接影响主程序的启动,从而此处重新开启一个线程,让CommandLineRunner和主线程相互独立
,此时抛出异常并不会影响到主线程,防止踩坑
定时任务执行类
package com.scheduledtask.task;
import com.scheduledtask.util.SpringContextUtils; import lombok.*; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils;
import java.lang.reflect.Method; import java.util.Objects;
@Slf4j @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) @Data public class SchedulingRunnable implements Runnable { private String beanName; private String methodName; private String params;
@Override public void run() { log.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params); long startTime = System.currentTimeMillis();
try { Object target = SpringContextUtils.getBean(beanName);
Method method = StringUtils.isEmpty(params) ? target.getClass().getDeclaredMethod(methodName) : target.getClass().getDeclaredMethod(methodName, String.class);
ReflectionUtils.makeAccessible(method);
if (StringUtils.isEmpty(params)) { method.invoke(target); } else { method.invoke(target, params); } } catch (Exception e) { e.printStackTrace(); log.error("定时任务异常 - bean:{},方法:{},参数:{}", beanName, methodName, params); } long time = System.currentTimeMillis() - startTime; log.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{}", beanName, methodName, params, time); }
@Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SchedulingRunnable that = (SchedulingRunnable) o; if (params == null) { return beanName.equals(that.beanName) && methodName.equals(that.methodName) && that.params == null; } return beanName.equals(that.beanName) && methodName.equals(that.methodName) && params.equals(that.params); }
@Override public int hashCode() { if (params == null) { return Objects.hash(beanName, methodName); } return Objects.hash(beanName, methodName, params); } }
|
工具类
获取实例对象
package com.scheduledtask.util;
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component;
@Component public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtils.applicationContext = applicationContext; }
public static Object getBean(String name) { return applicationContext.getBean(name); } }
|
就是获取定时任务业务逻辑类注解@Component上配置的实例名(对应数据库中的beanName)
实体对象与sql脚本
任务实体类
package com.scheduledtask.pojo;
import com.fasterxml.jackson.annotation.JsonFormat; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
import javax.persistence.Column; import javax.persistence.Id; import javax.persistence.Table; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import java.util.Date;
@Data @AllArgsConstructor @NoArgsConstructor @Table(name = "task") public class Task { @Id private Integer id;
@Column(name = "beanName") @NotBlank(message = "对象名不能为空!") private String beanName;
@Column(name = "methodName") @NotBlank(message = "方法名不能为空!") private String methodName;
@Column(name = "methodParams") private String methodParams;
@Column(name = "cronExpression") @NotBlank(message = "cron表达式不能为空!") @Pattern(message = "cron表达式错误!", regexp = "^\\s*($|#|\\w+\\s*=|(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?(?:,(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?)*)\\s+(\\?|\\*|(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?(?:,(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?)*)\\s+(\\?|\\*|(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?(?:,(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?)*|\\?|\\*|(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*)\\s+(\\?|\\*|(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?(?:,(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?)*|\\?|\\*|(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?(?:,(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?)*)(|\\s)+(\\?|\\*|(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?(?:,(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?)*))$") private String cronExpression;
@Column(name = "jobStatus") private Integer jobStatus;
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Column(name = "createTime") private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Column(name = "updateTime") private Date updateTime;
}
|
创表语句
CREATE TABLE `task` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `beanName` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '任务名称', `methodName` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '方法名称', `methodParams` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '方法参数', `cronExpression` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'cron表达式', `jobStatus` char(1) COLLATE utf8_unicode_ci DEFAULT '0' COMMENT '任务状态 0暂停 1正常', `remark` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '备注', `createTime` date DEFAULT NULL COMMENT '创建时间', `updateTime` date DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `beanName` (`beanName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='定时任务表';
|
数据处理
package com.scheduledtask.service;
import com.scheduledtask.mapper.TaskMapper; import com.scheduledtask.pojo.Task; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service @Transactional(rollbackFor = Exception.class) public class TaskService {
@Autowired private TaskMapper taskMapper;
public int insertTask(Task task) { return taskMapper.insertSelective(task); }
public List<Task> getTaskListByStatus(Integer jobStatus) { Example example = new Example(Task.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("jobStatus", jobStatus); return taskMapper.selectByExample(example); }
public Task findTaskByJobId(Integer id) { return taskMapper.selectByPrimaryKey(id); }
public boolean deleteTaskByJobId(Integer id) { return taskMapper.deleteByPrimaryKey(id) > 0 ? true : false; }
public boolean updateTask(Task task) { return taskMapper.updateByPrimaryKeySelective(task) > 0 ? true : false; }
}
|
数据连接
数据层类
package com.scheduledtask.mapper;
import com.scheduledtask.pojo.Task; import org.springframework.stereotype.Repository; import tk.mybatis.mapper.common.Mapper;
@Repository public interface TaskMapper extends Mapper<Task> {
}
|
注意:因为继承Mapper类,使用通用mapper插件做数据层处理,基本的CRUD单表操作方法都已有
映射文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.scheduledtask.mapper.TaskMapper">
</mapper>
|
枚举
package com.scheduledtask.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor public enum TaskStatus { SUSPEND("暂停", 0), NORMAL("正常", 1);
private String desc; private int value;
public String desc() { return this.desc; }
public int value() { return this.value; } }
|
定时任务业务逻辑类
package com.scheduledtask.component;
import org.springframework.stereotype.Component;
@Component("TaskOne") public class TaskOne { public void taskWithParams(String params) {
}
public void taskNoParams() {
} }
|
控制层类
package com.scheduledtask.controller;
import com.scheduledtask.enums.TaskStatus; import com.scheduledtask.pojo.Task; import com.scheduledtask.service.TaskService; import com.scheduledtask.task.CronTaskRegistrar; import com.scheduledtask.task.SchedulingRunnable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController @Validated public class TaskController {
@Autowired private TaskService taskService;
@Autowired private CronTaskRegistrar cronTaskRegistrar;
@PostMapping("/addTask") public String addTask(@Valid Task task) { if (taskService.insertTask(task) <= 0) { return "新增失败!"; } else { if (task.getJobStatus().equals(TaskStatus.NORMAL.value())) { SchedulingRunnable runnable = new SchedulingRunnable(task.getBeanName(), task.getMethodName(), task.getMethodParams()); cronTaskRegistrar.addCronTask(runnable, task.getCronExpression()); } } return "新增成功!"; }
@DeleteMapping("/deleteTask/{id}") public String deleteTask(@PathVariable Integer id) { Task task = taskService.findTaskByJobId(id); if (!taskService.deleteTaskByJobId(id)) { return "删除失败!"; } else { if (task.getJobStatus().equals(TaskStatus.NORMAL.value())) { SchedulingRunnable runnable = new SchedulingRunnable(task.getBeanName(), task.getMethodName(), task.getMethodParams()); cronTaskRegistrar.removeCronTask(runnable); } } return "删除成功!"; }
@PostMapping("/updateTask") public String updateTask(Task taskNew) { Task taskOld = taskService.findTaskByJobId(taskNew.getId()); if (taskService.updateTask(taskNew)) { if (taskOld.getJobStatus().equals(TaskStatus.NORMAL.value())) { SchedulingRunnable runnable = new SchedulingRunnable() .setBeanName(taskOld.getBeanName()) .setMethodName(taskOld.getMethodName()) .setParams(taskOld.getMethodParams()); cronTaskRegistrar.removeCronTask(runnable); } if (taskNew.getJobStatus().equals(TaskStatus.NORMAL.value())) { SchedulingRunnable runnable = new SchedulingRunnable(taskNew.getBeanName(), taskNew.getMethodName(), taskNew.getMethodParams()); cronTaskRegistrar.addCronTask(runnable, taskNew.getCronExpression()); } } else { return "更新失败!"; } return "更新成功!"; }
@GetMapping("/updateTaskStatus/{id}") public String updateTaskStatus(@PathVariable Integer id) { Task task = taskService.findTaskByJobId(id); if (task.getJobStatus().equals(TaskStatus.NORMAL.value())) { SchedulingRunnable runnable = new SchedulingRunnable(task.getBeanName(), task.getMethodName(), task.getMethodParams()); cronTaskRegistrar.removeCronTask(runnable); task.setJobStatus(0); return taskService.updateTask(task) ? "修改成功!" : "修改失败!"; } else { SchedulingRunnable runnable = new SchedulingRunnable(task.getBeanName(), task.getMethodName(), task.getMethodParams()); cronTaskRegistrar.addCronTask(runnable, task.getCronExpression()); task.setJobStatus(1); return taskService.updateTask(task) ? "修改成功!" : "修改失败!"; } } }
|
注意:此处用到的校验注解是在org.springframework.validation包下的,如只在方法参数上加@Validated校验注解无效的话,则在类上加@Validated注解,并在方法参数上加@Valid注解
测试
启动加载
测试类
package com.scheduledtask;
import com.scheduledtask.controller.TaskController; import com.scheduledtask.service.TaskService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class) @SpringBootTest(classes = ScheduledTaskApplication.class) public class ScheduledTaskTest {
@Autowired private TaskController taskController;
@Autowired private TaskService taskService;
@Test public void testTaskController(){
String msg = taskController.updateTaskStatus(50); System.out.println(msg); } }
|
源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/scheduledTaskRegistrar-demo