一、前言

Quartz 是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:

1.持久性作业 - 就是保持调度定时的状态
2.作业管理 - 对调度作业进行有效的管理

二、引入依赖

1
2
3
4
        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

三、Task或Job实现

首先我们需要定义实现一个定时功能的接口,我们可以称之为Task(或Job),如定时发送邮件的task(Job),重启机器的task(Job),优惠券到期发送短信提醒的task(Job),在springboot中只需要继承QuartzJobBean并重写executeInternal方法。

1
2
3
4
5
6
7
public class TestQuartz extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + "\t\tquartz task app is running…… ");
}
}

四、添加配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
public class QuartzConfig {

@Bean
public JobDetail teatQuartzDetail() {
return JobBuilder.newJob(TestQuartz.class).storeDurably().build();

}

@Bean
public Trigger testQuartzTrigger() {
SimpleScheduleBuilder scheduleBuilder =
SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(10) // 每隔两秒实行一次
.repeatForever();

return TriggerBuilder.newTrigger().startAt(new Date())
.forJob(teatQuartzDetail())
.withSchedule(scheduleBuilder).build();
}

@Bean
public Trigger testQuartzTrigger2() {
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(teatQuartzDetail())
.withIdentity("myTrigger1","myTriggerGroup1")
.usingJobData("job_trigger_param","job_trigger_param1")
.startNow()
//.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
.withSchedule(CronScheduleBuilder.cronSchedule("0/8 * * * * ? 2020"))
.build();
return trigger;
}

}

五、测试

运行项目,日志打印如下:

2020-10-19 18:27:24 quartz task app is running……
2020-10-19 18:27:28 quartz task app is running……
2020-10-19 18:27:32 quartz task app is running……
2020-10-19 18:27:38 quartz task app is running……
2020-10-19 18:27:40 quartz task app is running……
2020-10-19 18:27:48 quartz task app is running……
2020-10-19 18:27:48 quartz task app is running……

我们发现执行频率一个是8秒一次,一个是10秒一次

六、quartz持久化

6.1 背景

最近在做项目,项目中有个需求:需要使用定时任务,这个定时任务需要即时生效。
Quartz提供两种基本作业存储类型:
1.RAMJobStore:RAM也就是内存,默认情况下Quartz会将任务调度存在内存中,这种方式性能是最好的,因为内存的速度是最快的。不好的地方就是数据缺乏持久性,但程序崩溃或者重新发布的时候,所有运行信息都会丢失
2.JDBC作业存储:存到数据库之后,可以做单点也可以做集群,当任务多了之后,可以统一进行管理。关闭或者重启服务器,运行的信息都不会丢失。缺点就是运行速度快慢取决于连接数据库的快慢。

只有持久化以后,这样定时任务就可以做集群,任务可以进行管理,随时停止、暂停、修改任务。

6.2 Quartz初始化

6.2.1 脚本下载

官网 下载的quartz文件。如下所示:

下载后解压,在目录src\org\quartz\impl\jdbcjobstore中有对应的数据库初始化脚本,这里我们选择mysql数据库,那么需要导入tables_mysql_innodb.sql。

6.2.2 创建数据库

1
2
3
4
5
6
    create database quartzdb default character set utf8 collate utf8_general_ci;
create user 'quartz'@'%' identified by 'quartz#123';
create user 'quartz'@'localhost' identified by 'quartz#123';
grant all privileges on quartzdb.* to 'quartz'@'%' identified by 'quartz#123';
grant all privileges on quartzdb.* to 'quartz'@'localhost' identified by 'quartz#123';
flush privileges;

6.2.3 导入数据

导入上面脚本后,具体表信息如下:

表解释如下:

表名含义
qrtz_blob_triggers以Blob 类型存储的触发器。
qrtz_calendars存放日历信息, quartz可配置一个日历来指定一个时间范围。
qrtz_cron_triggers存放cron类型的触发器。
qrtz_fired_triggers存放已触发的触发器。
qrtz_job_details存放一个jobDetail信息。
qrtz_locks存储程序的悲观锁的信息(假如使用了悲观锁)。
qrtz_paused_trigger_graps存放暂停掉的触发器。
qrtz_scheduler_state调度器状态。
qrtz_simple_triggers简单触发器的信息。
qrtz_simprop_listeners触发器监听器。
qrtz_triggers触发器的基本信息。

