本章介绍Spring Boot中定时器的使用方法,总结个人对一些定时器用法的了解,如有错误欢迎指正。
定时器是什么?
定时器就是让程序定时执行某些任务,不需要人工手动执行。
为什么要使用定时器?
使用定时器,有很多好处。举个例子:在平台api对接中,请求通常需要携带token信息,而token有可能有固定时效。在token失效后,需要重新获取token信息。此时可以使用定时任务重新获取token。
怎么用定时器?
在Java中,有很多中方式处理定时任务。Timer,SpringBoot的@Scheduled注解,SpringBoot整合Quartz等方式。
整合Quartz
Spring Boot中使用Quartz是通过继承QuartzJobBean的方式,创建JobDetail和Trigger以达到定时效果。
1、创建SimpleScheduleExtendsQuartzJobTask任务类
package com.study.controller.schedule;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.Date;
public class SimpleScheduleExtendsQuartzJobTask extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("do schedule extends quartz job task " + new Date());
}
}
2、创建JobDetail和Trigger。创建JobDetail时需要使用JobBuilder.newJob()方法添加任务类,withIdentity()方法指定key,build()方法进行构建。TriggerBuilder.forJob()方法添加JobDetail,withSchedule()方法添加schedule。schedule可以通过SimpleScheduleBuilder进行构建,withIntervalInSeconds()指明每次执行定时任务的间隔时间。
package com.study.config;
import com.study.controller.schedule.SimpleScheduleExtendsQuartzJobTask;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SimpleScheduleConfig {
@Bean
public JobDetail simpleJobBeanDetails() {
return JobBuilder.newJob(SimpleScheduleExtendsQuartzJobTask.class).withIdentity("simpleJobBean").storeDurably().build();
}
@Bean
public Trigger simpleTrigger() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(15).repeatForever();
return TriggerBuilder.newTrigger().withIdentity("simpleTrigger").forJob(simpleJobBeanDetails()).withSchedule(scheduleBuilder).build();
}
}
上述方法已经可以到达效果,但仍然有需要改进的地方。我们来思考几个问题:
1、有大量的定时任务时如何解决?
2、当某个任务的执行时间需要修改时如何解决?
3、如何控制任务的启动和停止?
我们是否可以将所有的任务的类名,执行方法, 参数,以及执行时间保存在数据库中,通过对数据库的修改和访问,实现任务调度。
我们约定所有的定时脚本,都通过http://localhost:8080/authenticate/runScript?type=DoSimpleJobTask&args=aa方式进行访问,其中type指定类名,设置统一的访问方法为runScript()方法,通过反射的方式处理不同的任务。创建AuthenticateController
package com.study.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@RestController
public class AuthenticateController {
@RequestMapping("authenticate/runScript")
public String runScript(HttpServletRequest request, HttpServletResponse response) {
String type = request.getParameter("type");
if (null == type) {
System.out.println("type is null");
return "ok";
}
String argList = request.getParameter("args");
type = type.replace("/", ".");
String className = "script." + type;
String methodName = "runScript";
try {
Class<?> customClass = Class.forName(className);
Method method = customClass.getMethod(methodName, String.class);
Object result = method.invoke(customClass.newInstance(), argList);
if (null != result) {
if ((Boolean) result) {
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "ok";
}
}
因为配置信息保存在数据库中,所以添加maven依赖访问数据库
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
application.properties中设置mysql的连接配置。此处做下说明,设置时区serverTimezone=UTC,useSSL=false,因为mysql驱动为6.0版本,driver也改成了com.mysql.cj.jdbc.Driver
#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
任务配置类
package com.study.orm;
public class ScheduleTaskConfig {
private Integer id;
private String name;
private String task;
private String cron;
public ScheduleTaskConfig() {
}
public ScheduleTaskConfig(Integer id, String name, String task, String cron) {
this.id = id;
this.name = name;
this.task = task;
this.cron = cron;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
public String getCron() {
return cron;
}
public void setCron(String cron) {
this.cron = cron;
}
}
创建Mapper
package com.study.mapper;
import com.study.orm.ScheduleTaskConfig;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ScheduleTaskConfigMapper {
@Select("select * from tb_task")
List<ScheduleTaskConfig> fetchList();
}
service接口及实现
package com.study.service;
import com.study.orm.ScheduleTaskConfig;
import java.util.List;
public interface ScheduleTaskConfigService {
List<ScheduleTaskConfig> fetchList();
}
package com.study.service.impl;
import com.study.mapper.ScheduleTaskConfigMapper;
import com.study.orm.ScheduleTaskConfig;
import com.study.service.ScheduleTaskConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ScheduleTaskConfigServiceImpl implements ScheduleTaskConfigService {
@Autowired
private ScheduleTaskConfigMapper scheduleTaskConfigMapper;
@Override
public List<ScheduleTaskConfig> fetchList() {
return scheduleTaskConfigMapper.fetchList();
}
}
扫描mapper
package com;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@MapperScan("com.study.mapper")
@SpringBootApplication
@EnableScheduling
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
考虑到需要控制任务的启动和停止,采用ThreadPoolTaskScheduler进行任务调度。ThreadPoolTaskScheduler的schedule方法可以创建ScheduledFuture,ScheduledFuture中可以通过cancel(true),取消任务。
package com.study.controller.schedule;
import com.mchange.v1.util.MapUtils;
import com.study.orm.ScheduleTaskConfig;
import com.study.service.ScheduleTaskConfigService;
import com.study.utils.ServiceResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
@RequestMapping("schedule")
@RestController
public class ScheduleTaskManageController {
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@Bean
public ThreadPoolTaskScheduler getThreadPoolTaskScheduler() {
return new ThreadPoolTaskScheduler();
}
private Map<Integer, ScheduledFuture> scheduledFutureMap = new HashMap<>();
@Autowired
private ScheduleTaskConfigService scheduleTaskConfigService;
public void _init(){
System.out.println("++++++++++++init++++++++++++++++");
List<ScheduleTaskConfig> scheduleTaskConfigs = scheduleTaskConfigService.fetchList();
if (!CollectionUtils.isEmpty(scheduleTaskConfigs)) {
for (ScheduleTaskConfig scheduleTaskConfig : scheduleTaskConfigs) {
ScheduledFuture future = threadPoolTaskScheduler.schedule(new Runnable() {
@Override
public void run() {
try {
//task的内容设置为 curl "http://localhost:8080/authenticate/runScript?type=DoSimpleJobTask&args=aa"
//请求通过authenticate/runScript映射,通过反射调用script中的任务
System.out.println(scheduleTaskConfig.getTask());
Process process = Runtime.getRuntime().exec(scheduleTaskConfig.getTask());
} catch (IOException e) {
e.printStackTrace();
}
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
return new CronTrigger(scheduleTaskConfig.getCron()).nextExecutionTime(triggerContext);
}
});
scheduledFutureMap.put(scheduleTaskConfig.getId(), future);
}
}
}
public void _destory(){
System.out.println("++++++++++++++++destory++++++++++++");
if (scheduledFutureMap != null && !scheduledFutureMap.isEmpty()) {
for (Integer key : scheduledFutureMap.keySet()) {
System.out.println("scheduledFutureMap :" + key);
scheduledFutureMap.get(key).cancel(true);
}
}
scheduledFutureMap.clear();
}
@RequestMapping("start")
public String startScheduleTask() {
_init();
return "success";
}
@RequestMapping("stop")
public String stopScheduleTask() {
_destory();
return "success";
}
}
创建tb_task存放需要执行的任务
CREATE TABLE `tb_task` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(1000) DEFAULT NULL,
`task` varchar(1000) DEFAULT NULL,
`cron` varchar(1000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
select * from tb_task
1 测试 curl "http://localhost:8080/authenticate/runScript?type=DoSimpleJobTask&args=aa" 0/35 * * * * ?
2 测试访问百度 curl "www.baidu.com" 0/35 * * * * ?
最后再script下添加不同的任务类
package script;
public class DoSimpleJobTask {
public Boolean runScript(String args) {
System.out.println("this is test schedule task : " + args);
return false;
}
}
附cron表达式说明:
0/35 * * * * ?
秒 分 时 天 年 ?
大概的思路就是这样,可以根据自己需要添加其它。如
- 添加event,在项目启动之后,自动执行_init方法进行初始化。
- 当重新添加或修改了task表中的数据时,可以先调用stop方法,取消所有的任务,然后再调用start方法添加。
- 或者在页面编辑数据表中数据时,对应新增的方法,可以添加到schedule中,对于修改的方法可以根据id值获取到scheduledFutureMap的值,先取消,然后重新添加一个等等。
package com.study.event;
import com.study.controller.schedule.ScheduleTaskManageController;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class ApplicationStartup implements ApplicationListener<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
System.out.println("++++++++++++++application started event+++++++++++++++");
ScheduleTaskManageController scheduleTaskManageController = applicationStartedEvent.getApplicationContext().getBean(ScheduleTaskManageController.class);
scheduleTaskManageController._destory();
scheduleTaskManageController._init();
}
}
如果有什么错误,或者有更好的处理方式,也可以留言告诉我