boot-admin整合Quartz实现动态管理定时任务
淄博烧烤爆红出了圈,当你坐在八大局的烧烤摊,面前是火炉、烤串、小饼和蘸料,音乐响起,啤酒倒满,烧烤灵魂的party即将开场的时候,你系统中的Scheduler(调试器),也自动根据设定的Trigger(触发器),从容优雅的启动了一系列的Job(后台定时任务)。工作一切早有安排,又何须费心劳神呢?因为boot-admin早已将Quartz这块肉串在了烤签上!
Quartz是一款Java编写的开源任务调度框架,同时它也是Spring默认的任务调度框架。它的作用其实类似于Timer定时器以及ScheduledExecutorService调度线程池,当然Quartz作为一个独立的任务调度框架表现更为出色,功能更强大,能够定义更为复杂的执行规则。 boot-admin 是一款采用前后端分离模式、基于 SpringCloud 微服务架构 + vue-element-admin 的 SaaS 后台管理框架。 那么boot-admin怎样才能将Quartz串成串呢?一共分三步:
# 加入依赖
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
2
3
4
5
# 前端整合
vue页面以el-table作为任务的展示控件,串起任务的创建、修改、删除、挂起、恢复、状态查看等功能。
# vue页面
<template>
<div class="app-container" style="background-color: #FFFFFF;">
<!--功能按钮区-->
<div class="cl pd-5 bg-1 bk-gray">
<div align="left" style="float:left">
<el-button size="mini" type="primary" @click="search()">查询</el-button>
<el-button size="mini" type="primary" @click="handleadd()">添加</el-button>
</div>
<div align="right">
<!--分页控件-->
<div style="align:right">
<el-pagination
:current-page="BaseTableData.page.currentPage"
:page-sizes="[5,10,20,50,100,500]"
:page-size="BaseTableData.page.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="BaseTableData.page.total"
@size-change="handlePageSizeChange"
@current-change="handlePageCurrentChange"
/>
</div>
<!--分页控件-->
</div>
</div>
<!--功能按钮区-->
<!--表格-->
<el-table max-height="100%" :data="BaseTableData.table" style="width: 100%" :border="true">
<el-table-column type="index" :index="indexMethod" />
<el-table-column prop="jobName" label="任务名称" width="100px" />
<el-table-column prop="jobGroup" label="任务所在组" width="100px" />
<el-table-column prop="jobClassName" label="任务类名" />
<el-table-column prop="cronExpression" label="表达式" width="120" />
<el-table-column prop="timeZoneId" label="时区" width="120" />
<el-table-column prop="startTime" label="开始" width="120" :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"/>
<el-table-column prop="nextFireTime" label="下次" width="120" :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"/>
<el-table-column prop="previousFireTime" label="上次" width="120" :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"/>
<el-table-column prop="triggerState" label="状态" width="80">
<template slot-scope="scope">
<p v-if="scope.row.triggerState=='NORMAL'">等待</p>
<p v-if="scope.row.triggerState=='PAUSED'">暂停</p>
<p v-if="scope.row.triggerState=='NONE'">删除</p>
<p v-if="scope.row.triggerState=='COMPLETE'">结束</p>
<p v-if="scope.row.triggerState=='ERROR'">错误</p>
<p v-if="scope.row.triggerState=='BLOCKED'">阻塞</p>
</template>
</el-table-column>
<el-table-column label="操作" width="220px">
<template slot-scope="scope">
<el-button type="warning" size="least" title="挂起" @click="handlePause(scope.row)">挂起</el-button>
<el-button type="primary" size="least" title="恢复" @click="handleResume(scope.row)">恢复</el-button>
<el-button type="danger" size="least" title="删除" @click="handleDelete(scope.row)">删除</el-button>
<el-button type="success" size="least" title="修改" @click="handleUpdate(scope.row)">修改</el-button>
</template>
</el-table-column>
</el-table>
<!--表格-->
<!--主表单弹出窗口-->
<el-dialog
v-cloak
title="维护"
:visible.sync="InputBaseInfoDialogData.dialogVisible"
:close-on-click-modal="InputBaseInfoDialogData.showCloseButton"
top="5vh"
:show-close="InputBaseInfoDialogData.showCloseButton"
:fullscreen="InputBaseInfoDialogData.dialogFullScreen"
>
<!--弹窗头部header-->
<div slot="title" style="margin-bottom: 10px">
<div align="left" style="float:left">
<h3>定时任务管理</h3>
</div>
<div align="right">
<el-button type="text" title="全屏显示" @click="resizeInputBaseInfoDialogMax()"><i class="el-icon-arrow-up" /></el-button>
<el-button type="text" title="以弹出窗口形式显示" @click="resizeInputBaseInfoDialogNormal()"><i class="el-icon-arrow-down" /></el-button>
<el-button type="text" title="关闭" @click="closeInputBaseInfoDialog()"><i class="el-icon-error" /></el-button>
</div>
</div>
<!--弹窗头部header-->
<!--弹窗表单-->
<el-form
ref="InputBaseInfoForm"
:status-icon="InputBaseInfoDialogData.statusIcon"
:model="InputBaseInfoDialogData.data"
class="demo-ruleForm"
>
<el-form-item label="原任务名称" :label-width="InputBaseInfoDialogData.formLabelWidth" prop="jobName">
{{ InputBaseInfoDialogData.data.oldJobName }}【修改任务时使用】
</el-form-item>
<el-form-item label="原任务分组" :label-width="InputBaseInfoDialogData.formLabelWidth" prop="jobGroup">
{{ InputBaseInfoDialogData.data.oldJobGroup }}【修改任务时使用】
</el-form-item>
<el-form-item label="任务名称" :label-width="InputBaseInfoDialogData.formLabelWidth" prop="jobName">
<el-input v-model="InputBaseInfoDialogData.data.jobName" auto-complete="off" />
</el-form-item>
<el-form-item label="任务分组" :label-width="InputBaseInfoDialogData.formLabelWidth" prop="jobGroup">
<el-input v-model="InputBaseInfoDialogData.data.jobGroup" auto-complete="off" />
</el-form-item>
<el-form-item label="类名" :label-width="InputBaseInfoDialogData.formLabelWidth" prop="jobClassName">
<el-input v-model="InputBaseInfoDialogData.data.jobClassName" auto-complete="off" />
</el-form-item>
<el-form-item label="表达式" :label-width="InputBaseInfoDialogData.formLabelWidth" prop="cronExpression">
<el-input v-model="InputBaseInfoDialogData.data.cronExpression" auto-complete="off" />
</el-form-item>
</el-form>
<!--弹窗表单-->
<!--弹窗尾部footer-->
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="saveInputBaseInfoForm()">保 存</el-button>
</div>
<!--弹窗尾部footer-->
</el-dialog>
<!--弹出窗口-->
<!--查看场所弹出窗口-->
<el-dialog
v-cloak
title="修改任务"
:visible.sync="ViewBaseInfoDialogData.dialogVisible"
:close-on-click-modal="ViewBaseInfoDialogData.showCloseButton"
top="5vh"
:show-close="ViewBaseInfoDialogData.showCloseButton"
:fullscreen="ViewBaseInfoDialogData.dialogFullScreen"
>
<!--弹窗头部header-->
<div slot="title" style="margin-bottom: 10px">
<div align="left" style="float:left">
<h3>修改任务</h3>
</div>
<div align="right">
<el-button type="text" @click="dialogResize('ViewBaseInfoDialog',true)"><i class="el-icon-arrow-up" title="全屏显示" /></el-button>
<el-button type="text" @click="dialogResize('ViewBaseInfoDialog',false)"><i
class="el-icon-arrow-down"
title="以弹出窗口形式显示"
/></el-button>
<el-button type="text" @click="dialogClose('ViewBaseInfoDialog')"><i class="el-icon-error" title="关闭" /></el-button>
</div>
</div>
<!--弹窗头部header-->
<!--弹窗表单-->
<el-form
ref="ViewBaseInfoForm"
:status-icon="ViewBaseInfoDialogData.statusIcon"
:model="ViewBaseInfoDialogData.data"
class="demo-ruleForm"
>
<el-form-item label="表达式" :label-width="ViewBaseInfoDialogData.formLabelWidth" prop="cronExpression">
{{ this.BaseTableData.currentRow.cronExpression }}
</el-form-item>
</el-form>
<!--弹窗表单-->
</el-dialog>
</div>
</template>
<script>
import {
getBlankJob,
fetchJobPage,
getUpdateObject,
saveJob,
pauseJob,
resumeJob,
deleteJob
} from '@/api/job'
export default {
name: 'Jobmanage',
data: function() {
return {
/**
* 后台服务忙,防止重复提交的控制变量
* */
ServiceRunning: false,
/**
*表格和分页组件
* */
BaseTableData: {
currentRow: {},
page: {
currentPage: 1,
pageSize: 20,
pageNum: 1,
pages: 1,
size: 5,
total: 1
},
/**
*主表格数据
* */
table: [],
/**
*勾选选中的数据
* */
selected: []
},
InputBaseInfoDialogData: {
data: {},
dialogVisible: false,
dialogFullScreen: false,
formLabelWidth: '180px',
showCloseButton: false,
statusIcon: true
},
ViewBaseInfoDialogData: {
cronExpression: '',
dialogVisible: false,
dialogFullScreen: true,
formLabelWidth: '180px'
}
}
},
/**
*初始化自动执行查询表格数据--不用调整
**/
mounted: function() {
this.loadTableData()
},
methods: {
/**
* 查询---------根据实际调整参数
*/
async loadTableData() {
if (this.ServiceRunning) {
this.$message({
message: '请不要重复点击。',
type: 'warning'
})
return
}
this.ServiceRunning = true
const response = await fetchJobPage(this.BaseTableData.page)
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
const {
data
} = response
this.BaseTableData.page.total = data.total
this.BaseTableData.table = data.records
this.ServiceRunning = false
},
/**
* 每页大小调整事件
* @param val
*/
handlePageSizeChange(val) {
if (val != this.BaseTableData.page.pageSize) {
this.BaseTableData.page.pageSize = val
this.loadTableData()
}
},
/**
* 当前面号调整事件
* @param val
*/
handlePageCurrentChange(val) {
if (val != this.BaseTableData.page.currentPage) {
this.BaseTableData.page.currentPage = val
this.loadTableData()
}
},
dialogResize(dialogName, toMax) {
VFC_dialogResize(dialogName, toMax)
},
resizeInputBaseInfoDialogMax() {
this.InputBaseInfoDialogData.dialogFullScreen = true
},
resizeInputBaseInfoDialogNormal() {
this.InputBaseInfoDialogData.dialogFullScreen = false
},
dialogClose(dialogName) {
},
closeInputBaseInfoDialog() {
this.InputBaseInfoDialogData.dialogVisible = false
this.loadTableData()
},
async getBlankForm() {
const response = await getBlankJob()
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
const {
data
} = response
this.InputBaseInfoDialogData.data = data
},
async getUpdateForm(row) {
const response = await getUpdateObject(row)
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
const {
data
} = response
this.InputBaseInfoDialogData.data = data
},
// 弹出对话框
handleadd() {
this.getBlankForm()
this.InputBaseInfoDialogData.dialogVisible = true
},
handleUpdate(row) {
if (row.triggerState !== 'PAUSED') {
this.$message({
message: '请先挂起任务,再修改。',
type: 'warning'
})
return
}
this.getUpdateForm(row)
this.InputBaseInfoDialogData.dialogVisible = true
},
search() {
this.loadTableData()
},
/**
* 提交修改主表单
*/
async saveInputBaseInfoForm() {
if (this.ServiceRunning) {
this.$message({
message: '请不要重复点击。',
type: 'warning'
})
return
}
this.ServiceRunning = true
const response = await saveJob(this.InputBaseInfoDialogData.data)
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
this.ServiceRunning = false
this.$message({
message: '数据保存成功。',
type: 'success'
})
this.loadTableData()
},
async handlePause(row) {
if (this.ServiceRunning) {
this.$message({
message: '请不要重复点击。',
type: 'warning'
})
return
}
this.ServiceRunning = true
const response = await pauseJob(row)
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
this.ServiceRunning = false
this.$message({
message: '任务成功挂起。',
type: 'success'
})
this.loadTableData()
},
async handleResume(row) {
if (this.ServiceRunning) {
this.$message({
message: '请不要重复点击。',
type: 'warning'
})
return
}
this.ServiceRunning = true
const response = await resumeJob(row)
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
this.ServiceRunning = false
this.$message({
message: '任务成功恢复。',
type: 'success'
})
this.loadTableData()
},
async handleDelete(row) {
if (row.triggerState !== 'PAUSED') {
this.$message({
message: '请先挂起任务,再删除。',
type: 'warning'
})
return
}
if (this.ServiceRunning) {
this.$message({
message: '请不要重复点击。',
type: 'warning'
})
return
}
this.ServiceRunning = true
const response = await deleteJob(row)
if (response.code !== 100) {
this.ServiceRunning = false
this.$message({
message: response.message,
type: 'warning'
})
return
}
this.ServiceRunning = false
this.$message({
message: '任务成功删除。',
type: 'success'
})
this.loadTableData()
},
indexMethod(index) {
return this.BaseTableData.page.pageSize * (this.BaseTableData.page.currentPage - 1) + index + 1
},
dateTimeColFormatter(row, column, cellValue) {
return this.$commonUtils.dateTimeFormat(cellValue)
},
}
}
</script>
<style>
</style>
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# api定义
job.js定义访问后台接口的方式
import request from '@/utils/request'
//获取空任务
export function getBlankJob() {
return request({
url: '/api/system/auth/job/blank',
method: 'get'
})
}
//获取任务列表(分页)
export function fetchJobPage(data) {
return request({
url: '/api/system/auth/job/page',
method: 'post',
data
})
}
//获取用于修改的任务信息
export function getUpdateObject(data) {
return request({
url: '/api/system/auth/job/dataforupdate',
method: 'post',
data
})
}
//保存任务
export function saveJob(data) {
return request({
url: '/api/system/auth/job/save',
method: 'post',
data
})
}
//暂停任务
export function pauseJob(data) {
return request({
url: '/api/system/auth/job/pause',
method: 'post',
data
})
}
//恢复任务
export function resumeJob(data) {
return request({
url: '/api/system/auth/job/resume',
method: 'post',
data
})
}
//删除任务
export function deleteJob(data) {
return request({
url: '/api/system/auth/job/delete',
method: 'post',
data
})
}
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
# 后端整合
# 配置类
# 单独数据源配置
Quartz会自动创建11张数据表,数据源可以与系统主数据源相同,也可以独立设置。 笔者建议单独设置Quartz数据源。在配置文件 application.yml 添加以下内容
base2048:
job:
enable: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/base2048job?useSSL=false&serverTimezone=UTC&autoReconnect=true&allowPublicKeyRetrieval=true&useOldAliasMetadataBehavior=true
username: root
password: mysql
2
3
4
5
6
7
8
数据源配置类如下:
@Configuration
public class QuartzDataSourceConfig {
@Primary
@Bean(name = "defaultDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Bean(name = "quartzDataSource")
@QuartzDataSource
@ConfigurationProperties(prefix = "base2048.job.datasource")
public DruidDataSource quartzDataSource() {
return new DruidDataSource();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 调度器配置
在 resources 下添加 quartz.properties 文件,内容如下:
# 固定前缀org.quartz
# 主要分为scheduler、threadPool、jobStore、plugin等部分
#
#
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
<!-- 每个集群节点要有独立的instanceId -->
org.quartz.scheduler.instanceId = 'AUTO'
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# threadCount和threadPriority将以setter的形式注入ThreadPool实例
# 并发个数
org.quartz.threadPool.threadCount = 15
# 优先级
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.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = qzDS
org.quartz.dataSource.qzDS.maxConnections = 10
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
调度器配置类内容如下:
@Configuration
public class SchedulerConfig {
@Autowired
private MyJobFactory myJobFactory;
@Value("${base2048.job.enable:false}")
private Boolean JOB_LOCAL_RUNING;
@Value("${base2048.job.datasource.driver-class-name}")
private String dsDriver;
@Value("${base2048.job.datasource.url}")
private String dsUrl;
@Value("${base2048.job.datasource.username}")
private String dsUser;
@Value("${base2048.job.datasource.password}")
private String dsPassword;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setOverwriteExistingJobs(true);
// 延时启动
factory.setStartupDelay(20);
// 用于quartz集群,QuartzScheduler 启动时更新己存在的Job
// factory.setOverwriteExistingJobs(true);
// 加载quartz数据源配置
factory.setQuartzProperties(quartzProperties());
// 自定义Job Factory,用于Spring注入
factory.setJobFactory(myJobFactory);
// 在com.neusoft.jn.gpbase.quartz.job.BaseJobTemplate 同样出现该配置
//原因 : qrtz 在集群模式下 存在 同一个任务 一个在A服务器任务被分配出去 另一个B服务器任务不再分配的情况.
//
if(!JOB_LOCAL_RUNING){
// 设置调度器自动运行
factory.setAutoStartup(false);
}
return factory;
}
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
Properties properties = propertiesFactoryBean.getObject();
properties.setProperty("org.quartz.dataSource.qzDS.driver",dsDriver);
properties.setProperty("org.quartz.dataSource.qzDS.URL",dsUrl);
properties.setProperty("org.quartz.dataSource.qzDS.user",dsUser);
properties.setProperty("org.quartz.dataSource.qzDS.password",dsPassword);
return properties;
}
/*
* 通过SchedulerFactoryBean获取Scheduler的实例
*/
@Bean(name="scheduler")
public Scheduler scheduler() throws Exception {
return schedulerFactoryBean().getScheduler();
}
}
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
# 任务模板
# Job基类
public abstract class BaseJob implements Job, Serializable {
private static final String JOB_MAP_KEY = "self";
public static final String STATUS_RUNNING = "1";
public static final String STATUS_NOT_RUNNING = "0";
public static final String CONCURRENT_IS = "1";
public static final String CONCURRENT_NOT = "0";
/**
* 任务名称
*/
private String jobName;
/**
* 任务分组
*/
private String jobGroup;
/**
* 任务状态 是否启动任务
*/
private String jobStatus;
/**
* cron表达式
*/
private String cronExpression;
/**
* 描述
*/
private String description;
/**
* 任务执行时调用哪个类的方法 包名+类名
*/
private Class beanClass = this.getClass();
/**
* 任务是否有状态
*/
private String isConcurrent;
/**
* Spring bean
*/
private String springBean;
/**
* 任务调用的方法名
*/
private String methodName;
/**
* 为了将执行后的任务持久化到数据库中
*/
@JsonIgnore
private JobDataMap dataMap = new JobDataMap();
public JobKey getJobKey(){
return JobKey.jobKey(jobName, jobGroup);// 任务名称和组构成任务key
}
public JobDataMap getDataMap(){
if(dataMap.size() == 0){
dataMap.put(JOB_MAP_KEY,this);
}
return dataMap;
}
public String getJobName() {
return jobName;
}
public void setJobName(String jobName) {
this.jobName = jobName;
}
public String getJobGroup() {
return jobGroup;
}
public void setJobGroup(String jobGroup) {
this.jobGroup = jobGroup;
}
public String getJobStatus() {
return jobStatus;
}
public void setJobStatus(String jobStatus) {
this.jobStatus = jobStatus;
}
public String getCronExpression() {
return cronExpression;
}
public void setCronExpression(String cronExpression) {
this.cronExpression = cronExpression;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Class getBeanClass() {
return beanClass;
}
public void setBeanClass(Class beanClass) {
this.beanClass = beanClass;
}
public String getIsConcurrent() {
return isConcurrent;
}
public void setIsConcurrent(String isConcurrent) {
this.isConcurrent = isConcurrent;
}
public String getSpringBean() {
return springBean;
}
public void setSpringBean(String springBean) {
this.springBean = springBean;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
}
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
# Job模板类
@Slf4j
public abstract class BaseJobTemplate extends BaseJob {
@Value("${base2048.job.enable:false}")
private Boolean JOB_LOCAL_RUNING;
@Override
public final void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
if (JOB_LOCAL_RUNING) {
try {
this.runing(jobExecutionContext);
} catch (Exception ex) {
throw new JobExecutionException(ex);
}
} else {
log.info("配置参数不允许在本机执行定时任务");
}
}
public abstract void runing(JobExecutionContext jobExecutionContext);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Job示例类
业务Job从模板类继承。
@Slf4j
@Component
@DisallowConcurrentExecution
public class TestJob extends BaseJobTemplate {
@Override
public void runing(JobExecutionContext jobExecutionContext) {
try {
log.info("测试任务开始:【{}】", Instant.now().atOffset(ZoneOffset.ofHours(8)));
System.out.println("============= 测试任务正在运行 =====================");
System.out.println("============= Test job is running ===============");
log.info("测试任务结束:【{}】", Instant.now().atOffset(ZoneOffset.ofHours(8)));
} catch (Exception ex) {
log.error("测试任务异常:【{}】", Instant.now().atOffset(ZoneOffset.ofHours(8)));
log.error(ex.getMessage(), ex);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 管理功能
# Controller
@RestController
@RequestMapping("/api/system/auth/job")
@Slf4j
public class QuartzJobController {
@Resource
private QuartzService quartzService;
@PostMapping("/save")
@ApiOperation(value = "保存添加或修改任务",notes = "保存添加或修改任务")
public ResultDTO addOrUpdate(@RequestBody JobUpdateDTO jobUpdateDTO) throws Exception {
if (StringUtils.isBlank(jobUpdateDTO.getOldJobName())) {
ResultDTO resultDTO = this.addSave(jobUpdateDTO);
return resultDTO;
} else {
/**
* 先删除后添加
*/
JobDTO jobDTO = new JobDTO();
jobDTO.setJobName(jobUpdateDTO.getOldJobName());
jobDTO.setJobGroup(jobUpdateDTO.getOldJobGroup());
this.delete(jobDTO);
ResultDTO resultDTO = this.addSave(jobUpdateDTO);
return resultDTO;
}
}
private ResultDTO addSave(@RequestBody JobUpdateDTO jobUpdateDTO) throws Exception {
BaseJob job = (BaseJob) Class.forName(jobUpdateDTO.getJobClassName()).newInstance();
job.setJobName(jobUpdateDTO.getJobName());
job.setJobGroup(jobUpdateDTO.getJobGroup());
job.setDescription(jobUpdateDTO.getDescription());
job.setCronExpression(jobUpdateDTO.getCronExpression());
try {
quartzService.addJob(job);
return ResultDTO.success();
}catch (Exception ex){
log.error(ex.getMessage(),ex);
return ResultDTO.failureCustom("保存添加任务时服务发生意外情况。");
}
}
@PostMapping("/page")
@ApiOperation(value = "查询任务",notes = "查询任务")
public ResultDTO getJobPage(@RequestBody BasePageQueryVO basePageQueryVO) {
try {
IPage<JobDTO> jobDtoPage = quartzService.queryJob(basePageQueryVO.getCurrentPage(),basePageQueryVO.getPageSize());
return ResultDTO.success(jobDtoPage);
}catch (Exception ex){
log.error(ex.getMessage(),ex);
return ResultDTO.failureCustom("查询任务时服务发生意外情况。");
}
}
@PostMapping("/pause")
@ApiOperation(value = "暂停任务",notes = "暂停任务")
public ResultDTO pause(@RequestBody JobDTO jobDTO) {
try {
quartzService.pauseJob(jobDTO.getJobName(),jobDTO.getJobGroup());
return ResultDTO.success();
}catch (Exception ex){
log.error(ex.getMessage(),ex);
return ResultDTO.failureCustom("暂停任务时服务发生意外情况。");
}
}
@PostMapping("/resume")
@ApiOperation(value = "恢复任务",notes = "恢复任务")
public ResultDTO resume(@RequestBody JobDTO jobDTO) {
try {
quartzService.resumeJob(jobDTO.getJobName(),jobDTO.getJobGroup());
return ResultDTO.success();
}catch (Exception ex){
log.error(ex.getMessage(),ex);
return ResultDTO.failureCustom("恢复任务时服务发生意外情况。");
}
}
@PostMapping("/delete")
@ApiOperation(value = "删除任务",notes = "删除任务")
public ResultDTO delete(@RequestBody JobDTO jobDTO) {
try {
if(quartzService.deleteJob(jobDTO.getJobName(),jobDTO.getJobGroup())) {
return ResultDTO.failureCustom("删除失败。");
}else{
return ResultDTO.success();
}
}catch (Exception ex){
log.error(ex.getMessage(),ex);
return ResultDTO.failureCustom("删除任务时服务发生意外情况。");
}
}
@GetMapping("/blank")
public ResultDTO getBlankJobDTO(){
JobUpdateDTO jobUpdateDTO = new JobUpdateDTO();
jobUpdateDTO.setJobClassName("com.qiyuan.base2048.quartz.job.jobs.");
jobUpdateDTO.setCronExpression("*/9 * * * * ?");
return ResultDTO.success(jobUpdateDTO);
}
@PostMapping("/dataforupdate")
public ResultDTO getUpdateJobDTO(@RequestBody JobDTO jobDTO){
JobUpdateDTO jobUpdateDTO = JobDtoTransMapper.INSTANCE.map(jobDTO);
jobUpdateDTO.setOldJobName(jobDTO.getJobName());
jobUpdateDTO.setOldJobGroup(jobDTO.getJobGroup());
return ResultDTO.success(jobUpdateDTO);
}
}
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
# JobDTO
@Data
public class JobDTO {
private String jobClassName;
private String jobName;
private String jobGroup;
private String description;
private String cronExpression;
private String triggerName;
private String triggerGroup;
private String timeZoneId;
private String triggerState;
private Date startTime;
private Date nextFireTime;
private Date previousFireTime;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# JobUpdateDTO
@Data
public class JobUpdateDTO extends JobDTO{
private String oldJobName;
private String oldJobGroup;
}
2
3
4
5
# Service
@Service
@Slf4j
public class QuartzServiceImpl implements QuartzService {
/**
* Scheduler代表一个调度容器,一个调度容器可以注册多个JobDetail和Trigger.当Trigger和JobDetail组合,就可以被Scheduler容器调度了
*/
@Autowired
private Scheduler scheduler;
@Resource
private QrtzJobDetailsMapper qrtzJobDetailsMapper;
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Autowired
public QuartzServiceImpl(Scheduler scheduler){
this.scheduler = scheduler;
}
@Override
public IPage<JobDTO> queryJob(int pageNum, int pageSize) throws Exception{
List<JobDTO> jobList = null;
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
jobList = new ArrayList<>();
for (JobKey jobKey : jobKeys) {
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
for (Trigger trigger : triggers) {
JobDTO jobDetails = new JobDTO();
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
jobDetails.setCronExpression(cronTrigger.getCronExpression());
jobDetails.setTimeZoneId(cronTrigger.getTimeZone().getDisplayName());
}
jobDetails.setTriggerGroup(trigger.getKey().getName());
jobDetails.setTriggerName(trigger.getKey().getGroup());
jobDetails.setJobGroup(jobKey.getGroup());
jobDetails.setJobName(jobKey.getName());
jobDetails.setStartTime(trigger.getStartTime());
jobDetails.setJobClassName(scheduler.getJobDetail(jobKey).getJobClass().getName());
jobDetails.setNextFireTime(trigger.getNextFireTime());
jobDetails.setPreviousFireTime(trigger.getPreviousFireTime());
jobDetails.setTriggerState(scheduler.getTriggerState(trigger.getKey()).name());
jobList.add(jobDetails);
}
}
} catch (SchedulerException e) {
e.printStackTrace();
}
IPage<JobDTO> jobDTOPage = new Page<>(pageNum,pageSize);
jobDTOPage.setRecords(jobList);
jobDTOPage.setTotal(jobList.size());
jobDTOPage.setCurrent(1);
jobDTOPage.setPages(1);
jobDTOPage.setSize(jobList.size());
return jobDTOPage;
}
/**
* 添加一个任务
* @param job
* @throws SchedulerException
*/
@Override
public void addJob(BaseJob job) throws SchedulerException {
/** 创建JobDetail实例,绑定Job实现类
* JobDetail 表示一个具体的可执行的调度程序,job是这个可执行调度程序所要执行的内容
* 另外JobDetail还包含了这个任务调度的方案和策略**/
// 指明job的名称,所在组的名称,以及绑定job类
JobDetail jobDetail = JobBuilder.newJob(job.getBeanClass())
.withIdentity(job.getJobKey())
.withDescription(job.getDescription())
.usingJobData(job.getDataMap())
.build();
/**
* Trigger代表一个调度参数的配置,什么时候去调度
*/
//定义调度触发规则, 使用cronTrigger规则
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(job.getJobName(),job.getJobGroup())
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
.startNow()
.build();
//将任务和触发器注册到任务调度中去
scheduler.scheduleJob(jobDetail,trigger);
//判断调度器是否启动
if(!scheduler.isStarted()){
scheduler.start();
}
log.info(String.format("定时任务:%s.%s-已添加到调度器!", job.getJobGroup(),job.getJobName()));
}
/**
* 根据任务名和任务组名来暂停一个任务
* @param jobName
* @param jobGroupName
* @throws SchedulerException
*/
@Override
public void pauseJob(String jobName,String jobGroupName) throws SchedulerException {
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroupName));
}
/**
* 根据任务名和任务组名来恢复一个任务
* @param jobName
* @param jobGroupName
* @throws SchedulerException
*/
@Override
public void resumeJob(String jobName,String jobGroupName) throws SchedulerException {
scheduler.resumeJob(JobKey.jobKey(jobName,jobGroupName));
}
public void rescheduleJob(String jobName,String jobGroupName,String cronExpression,String description) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withDescription(description).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
}
/**
* 根据任务名和任务组名来删除一个任务
* @param jobName
* @param jobGroupName
* @throws SchedulerException
*/
@Override
public boolean deleteJob(String jobName,String jobGroupName) throws SchedulerException {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName,jobGroupName);
scheduler.pauseTrigger(triggerKey); //先暂停
scheduler.unscheduleJob(triggerKey); //取消调度
boolean flag = scheduler.deleteJob(JobKey.jobKey(jobName,jobGroupName));
return flag;
}
private JobDTO createJob(String jobName, String jobGroup, Scheduler scheduler, Trigger trigger)
throws SchedulerException {
JobDTO job = new JobDTO();
job.setJobName(jobName);
job.setJobGroup(jobGroup);
job.setDescription("触发器:" + trigger.getKey());
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
job.setTriggerState(triggerState.name());
if(trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger)trigger;
String cronExpression = cronTrigger.getCronExpression();
job.setCronExpression(cronExpression);
}
return job;
}
}
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
至此,烤串完毕,火侯正好,外酥里嫩! 项目源码仓库github (opens new window) 项目源码仓库gitee (opens new window)