6.3 项目配置

6.3.1 依赖引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

6.3.2 application.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
server:
port: 10005

spring:
datasource:
druid:
# 数据库访问配置, 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/quartzdb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&allowMultiQueries=true&useSSL=false
username: quartz
password: quartz#123
# 连接池配置
initial-size: 5
min-idle: 5
max-active: 20
# 连接等待超时时间
max-wait: 30000
# 配置检测可以关闭的空闲连接间隔时间
time-between-eviction-runs-millis: 60000
# 配置连接在池中的最小生存时间
min-evictable-idle-time-millis: 300000
validation-query: select '1' from dual
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-open-prepared-statements: 20
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters, 去掉后监控界面sql无法统计, 'wall'用于防火墙
filters: stat,wall
# Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
aop-patterns: com.wno704.boot.service.*


# WebStatFilter配置
web-stat-filter:
enabled: true
# 添加过滤规则
url-pattern: /*
# 忽略过滤的格式
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'

# StatViewServlet配置
stat-view-servlet:
enabled: true
# 访问路径为/druid时,跳转到StatViewServlet
url-pattern: /druid/*
# 是否能够重置数据
reset-enable: false
# 需要账号密码才能访问控制台
login-username: druid
login-password: druid123
# IP白名单
# allow: 127.0.0.1
# IP黑名单(共同存在时,deny优先于allow)
# deny: 192.168.1.218

# 配置StatFilter
filter:
stat:
log-slow-sql: true

6.3.3 quartz.properties配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
org.quartz.scheduler.instanceName=MyScheduler
#ID设置为自动获取 每一个必须不同 (所有调度器实例中是唯一的)
org.quartz.scheduler.instanceId=AUTO
#指定调度程序的主线程是否应该是守护线程
org.quartz.scheduler.makeSchedulerThreadDaemon=true
#ThreadPool实现的类名
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#ThreadPool配置线程守护进程
org.quartz.threadPool.makeThreadsDaemons=true
#线程数量
org.quartz.threadPool.threadCount=20
#线程优先级
org.quartz.threadPool.threadPriority=5
#数据保存方式为持久化
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
#StdJDBCDelegate说明支持集群
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#quartz内部表的前缀
org.quartz.jobStore.tablePrefix=QRTZ_
#是否加入集群
org.quartz.jobStore.isClustered=true
#容许的最大作业延长时间
org.quartz.jobStore.misfireThreshold=25000

6.3.4 quartz配置类

ScheduleConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class ScheduleConfig {

@Bean
public ThreadPoolTaskExecutor scheduleJobExecutorService(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(20);
executor.setKeepAliveSeconds(30);
executor.setThreadNamePrefix("fb-Job-Thread");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}

@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setQuartzProperties(quartzProperties());

factory.setSchedulerName("FB_Quartz_Scheduler");
// 延时启动
factory.setStartupDelay(1);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
// 启动时更新己存在的 Job
factory.setOverwriteExistingJobs(true);
// 设置自动启动,默认为 true
factory.setAutoStartup(true);
return factory;
}

@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}

}

6.4 任务自定义配置与执行记录

6.4.1 定时任务配置表与记录表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
DROP TABLE IF EXISTS QRTZ_JOB;
DROP TABLE IF EXISTS QRTZ_JOB_log;

CREATE TABLE `QRTZ_JOB` (
`JOB_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`BEAN_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'spring bean名称',
`METHOD_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '方法名',
`PARAMS` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '参数',
`CRON_EXPRESSION` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'cron表达式',
`START_TIME` datetime(0) NOT NULL DEFAULT NOW() COMMENT '开始执行时间',
`END_TIME` datetime(0) NOT NULL DEFAULT NOW() COMMENT '结束执行时间',
`STATUS` char(2) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '任务状态 0:正常 1:暂停 2:删除',
`UPDATE_STATUS` char(2) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '加载状态 0:未加载 1:已加载',
`REMARK` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
`CREATE_TIME` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`JOB_ID`),
KEY `t_job_create_time` (`CREATE_TIME`)
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '定时任务表' ROW_FORMAT = Dynamic;


-- ----------------------------
-- Records of t_job
-- ----------------------------
INSERT INTO `QRTZ_JOB` VALUES (1, 'loadUpdateTask', 'loadUpdateJob', NULL, '0/30 * * * * ?','2018-03-20 21:55:14', '3000-01-01 00:00:00', '0', '0', '更新 task自动读取', now());


-- ----------------------------
-- Table structure for t_job_log
-- ----------------------------
CREATE TABLE `QRTZ_JOB_LOG` (
`LOG_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务日志id',
`JOB_ID` bigint(20) NOT NULL COMMENT '任务id',
`BEAN_NAME` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'spring bean名称',
`METHOD_NAME` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '方法名',
`PARAMS` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '参数',
`STATUS` char(2) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '任务状态 0:成功 1:失败',
`ERROR` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '失败信息',
`TIMES` decimal(11, 0) NULL DEFAULT NULL COMMENT '耗时(单位:毫秒)',
`CREATE_TIME` datetime(0) DEFAULT NOW() COMMENT '创建时间',
PRIMARY KEY (`LOG_ID`),
KEY `QRTZ_JOB_LOG_create_time` (`CREATE_TIME`)
) ENGINE = MyISAM AUTO_INCREMENT = 0 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '调度日志表' ROW_FORMAT = Dynamic;

6.4.2 配置表与记录表的实体

Job.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Data
public class Job implements Serializable {

private static final long serialVersionUID = 21312312424L;

/**
* 任务调度参数 key
*/
public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY";

