目录
3.1 任务Job、JobDetail、JobBuilder
3.2 触发器 Trigger、TriggerBuilder
3.3 调度器Scheduler、SchedulerFactory、SchedulerFactoryBean
公司旧系统需要增加定时任务功能,甲方希望功能简洁且不增加运维人员工作量,因此参考网上案例基于Spring矿机用Quartz写了简单的定时任务模块。
一 项目参考
Spring 集成quartz框架的两种方式_皮卡车厘子的博客-CSDN博客_spring集成quartz
Quartz- Quartz API以及Jobs 和Triggers介绍_小小工匠的博客-CSDN博客_quartz-jobs
定时任务框架Quartz-(一)Quartz入门与Demo搭建_是Guava不是瓜娃的博客-CSDN博客_quartz
Quartz-scheduler 定时器概述、核心 API 与 快速入门_蚩尤后裔的博客-CSDN博客_org.quartz-scheduler
项目使用的是quartz版本是2.2.1,需要提前引入到pom文件中
<!--核心包-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
避免和谐,这里复制一份留着自己看:下面内容基本都是贴的链接里的,因为这部分公司项目代码也是借鉴的模板,所以贴了部分不涉密的模板代码块。
Java项目中常使用的定时器有JDK Timer、Quartz、Spring Task等三种。Quartz的功能强大,配置也比较复杂,适合大型、多定时任务的项目使用。Spring Task配置较为简单轻量,需要Spring框架支持。JDK自带的定时器Timer使用灵活,配置简单,适合中小型项目。这里记录下quartz方式
一、Quartz作业类的继承方式来讲,可以分为两类:
1.作业类需要继承自特定的作业类基类,如Quartz中需要继承自org.springframework.scheduling.quartz.QuartzJobBean;
2.作业类即普通的java类,不需要继承自任何基类。
使用QuartzJobBean,需要继承,而且在注入Spring容器中其他service时候需要在schedulerContextAsMap中注入,比较麻烦,否则不能成功(具体操作参考:https://blog.csdn.net/whaosy/article/details/6298686)。
使用MethodInvokeJobDetailFactoryBean则需要指定targetObject(任务实例)和targetMethod(实例中要执行的方法),可以方便注入Spring容器中其他的service。后者优点是无侵入,业务逻辑简单。所以我更推荐的第二种!
二、从任务调度的触发时机来分,这里主要是针对作业使用的触发器,主要有以下两种:
每隔指定时间则触发一次,在Quartz中对应的触发器为:org.springframework.scheduling.quartz.SimpleTriggerBean
每到指定时间则触发一次,在Quartz中对应的调度器为:org.springframework.scheduling.quartz.CronTriggerBean
三、下面只保留了原作者写的第二种——作业类不继承特定基类
package com.summer.job;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.summer.opendoor.service.OpenDoorService;
@Component
public class JobMethodInvoke{
public JobMethodInvoke() {
super();
System.out.println("JobMethodInvoke初始化");
}
@Autowired
private OpenDoorService openDoorService;
protected void doSomething() {
System.out.println("JOB任务执行了");
openDoorService.open();
}
}
注意中间的引用关系,如下图
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!--第一步 要执行任务的作业类。 -->
<bean id="testQuartz" class="com.summer.job.JobMethodInvoke" />
<!-- 第二步 将需要执行的定时任务注入JOB中。 -->
<bean id="jobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="testQuartz"></property>
<!-- 任务类中需要执行的方法 -->
<property name="targetMethod" value="doSomething"></property>
<!-- 上一次未执行完成的,要等待有再执行。 -->
<property name="concurrent" value="false"></property>
</bean>
<!--第三步 基本的定时器,会绑定具体的任务。 -->
<!-- 第一种 SimpleTriggerBean,只支持按照一定频度调用任务,如每隔30分钟运行一次。配置方式如下: -->
<!-- <bean id="testTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail"></property>
<property name="startDelay" value="3000"></property>
<property name="repeatInterval" value="2000"></property>
</bean> -->
<!-- 第二种 CronTriggerBean,支持到指定时间运行一次,如每天12:00运行一次等。配置方式如下: -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="jobDetail" />
<!-- <!—每天12:00运行一次 —> -->
<property name="cronExpression" value="0 0 12 * * ?" />
</bean>
<!-- 第四步 配置调度工厂 -->
<bean id="schedulerFactoryBean" lazy-init="true"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<!-- <ref bean="testTrigger"></ref> -->
<ref bean="cronTrigger" />
</list>
</property>
</bean>
</beans>
二 个人的定时任务实现
下面贴一下根据自己项目用到定时任务的简化版本,使用的工具是idea
1 基本搭建
1.创建一个spring项目
2.pom文件引入必要依赖
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<config>
<!-- https://blog.csdn.net/yk614294861/article/details/84324603 参考第二步作业类不需要继承特定基类 -->
<!-- 任务初始化 第一步 要执行任务的作业类-->
<bean id="initQuartzTask" class="com.test.batch.scheduler.core.InitQuartzTask">
<ref name="schedulerFactoryBean">schedulerFactoryBean</ref>
</bean>
<!-- 第二步 将需要执行的定时任务注入JOB中 -->
<bean id="methodInvokingJobDetailFactoryBean" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<ref name="targetObject">initQuartzTask</ref>
<!-- 任务类中需要执行的方法 也就是InitQuartzTask类中 scheduleTask方法-->
<param name="targetMethod">scheduleTask</param>
</bean>
<!-- 第三步主定时计划 触发器将job注入 SimpleTriggerBean,只支持按照一定频度调用任务,如每隔x分终运行 -->
<bean id="managerTriggerBean" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<ref name="jobDetail">methodInvokingJobDetailFactoryBean</ref>
<param name="startDelay">1000</param><!-- 延时1秒执行任务 -->
<param name="repeatInterval">600000</param><!-- 任务执行周期,每隔 10分钟就启动加载任务执行 -->
</bean>
<!-- 第四步注册SchedulerFactoryBean 配置调度工厂-->
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<param name="applicationContextSchedulerContextKey">applicationContextKey</param>
<list name="triggers">
<ref>managerTriggerBean</ref>
</list>
</bean>
</config>
</spring:beans>
2 业务基本实现
2.1 主定时任务
<bean id="managerTriggerBean" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<ref name="jobDetail">methodInvokingJobDetailFactoryBean</ref>
<param name="startDelay">1000</param><!-- 延时1秒执行任务 -->
<param name="repeatInterval">600000</param><!-- 任务执行周期,每隔 10分钟就启动加载任务执行 -->
</bean>
如图创建了SimpleTriggerFactoryBean类型主定时任务,每10分钟一次读取定时任务表中的数据,经过一定校验后维护调度器。
2.2 业务类定时任务
1)项目的业务类定时任务及相关配置都存在指定数据库中,大概内容为执行频率、是否允许并发、是否发送短信、禁止运行时间等辅助信息。业务如何运行的则只能通过修改业务代码实现。
2)如何把定时任务加载到管理器中:大概流程是主定时任务每10分钟一次从定时任务数据表中读取任务批量执行configTask方法,将定时任务加载到Scheduler或进行更新。任务属性包括要数据表唯一主键、Cron表达式表达式、需要执行的任务名等信息。
优点:不需要重启服务就能定时更新任务信息。
缺点:需要通过数据库直接维护定时任务信息,风险较高;(后面补了基本数据录入页面)
数据库信息修改后需要等待主任务读取数据(间隔为10分钟),不能即刻生效(项目后面进行了优化,可以通过其他方式执行相关任务而不走定时(两个渠道代码逻辑上互不影响,业务上冲突另外解决))。
public static void configTask(ScheduleTask task, Scheduler schedulerFactoryBean) {
try {
// 获取定时工厂类
Scheduler scheduler = schedulerFactoryBean;
// 根据任务ID获取触发器key
TriggerKey triggerKey = TriggerKey.triggerKey(task.getTaskId());
// 获取触发器
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 如果没有触发器,则新建触发器
if(Util.isNullOrEmpty(trigger)) {
// 如果计划任务状态是启用状态,则创建新的触发器
if("1".equals(task.getTaskStatus())) {
createTrigger(task, scheduler);
}
}else {
// Trigger已存在,那么更新相应的定时设置
updateTrigger(task, scheduler, trigger, triggerKey);
}
} catch (SchedulerException e) {
e.printStackTrace();
}
}
图中的task是定时任务信息
public class ScheduleTask {
/** 任务ID */
private String taskId;
/** 任务名称 */
private String taskName;
/** 任务分组 */
private String taskGroup;
/** 任务状态 0禁用 1启用 2删除*/
private String taskStatus;
/** 任务运行时间表达式 */
private String cronExpression;
/** 任务描述 */
private String description;
/** 任务类 */
private String targetObject;
/** 任务方法 */
private String targetMethod;
/** 是否并发 0禁用 1启用 */
private String concurrent;
/** 任务开始时间 **/
private Timestamp startTime;
/**延期时间*/
private long delayTime;
/**操作员信息*/
private String userInfo;
}
private static void createTrigger(ScheduleTask task, Scheduler scheduler) throws SchedulerException {
// 根据任务是否并行,创建不同的job
Class clazz = "1".equals(task.getConcurrent()) ? QuartzTaskFactory.class
: QuartzJobFactoryDisallowConcurrentExecution.class;
// JobBuilder.newJob(自定义任务类).withIdentity(任务名称,组名).build()
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(task.getTaskId()).build();
jobDetail.getJobDataMap().put("ScheduleTask", task);
// 获取调度时间,并赋值
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(task.getCronExpression());
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(task.getTaskId()).withSchedule(scheduleBuilder).build();
// Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job
scheduler.scheduleJob(jobDetail, trigger);
log.info(new Date() + ": 新建" + task.getTaskName() + "计划任务");
}
private static void updateTrigger(ScheduleTask task, Scheduler scheduler, CronTrigger trigger,TriggerKey triggerKey) throws SchedulerException {
// 任务为可用状态
if("1".equals(task.getTaskStatus())) {
// 如果触发器的任务时间跟数据库中任务的任务时间相同,则不用更新设置
if(!trigger.getCronExpression().equalsIgnoreCase(task.getCronExpression())) {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(task.getCronExpression());
// 按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
}
}else {
// 不可用
scheduler.pauseTrigger(trigger.getKey());// 停止触发器
scheduler.unscheduleJob(trigger.getKey());// 移除触发器
scheduler.deleteJob(trigger.getJobKey());// 删除任务
}
}
3 Quartz
Quartz的核心主要是调度器Scheduler、任务Job、触发器Trigger三部分
Quartz的核心是scheduler接口类,而具体的实现类都是通过SchedulerFactory工厂类实现
Quartz API 主要包含了以下接口
- Scheduler – 调度器,将多个JobDetail和Trigger注册到Scheduler后,通过Scheduler控制执行
- Job – 由调度器调度的任务需要实现的接口(需要调度的具体内容)。(主要实现其execute()方法)
- JobDetail – 用于绑定Job,定义任务的实例,包含了任务调度的方案和策略等属性。
- Trigger – 用于定义需要执行的任务和任务的执行时间。(包含SimpleTrigger和CronTrigger)
- JobBuilder – 用于定义/创建 JobDetail 实例。
- TriggerBuilder – 用于定义/创建 Trigger 对象。
3.1 任务Job、JobDetail、JobBuilder
需要执行的调度任务需要实现Job接口重写execute()方法(Job中也只有这一个方法)
public interface Job {
void execute(JobExecutionContext context)
throws JobExecutionException;
}
@DisallowConcurrentExecution
public class QuartzTaskFactory implements Job{
private static Log log = LogFactory.getLog(QuartzTaskFactory.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("----------定时任务开始----------");
ScheduleTask scheduleTask = (ScheduleTask) context.getMergedJobDataMap().get("ScheduleTask");
try {
TaskUtils.invokMethod(scheduleTask,TaskUtils.getApplicationContext(context));
} catch (Exception e) {
log.error(e.getMessage());
}
log.info("----------定时任务结束----------" + scheduleTask.getTaskName());
}
}
补充:@DisallowConcurrentExecution这个注解不是必须的,他的作用是禁止Quart的并发操作。一般针对于Job任务执行时间大于间隔时间的情况(例如任务间隔为15s,当时执行要30s,不过这种情况建议修改业务或者时间间隔)
创建JobBuilder定义JobDetail,并生成唯一的任务标识(JobKey)
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(task.getTaskId()).build();
// JobBuilder类里的JobDetail方法
public JobDetail build() {
JobDetailImpl job = new JobDetailImpl();
job.setJobClass(jobClass);
job.setDescription(description);
if(key == null)
key = new JobKey(Key.createUniqueName(null), null);
job.setKey(key);
job.setDurability(durability);
job.setRequestsRecovery(shouldRecover);
if(!jobDataMap.isEmpty())
job.setJobDataMap(jobDataMap);
return job;
}
3.2 触发器 Trigger、TriggerBuilder
整体来说触发器是用来告诉调度程序作业什么是否触发的,例如下图中跟时间有关属性、JobKey属性(Trigger绑定的Job实例标识)、是否重复执行、执行间隔等
常见的Trigger主要是SimpleTrigger和CronTrigger
- SimpleTrigger:在一个指定时间段内执行一次作业任务或是在指定的时间间隔内多次执行作业任务。
- CronTrigger:基于日历的作业调度器,而不是像SimpleTrigger那样精确指定间隔时间,因此SimpleTrigger能够实现的,CronTrigger都能够实现,所以普遍都用CronTrigger。
项目使用的就是CronTrigger,先通过CronScheduleBuilder根据Cron表达式生成调度规则,再通过TriggerBuilder构建CronTrigger类型的触发器,最后用scheduler.scheduleJob(jobDetail, trigger);方法将cronTrigger触发器和任务jobDetail绑定。
由上述步骤可以看出,Cron变换后需要重新构建CronScheduleBuilder再构建CronTrigger,然后调用scheduler.rescheduleJob(triggerKey, CronTrigger);更新触发器才行。其中triggerKey是触发器的Key,是通过TriggerKey.triggerKey(taskId)获取到的。
备注:一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job
3.3 调度器Scheduler、SchedulerFactory、SchedulerFactoryBean
主要方法 启动、暂停、关闭等
项目使用SchedulerFactoryBean在afterPropertiesSet方法里初始化SchedulerFactory,代码里默认生成StdSchedulerFactory这一个(DirectSchedulerFactory可以在代码里定制Schduler 参数)。并且查看相关代码,可以知道还要设置