目录
- boot-admin整合Quartz实现动态管理定时任务
- 加入依赖
- 前端整合
- vue页面
- 后端整合配置类单独数据源配置
- 调度器配置
- 任务模板
- Job示例类
- 管理功能
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> |
前端整合
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> |
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 | |
}) | |
} |
后端整合配置类单独数据源配置
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 |
数据源配置类如下:
@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(); | |
} | |
} |
调度器配置
在 resources 下添加 quartz.properties 文件,内容如下:
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' | |
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool | |
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.impl.jdbcjobstore.JobStoreTX | |
org.quartz.jobStore.tablePrefix = QRTZ_ | |
org.quartz.jobStore.dataSource = qzDS | |
org.quartz.dataSource.qzDS.maxConnections = 10 |
调度器配置类内容如下:
public class SchedulerConfig { | |
private MyJobFactory myJobFactory; | |
private Boolean JOB_LOCAL_RUNING; | |
private String dsDriver; | |
private String dsUrl; | |
private String dsUser; | |
private String dsPassword; | |
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; | |
} | |
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的实例 | |
*/ | |
public Scheduler scheduler() throws Exception { | |
return schedulerFactoryBean().getScheduler(); | |
} | |
} |
任务模板
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; | |
/** | |
* 为了将执行后的任务持久化到数据库中 | |
*/ | |
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; | |
} | |
} |
Job模板类
public abstract class BaseJobTemplate extends BaseJob { | |
private Boolean JOB_LOCAL_RUNING; | |
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); | |
} |
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); | |
} | |
} | |
} |
管理功能
Controller
public class QuartzJobController { | |
private QuartzService quartzService; | |
public ResultDTO addOrUpdate( 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( 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("保存添加任务时服务发生意外情况。"); | |
} | |
} | |
public ResultDTO getJobPage( 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("查询任务时服务发生意外情况。"); | |
} | |
} | |
public ResultDTO pause( JobDTO jobDTO) { | |
try { | |
quartzService.pauseJob(jobDTO.getJobName(),jobDTO.getJobGroup()); | |
return ResultDTO.success(); | |
}catch (Exception ex){ | |
log.error(ex.getMessage(),ex); | |
return ResultDTO.failureCustom("暂停任务时服务发生意外情况。"); | |
} | |
} | |
public ResultDTO resume( JobDTO jobDTO) { | |
try { | |
quartzService.resumeJob(jobDTO.getJobName(),jobDTO.getJobGroup()); | |
return ResultDTO.success(); | |
}catch (Exception ex){ | |
log.error(ex.getMessage(),ex); | |
return ResultDTO.failureCustom("恢复任务时服务发生意外情况。"); | |
} | |
} | |
public ResultDTO delete( 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("删除任务时服务发生意外情况。"); | |
} | |
} | |
public ResultDTO getBlankJobDTO(){ | |
JobUpdateDTO jobUpdateDTO = new JobUpdateDTO(); | |
jobUpdateDTO.setJobClassName("com.qiyuan.base2048.quartz.job.jobs."); | |
jobUpdateDTO.setCronExpression("*/9 * * * * ?"); | |
return ResultDTO.success(jobUpdateDTO); | |
} | |
public ResultDTO getUpdateJobDTO( JobDTO jobDTO){ | |
JobUpdateDTO jobUpdateDTO = JobDtoTransMapper.INSTANCE.map(jobDTO); | |
jobUpdateDTO.setOldJobName(jobDTO.getJobName()); | |
jobUpdateDTO.setOldJobGroup(jobDTO.getJobGroup()); | |
return ResultDTO.success(jobUpdateDTO); | |
} | |
} |
JobDTO
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; | |
} |
JobUpdateDTO
public class JobUpdateDTO extends JobDTO{ | |
private String oldJobName; | |
private String oldJobGroup; | |
} |
Service
public class QuartzServiceImpl implements QuartzService { | |
/** | |
* Scheduler代表一个调度容器,一个调度容器可以注册多个JobDetail和Trigger.当Trigger和JobDetail组合,就可以被Scheduler容器调度了 | |
*/ | |
private Scheduler scheduler; | |
private QrtzJobDetailsMapper qrtzJobDetailsMapper; | |
private SchedulerFactoryBean schedulerFactoryBean; | |
public QuartzServiceImpl(Scheduler scheduler){ | |
this.scheduler = scheduler; | |
} | |
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 | |
*/ | |
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 | |
*/ | |
public void pauseJob(String jobName,String jobGroupName) throws SchedulerException { | |
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroupName)); | |
} | |
/** | |
* 根据任务名和任务组名来恢复一个任务 | |
* @param jobName | |
* @param jobGroupName | |
* @throws SchedulerException | |
*/ | |
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 | |
*/ | |
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; | |
} | |
} |
至此,烤串完毕,火侯正好,外酥里嫩!