public enum ScheduleStatus {
/**
* 正常
*/
NORMAL("0"),
/**
* 暂停
*/
PAUSE("1"),
/**
* 删除
*/
DEL("2");

private String value;

ScheduleStatus(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}

private Long jobId;

private String beanName;

private String methodName;

private String params;

private String cronExpression;

private String startTime;

private String endTime;

private String status;

private String remark;

private String createTime;

}

JobLog.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Data
public class JobLog implements Serializable {

private static final long serialVersionUID = 1L;
/**
* 任务执行成功
*/
public static final String JOB_SUCCESS = "0";
/**
* 任务执行失败
*/
public static final String JOB_FAIL = "1";

private Long logId;

private Long jobId;

private String beanName;

private String methodName;

private String params;

private String status;

private String errorInfo;

private Long times;

private Date createTime;
}

6.4.3 配置表与记录表的实体与实现

JobService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public interface JobService {

public int init();
/**
* 获取定时任务
*
* @param jobId 任务id
* @return 定时任务
*/
Job findJob(Long jobId);

/**
* 创建定时任务
*
* @param job job
*/
void createJob(Job job);

/**
* 更新定时任务
*
* @param job 定时任务
*/
void updateJob(Job job);

/**
* 删除定时任务
*
* @param jobIds 定时任务id数组
*/
void deleteJobs(String[] jobIds);

/**
* 批量更新定时任务状态
*
* @param jobIds 定时任务id
* @param status 待更新状态
*/
void updateBatch(String jobIds, String status);

/**
* 运行定时任务
*
* @param jobIds 定时任务id
*/
void run(String jobIds);

/**
* 暂停定时任务
*
* @param jobIds 定时任务id
*/
void pause(String jobIds);

/**
* 恢复定时任务
*
* @param jobIds 定时任务id
*/
void resume(String jobIds);
}

JobServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Service("JobService")
@RequiredArgsConstructor
public class JobServiceImpl implements JobService {

private final Scheduler scheduler;

@Autowired
private JdbcTemplate jdbcTemplateQuartz;

@PostConstruct
@Override
public int init() {
String sql = "SELECT T.JOB_ID,T.BEAN_NAME,T.METHOD_NAME,T.PARAMS,T.CRON_EXPRESSION,T.START_TIME,T.END_TIME,T.STATUS,T.REMARK,T.CREATE_TIME FROM QRTZ_JOB T WHERE T.UPDATE_STATUS = 0 ";

List<Job> scheduleJobList = this.jdbcTemplateQuartz.query(sql, new BeanPropertyRowMapper<Job>(Job.class));
scheduleJobList.forEach(scheduleJob -> {
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJob.getJobId());
if (cronTrigger == null) {
ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
} else {
ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
}
String upSql = "UPDATE QRTZ_JOB T SET T.UPDATE_STATUS = '1' WHERE T.JOB_ID = ? ";
this.jdbcTemplateQuartz.update(upSql, new Object[] {scheduleJob.getJobId()});
});
return scheduleJobList.size();
}

