文章目录
前言
本文使用springboot 2.0集成quartz,扩展quartz实现动态定时任务,包含实时创建/修改/删除定时任务,及动态配置定时业务服务,并提供界面(vue+ iview)管理。定时任务开发成本接近"0"。阅读文本你可以了解到:
1. springboot2.0集成quartz配置
2. 不修改代码,普通业务如何快速转换为定时服务,及其实现原理
正文
功能预览:
- 1.动态添加/删除/修改/启停定时任务及规则
- 2.运行时配置任务作业,"0"开发
- 3.定时日志实时显示
1. pom.xml添加quartz的maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RC1</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
springboot2.0 开始支持spring-boot-starter-quartz,很方便集成
2. SchedulerConfig实现加载quartz的相关配置
2.1 SchedulerConfig
@Configuration
public class SchedulerConfig {//1
@Resource(name="dataSource")
private DataSource dataSource;//2
@Bean(name="SchedulerFactory")
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setQuartzProperties(quartzProperties());
factory.setApplicationContextSchedulerContextKey("applicationContextKey");//3
factory.setDataSource(dataSource);
return factory;
}
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
//在quartz.properties中的属性被读取并注入后再初始化对象
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
}
- 配置类很简单,定义SchedulerFactory工厂,加载quartz属性配置。
- 采用集群模式,故设置quartz外部数据源。
- factory.setApplicationContextSchedulerContextKey(“applicationContextKey”);主要用于DynamicQuartzJob获取定时任务的上下文。
2.2 quartz.properties
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# threadCount和threadPriority将以setter的形式注入ThreadPool实例
# 并发个数
org.quartz.threadPool.threadCount = 5
# 优先级
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
# 容许的最大作业延长时间
org.quartz.jobStore.misfireThreshold = 5000
# 默认存储在内存中
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered = true
# 表的前缀
org.quartz.jobStore.tablePrefix = QRTZ_
quartz.properties 主要设置集群模式及jdbc持久化方式
3. QrtzJobDetailsController 实现操作api接口
QrtzJobDetailsController定义操作quartz任务rest接口,本项目为界面提供api服务
@CrossOrigin(origins={"*"})//1
@Controller
@RequestMapping("/qrtzJobDetails")
public class QrtzJobDetailsController {
@Autowired
private QrtzJobDetailsService qrtzJobDetailsService;
/**
* 查询定时任务
*/
@RequestMapping(value = "/listByPage", method=RequestMethod.GET)
@ResponseBody
public Map<String, Object> listByPage(String filter, QrtzJobDetails qrtzJobDetails, Page<Map<String, Object>> page, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
page = this.qrtzJobDetailsService.findMapListByPage(qrtzJobDetails, page);
map.put("data", page);
map.put("total", page.getTotal());
return map;
}
//动态添加定时任务"
@RequestMapping(value = "/add", method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> addQrtzJobDetails(@RequestBody QrtzJobDetails qrtzJobDetails, HttpServletRequest request) throws Exception {
Map<String, Object> map = new HashMap<>();
map = this.qrtzJobDetailsService.createQrtzJobDetails(qrtzJobDetails);
map.put("success", true);
map.put("msg", "定时任务添加成功");
return map;
}
//动态修改定时任务
@RequestMapping(value = "/edit", method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> updateQrtzJobDetails(@RequestBody QrtzJobDetails qrtzJobDetails, HttpServletRequest request) throws Exception {
Map<String, Object> map = new HashMap<>();
map = this.qrtzJobDetailsService.updateQrtzJobDetails(qrtzJobDetails);
return map;
}
//动态删除定时任务,先暂停再删除
@RequestMapping(value = "/delete", method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> deleteQrtzJobDetails(@RequestBody QrtzJobDetails qrtzJobDetails, HttpServletRequest request) throws Exception{
Map<String, Object> map = new HashMap<>();
map = this.qrtzJobDetailsService.deleteQrtzJobDetails(qrtzJobDetails);
return map;
}
//暂停定时任务
@RequestMapping(value = "/pause", method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> pauseJob(@RequestBody QrtzJobDetails qrtzJobDetails, HttpServletRequest request) throws Exception{
Map<String, Object> map = new HashMap<>();
map = this.qrtzJobDetailsService.pauseJob(qrtzJobDetails);
return map;
}
//恢复暂停的定时任务
@RequestMapping(value = "/resume", method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> resumeJob(@RequestBody QrtzJobDetails qrtzJobDetails, HttpServletRequest request) throws Exception{
Map<String, Object> map = new HashMap<>();
map = this.qrtzJobDetailsService.resumeJob(qrtzJobDetails);
return map;
}
}
- 管理页面采用前后端分离,接口设置可跨域访问 @CrossOrigin(origins={"*"})
4. QrtzJobDetailsService 页面管理实现
QrtzJobDetailsService实现管理功能动态job的创建/修改/删除/暂停等任务功能
@Service("qrtzJobDetailsService")
@Transactional(rollbackFor=Exception.class)
public class QrtzJobDetailsServiceImpl implements QrtzJobDetailsService {
private static final Logger LOGGER=LoggerFactory.getLogger(QrtzJobDetailsServiceImpl.class);
/** triggerName 前缀*/
private static final String TRIGGER_NAME_PREFIX = "triggerName.";
/** jobName/triggerName 默认组 */
private static final String GROUP_DEFAULT = "DEFAULT";
@Autowired
private QrtzJobDetailsDao qrtzJobDetailsDao;//1
@Autowired
private Scheduler scheduler;
@Override
public Map<String, Object> createQrtzJobDetails(QrtzJobDetails qrtzJobDetails) throws Exception{
Map<String, Object> resultMap = new HashMap<>();
// 非空校验
if (qrtzJobDetails == null) {
throw new Exception("qrtzJobDetails 为空");
}
if (StringUtils.isEmpty(qrtzJobDetails.getJobName())) {
throw new Exception("qrtzJobDetails serviceInfo 为空");
}
// 定时服务有效性校验 (校验是否存在对应的servcie.method )
this.checkServiceAndMethod(qrtzJobDetails.getJobName());//2
// 唯一性校验
String jobName = qrtzJobDetails.getJobName();
String triggerName = TRIGGER_NAME_PREFIX + qrtzJobDetails.getJobName();
String jobGroup = StringUtils.isEmpty(qrtzJobDetails.getJobGroup())? GROUP_DEFAULT : qrtzJobDetails.getJobGroup();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
if (scheduler.checkExists(jobKey)) {
throw new DynamicQuartzException(qrtzJobDetails.getJobName() + "服务方法对应定时任务已经存在!");
}
// 构建job信息
JobDetail job = JobBuilder.newJob(DynamicQuartzJob.class)//3
.withIdentity(jobKey)
.withDescription(qrtzJobDetails.getDescription())
.build();
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, jobGroup);
// 构建job的触发规则 cronExpression
Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(qrtzJobDetails.getCronExpression())).build();
// 注册job和trigger信息
scheduler.scheduleJob(job, trigger);
resultMap.put("success", true);
resultMap.put("msg", "创建QrtzJobDetails 成功!");
return resultMap;
}
@Override
public Map<String, Object> updateQrtzJobDetails(QrtzJobDetails qrtzJobDetails) throws Exception {
Map<String, Object> resultMap = new HashMap<>();
JobKey jobKey = JobKey.jobKey(qrtzJobDetails.getJobName(), qrtzJobDetails.getJobGroup());
TriggerKey triggerKey = null;
List<? extends Trigger> list = scheduler.getTriggersOfJob(jobKey);
if (list == null || list.size() != 1) {
return resultMap;
}
for (Trigger trigger : list) {
//暂停触发器
scheduler.pauseTrigger(trigger.getKey());
triggerKey = trigger.getKey();
}
Trigger newTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(qrtzJobDetails.getCronExpression())).build();
scheduler.rescheduleJob(newTrigger.getKey(), newTrigger);
LOGGER.info("update job name:{} success", qrtzJobDetails.getJobName());
resultMap.put("success", true);
resultMap.put("msg", "update job success");
return resultMap;
}
@Override
public Map<String, Object> deleteQrtzJobDetails(QrtzJobDetails qrtzJobDetails) throws Exception {
Map<String, Object> resultMap = new HashMap<>();
JobKey jobKey = JobKey.jobKey(qrtzJobDetails.getJobName(), qrtzJobDetails.getJobGroup());
QuartzUtil.deleteJob(scheduler, jobKey);
LOGGER.info("delete job name:{} success", qrtzJobDetails.getJobName());
return resultMap;
}
@Override
public QrtzJobDetails findQrtzJobDetailsByPrimaryKey(String id) {
return this.qrtzJobDetailsDao.selectByPrimaryKey(id);
}
@Override
public Page<Map<String, Object>> findMapListByPage(QrtzJobDetails qrtzJobDetails, Page<Map<String, Object>> page) {
page = PageHelper.startPage(page.getPageNum(), page.getPageSize());
this.qrtzJobDetailsDao.selectMapList(qrtzJobDetails);
return page;
}
@Override
public Page<QrtzJobDetails> findListByPage(QrtzJobDetails qrtzJobDetails, Page<QrtzJobDetails> page) {
page = PageHelper.startPage(page.getPageNum(), page.getPageSize());
this.qrtzJobDetailsDao.selectList(qrtzJobDetails);
return page;
}
@Override
public List<Map<String, Object>> findMapList(QrtzJobDetails qrtzJobDetails) {
return this.qrtzJobDetailsDao.selectMapList(qrtzJobDetails);
}
@Override
public List<QrtzJobDetails> findList(QrtzJobDetails qrtzJobDetails){
return this.qrtzJobDetailsDao.selectList(qrtzJobDetails);
}
@Override
public Map<String, Object> pauseJob(QrtzJobDetails qrtzJobDetails)
throws Exception {
scheduler.pauseJob(JobKey.jobKey(qrtzJobDetails.getJobName(), qrtzJobDetails.getJobGroup()));
LOGGER.info("pause job name:{} success", qrtzJobDetails.getJobName());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("msg", "pause job success!");
return result;
}
@Override
public Map<String, Object> resumeJob(QrtzJobDetails qrtzJobDetails)
throws Exception {
scheduler.resumeJob(JobKey.jobKey(qrtzJobDetails.getJobName(), qrtzJobDetails.getJobGroup()));
LOGGER.info("resume job name:{} success", qrtzJobDetails.getJobName());
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("msg", "resume job success!");
return result;
}
/**
* <li>校验服务和方法是否存在</li>
* @param jobName
* @throws DynamicQuartzException
*/
private void checkServiceAndMethod(String jobName) throws DynamicQuartzException {
String[] serviceInfo = jobName.split("\\.");
String beanName = serviceInfo[0];
String methodName = serviceInfo[1];
if (! SpringContextHolder.existBean(beanName)) {
throw new DynamicQuartzException("找不到对应服务");
}
if (! SpringContextHolder.existBeanAndMethod(beanName, methodName, null)) {
throw new DynamicQuartzException("服务方法不存在");
}
}
}
- qrtzJobDetailsDao连接quartz集群的数据库,仅用于界面查询。所有任务操作通过scheduler接口操作
- jobName格式:spring的beanName +"."+method方法名称。checkServiceAndMethod(String jobName)用于创建时候保证对应服务存在且可用。值得注意,jobName命名是实现动态定时任务的关键, 因为它不仅标识不同定时任务身份,还用于动态调用实现。详见DynamicQuartzJob
- 创建的定时任务都是动态任务类型JobBuilder.newJob(DynamicQuartzJob.class)类型。
5. DynamicQuartzJob 任务动态调用
DynamicQuartzJob 是实现"动态"的核心
@PersistJobDataAfterExecution
@DisallowConcurrentExecution// 不允许并发执行
public class DynamicQuartzJob extends QuartzJobBean { //1
private static final Logger logger = LoggerFactory.getLogger(DynamicQuartzJob.class);
@Override
protected void executeInternal(JobExecutionContext jobexecutioncontext) throws JobExecutionException {
// use JobDetailImpl replace JobDetail for get jobName
JobDetailImpl jobDetail = (JobDetailImpl) jobexecutioncontext.getJobDetail();
String name = jobDetail.getName();
if (StringUtils.isEmpty(name)) {
throw new JobExecutionException("can not find service info, because desription is empty");
}
String[] serviceInfo = name.split("\\.");
String beanName = serviceInfo[0];
String methodName = serviceInfo[1];
Object serviceImpl = getApplicationContext(jobexecutioncontext).getBean(beanName);
Method method;
try {
Class<?>[] parameterTypes = null;
Object[] arguments = null;
method = serviceImpl.getClass().getMethod(methodName,parameterTypes);
logger.info("dynamic invoke {}.{}()", serviceImpl.getClass().getName(), methodName);
method.invoke(serviceImpl, arguments);
} catch (NoSuchMethodException | SecurityException
| IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
logger.error("reflect invoke service method error", e);
}
}
private ApplicationContext getApplicationContext(final JobExecutionContext jobexecutioncontext) {
try {
//applicationContextKey 在SchedulerFactoryBean中配置
return (ApplicationContext) jobexecutioncontext.getScheduler().getContext().get("applicationContextKey");
} catch (SchedulerException e) {
logger.error("jobexecutioncontext.getScheduler().getContext() error!", e);
throw new RuntimeException(e);
}
}
}
- DynamicQuartzJob和普通定时任务job一样,也要继承QuartzJobBean并实现executeInternal(JobExecutionContext jobexecutioncontext)。但是和普通定时job直接写业务实现不同,
- 我们通过jobexecutioncontext.getJobDetail()获取当前执行定时任务的详细信息。JobDetailImpl中我们可以获取任务名称jobName,如helloService.sayHello。有这个我们就可以利用找到beanName找到
对应的业务service,并通过反射调用指定方法。这样job里面就不用写任何业务逻辑,而是实现对应业务的"寻址"及调用。因为具体的执行业务取决于运行时DynamicQuartzJob的JobDetail"实例",所以称之为"动态"
附录
- 项目地址: https://github.com/MusicXi/demo-quartz-dynamic
- 使用技术及版本
- Spring Boot : 2.0.0.RC1
- Mybatis
- Pagehelper
- iview
- h2
演示说明
开发模式(使用h2数据库)
1. application.properties配置模式spring.profiles.active=dev
2. 运行:com.myron.quartz.Application
生产模式(使用mysql数据库)
1. mysql数据执行classpath:tables_mysql.sql 创建quartz相关表
2. application-pro.properties 配置mysql数据链接
3. application.properties 配置激活spring.profiles.active=pro
4. 运行:com.myron.quartz.Application
界面访问
1. 方式一: 内嵌页面: http://localhost:7070/
2. 方式二: 前后分离: front/quartz.html 右键浏览器打开
默认任务
配置项目启动默认定时任务
@Component//被spring容器管理
@Order(1)//如果多个自定义ApplicationRunner,用来标明执行顺序
public class MyApplicationRunner implements ApplicationRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(MyApplicationRunner.class);
@Autowired
private QrtzJobDetailsService qrtzJobDetailsService;
@Override
public void run(ApplicationArguments applicationArguments) throws Exception {
QrtzJobDetails qrtzJobDetails = new QrtzJobDetails();
qrtzJobDetails.setJobName("helloService.sayHello");
qrtzJobDetails.setCronExpression("*/5 * * * * ?");
qrtzJobDetails.setDescription("测试任务");
QrtzJobDetails qrtzJobDetails1 = new QrtzJobDetails();
qrtzJobDetails1.setJobName("helloService.sayBye");
qrtzJobDetails1.setCronExpression("*/15 * * * * ?");
qrtzJobDetails1.setDescription("测试任务111111");
LOGGER.info("add default time job:{}", JSON.toJSONString(qrtzJobDetails, SerializerFeature.PrettyFormat));
LOGGER.info("add default time job:{}", JSON.toJSONString(qrtzJobDetails1, SerializerFeature.PrettyFormat));
qrtzJobDetailsService.createQrtzJobDetails(qrtzJobDetails);
qrtzJobDetailsService.createQrtzJobDetails(qrtzJobDetails1);
}
}
参考文章
- SpringBoot WebSocket 最简单的使用方法(实现网页动态打印后台消息) https://blog.csdn.net/shida_csdn/article/details/80528112
- spring4 使用websocket https://www.cnblogs.com/nevermorewang/p/7274217.html