@Override
public Job findJob(Long jobId) {
// TODO Auto-generated method stub
String sql = "SELECT T.JOB_ID,T.BEAN_NAME,T.METHOD_NAME,T.PARAMS,T.CRON_EXPRESSION,T.START_TIME,T.END_TIME,T.STATUS,T.REMARK,T.CREATE_TIME FROM QRTZ_JOB T WHERE T.JOB_ID = ? ";
List<Job> scheduleJobList = this.jdbcTemplateQuartz.query(sql, new Object[] {jobId}, new BeanPropertyRowMapper<Job>(Job.class));
return scheduleJobList.get(0);
}

@Override
public void createJob(Job job) {
// TODO Auto-generated method stub

}

@Override
public void updateJob(Job job) {
// TODO Auto-generated method stub

}

@Override
public void deleteJobs(String[] jobIds) {
// TODO Auto-generated method stub

}

@Override
public void updateBatch(String jobIds, String status) {
// TODO Auto-generated method stub
}

@Override
public void run(String jobIds) {
// TODO Auto-generated method stub
String[] list = jobIds.split(",");
Arrays.stream(list).forEach(jobId -> ScheduleUtils.run(scheduler, this.findJob(Long.valueOf(jobId))));
}

@Override
public void pause(String jobIds) {
// TODO Auto-generated method stub
}

@Override
public void resume(String jobIds) {
// TODO Auto-generated method stub
}

}

JobLogService.java

1
2
3
4
5
6
7
8
public interface JobLogService {
/**
* 保存定时任务日志
*
* @param log 定时任务日志
*/
void saveJobLog(JobLog log);
}

JobLogServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service("JobLogService")
public class JobLogServiceImpl implements JobLogService {

@Autowired
private JdbcTemplate jdbcTemplateQuartz;

@Override
public void saveJobLog(JobLog jobLog) {
String sql = "INSERT INTO QRTZ_JOB_LOG (JOB_ID,BEAN_NAME,METHOD_NAME,PARAMS,STATUS,ERROR,TIMES) VALUES (?,?,?,?,?,?,?)";
this.jdbcTemplateQuartz.update(sql,
new Object[] {jobLog.getJobId(),jobLog.getBeanName(),
jobLog.getMethodName(),jobLog.getParams(),jobLog.getStatus(),
jobLog.getErrorInfo(),jobLog.getTimes()
});
}
}

6.5 定时任务控制

6.5.1 基础工具类

工具类 SpringContextUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}

public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}

public static <T> T getBean(String name, Class<T> 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);
}


}

DateUtil.java

1
2
3
4
5
6
7
8
9
10
public class DateUtil {

public static final String FULL_TIME_SPLIT_PATTERN = "yyyy-MM-dd HH:mm:ss";

public static Date getFormatDate(String dateStr) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(FULL_TIME_SPLIT_PATTERN);
return sdf.parse(dateStr);
}

}

6.5.2 定时任务执行类

ScheduleRunnable.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Slf4j
public class ScheduleRunnable implements Runnable {

private Object target;
private Method method;
private String params;

ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException {
this.target = SpringContextUtil.getBean(beanName);
this.params = params;

if (StringUtils.isNotBlank(params)) {
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
} else {
this.method = target.getClass().getDeclaredMethod(methodName);
}
}

@Override
public void run() {
try {
ReflectionUtils.makeAccessible(method);
if (StringUtils.isNotBlank(params)) {
method.invoke(target, params);
} else {
method.invoke(target);
}
} catch (Exception e) {
log.error("执行定时任务失败", e);
}
}

}

6.5.3 定时任务工具类

ScheduleUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@Slf4j
public class ScheduleUtils {

private static final String JOB_NAME_PREFIX = "TASK_";

/**
* 获取触发器 key
*/
private static TriggerKey getTriggerKey(Long jobId) {
return TriggerKey.triggerKey(JOB_NAME_PREFIX + jobId);
}

/**
* 获取jobKey
*/
private static JobKey getJobKey(Long jobId) {
return JobKey.jobKey(JOB_NAME_PREFIX + jobId);
}

/**
* 获取表达式触发器
*/
public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) {
try {
return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
} catch (SchedulerException e) {
log.error("获取Cron表达式失败", e);
}
return null;
}

/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, Job scheduleJob) {
try {
// 构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId()))
.build();
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().startAt(DateUtil.getFormatDate(scheduleJob.getStartTime())).endAt(DateUtil.getFormatDate(scheduleJob.getEndTime()))
.withIdentity(getTriggerKey(scheduleJob.getJobId()))
.withSchedule(scheduleBuilder).build();
// 放入参数,运行时的方法可以获取
trigger.getJobDataMap().put(Job.JOB_PARAM_KEY, scheduleJob);
trigger.getJobDataMap().put("jobId", scheduleJob.getJobId());
trigger.getJobDataMap().put("beanName", scheduleJob.getBeanName());
trigger.getJobDataMap().put("methodName", scheduleJob.getMethodName());
trigger.getJobDataMap().put("params", scheduleJob.getParams());

scheduler.scheduleJob(jobDetail, trigger);

// 暂停任务
if (scheduleJob.getStatus().equals(Job.ScheduleStatus.PAUSE.getValue())) {
pauseJob(scheduler, scheduleJob.getJobId());
}else if (scheduleJob.getStatus().equals(Job.ScheduleStatus.DEL.getValue())) {
deleteScheduleJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException | ParseException e) {
log.error("创建定时任务失败", e);
}
}

/**
* 更新定时任务
*/
public static void updateScheduleJob(Scheduler scheduler, Job scheduleJob) {
try {
deleteScheduleJob(scheduler, scheduleJob.getJobId());

// 构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId()))
.build();
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();

// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().startAt(DateUtil.getFormatDate(scheduleJob.getStartTime())).endAt(DateUtil.getFormatDate(scheduleJob.getEndTime()))
.withIdentity(getTriggerKey(scheduleJob.getJobId()))
.withSchedule(scheduleBuilder).build();
// 放入参数,运行时的方法可以获取
trigger.getJobDataMap().put(Job.JOB_PARAM_KEY, scheduleJob);
trigger.getJobDataMap().put("jobId", scheduleJob.getJobId());
trigger.getJobDataMap().put("beanName", scheduleJob.getBeanName());
trigger.getJobDataMap().put("methodName", scheduleJob.getMethodName());
trigger.getJobDataMap().put("params", scheduleJob.getParams());
scheduler.scheduleJob(jobDetail, trigger);

// 暂停任务
if (scheduleJob.getStatus().equals(Job.ScheduleStatus.PAUSE.getValue())) {
pauseJob(scheduler, scheduleJob.getJobId());
}else if (scheduleJob.getStatus().equals(Job.ScheduleStatus.DEL.getValue())) {
deleteScheduleJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException | ParseException e) {
log.error("更新定时任务失败", e);
}
}

/**
* 立即执行任务
*/
public static void run(Scheduler scheduler, Job scheduleJob) {
try {
// 参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(Job.JOB_PARAM_KEY, scheduleJob);
dataMap.put("jobId", scheduleJob.getJobId());
dataMap.put("beanName", scheduleJob.getBeanName());
dataMap.put("methodName", scheduleJob.getMethodName());
dataMap.put("params", scheduleJob.getParams());
scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
} catch (SchedulerException e) {
log.error("执行定时任务失败", e);
}
}

/**
* 暂停任务
*/
public static void pauseJob(Scheduler scheduler, Long jobId) {
try {
scheduler.pauseJob(getJobKey(jobId));
} catch (SchedulerException e) {
log.error("暂停定时任务失败", e);
}
}

/**
* 恢复任务
*/
public static void resumeJob(Scheduler scheduler, Long jobId) {
try {
scheduler.resumeJob(getJobKey(jobId));
} catch (SchedulerException e) {
log.error("恢复定时任务失败", e);
}
}

/**
* 删除定时任务
*/
public static void deleteScheduleJob(Scheduler scheduler, Long jobId) {
try {
scheduler.deleteJob(getJobKey(jobId));
} catch (SchedulerException e) {
log.error("删除定时任务失败", e);
}
}
}

6.5.4 Job

ScheduleJob.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Slf4j
public class ScheduleJob extends QuartzJobBean {

private ThreadPoolTaskExecutor scheduleJobExecutorService = SpringContextUtil.getBean("scheduleJobExecutorService", ThreadPoolTaskExecutor.class);

@Autowired
private JobService jobService;

@SneakyThrows
@Override
protected void executeInternal(JobExecutionContext context) {

JobDataMap jobDataMap = context.getTrigger().getJobDataMap();
Job scheduleJob = new Job();
scheduleJob.setJobId((Long)context.getMergedJobDataMap().get("jobId"));
scheduleJob.setBeanName((String)context.getMergedJobDataMap().get("beanName"));
scheduleJob.setMethodName((String)context.getMergedJobDataMap().get("methodName"));
scheduleJob.setParams((String)context.getMergedJobDataMap().get("params"));

// 获取spring bean
JobLogService scheduleJobLogService = SpringContextUtil.getBean(JobLogService.class);
JobLog jobLog = new JobLog();
jobLog.setJobId(scheduleJob.getJobId());
jobLog.setBeanName(scheduleJob.getBeanName());
jobLog.setMethodName(scheduleJob.getMethodName());
jobLog.setParams(scheduleJob.getParams());
jobLog.setCreateTime(new Date());
long startTime = System.currentTimeMillis();
try {
// 执行任务
log.info("任务准备执行,任务ID:{}", scheduleJob.getJobId());
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(),
scheduleJob.getParams());
Future<?> future = scheduleJobExecutorService.submit(task);
future.get();
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes(times);
// 任务状态 0:成功 1:失败
jobLog.setStatus(JobLog.JOB_SUCCESS);

log.info("任务执行完毕,任务ID:{} 总共耗时:{} 毫秒", scheduleJob.getJobId(), times);
} catch (Exception e) {
log.error("任务执行失败,任务ID:" + scheduleJob.getJobId(), e);
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes(times);
// 任务状态 0:成功 1:失败
jobLog.setStatus(JobLog.JOB_FAIL);
jobLog.setErrorInfo(StringUtils.substring(e.toString(), 0, 2000));
} finally {
scheduleJobLogService.saveJobLog(jobLog);
}
}

}

6.6 定时任务读取配置task

LoadUpdateTask.java

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@Component
public class LoadUpdateTask {
@Autowired
private JobService jobService;

public void loadUpdateJob() {
log.info("加载需要修改的job-开始~~~");
log.info("已加载{}个job~~~",jobService.init());
log.info("加载需要加载的job-结束~~~");
}
}

6.7 测试

2020-10-20 17:01:45.050 INFO 5480 --- [eduler_Worker-1] com.wno704.boot.util.ScheduleJob : 任务准备执行,任务ID:1
2020-10-20 17:01:45.055 INFO 5480 --- [ fb-Job-Thread1] com.wno704.boot.job.LoadUpdateTask : 加载需要修改的job-开始~~~
2020-10-20 17:01:45.056 INFO 5480 --- [ fb-Job-Thread1] com.wno704.boot.job.LoadUpdateTask : 已加载0个job~~~
2020-10-20 17:01:45.057 INFO 5480 --- [ fb-Job-Thread1] com.wno704.boot.job.LoadUpdateTask : 加载需要加载的job-结束~~~
2020-10-20 17:01:45.057 INFO 5480 --- [eduler_Worker-1] com.wno704.boot.util.ScheduleJob : 任务执行完毕,任务ID:1 总共耗时:7 毫秒

查看记录表

注意:如果定时器无法加载,有可能是已经加载,或者失效等,需要清空定时器相关表,执行以下删除脚本:

1
2
3
4
5
6
7
8
9
10
11
12
delete from qrtz_blob_triggers;
delete from qrtz_calendars;
delete from qrtz_cron_triggers;
delete from qrtz_fired_triggers;
delete from qrtz_job_log;
delete from qrtz_locks;
delete from qrtz_paused_trigger_grps;
delete from qrtz_scheduler_state;
delete from qrtz_simple_triggers;
delete from qrtz_simprop_triggers;
delete from qrtz_triggers;
delete from qrtz_job_details;