简介
Quartz是一个功能丰富的开源作业调度库,几乎可以集成到任何Java应用程序中——从最小的独立应用程序到最大的电子商务系统。Quartz可以设置任务执行的时间和次数来执行数十、数百甚至数万个工作。
配置与使用
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
添加配置
spring:
quartz:
#quartz相关属性配置
properties:
org:
quartz:
scheduler:
#调度标识名 集群中每一个实例都必须使用相同的名称 (区分特定的调度器实例)
instanceName: DefaultQuartzScheduler
#ID设置为自动获取 每一个必须不同 (所有调度器实例中是唯一的)
instanceId: AUTO
jobStore:
#数据保存方式为持久化
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#表的前缀
tablePrefix: QRTZ_
#加入集群 true 为集群 false不是集群
isClustered: false
#调度实例失效的检查时间间隔
clusterCheckinInterval: 10000
#设置为TRUE不会出现序列化非字符串类到 BLOB 时产生的类版本问题
useProperties: true
threadPool:
#ThreadPool 实现的类名
class: org.quartz.simpl.SimpleThreadPool
#线程数量
#threadCount: 10
#线程优先级,属性的最大值是常量 java.lang.Thread.MAX_PRIORITY,等于10。最小值为常量 java.lang.Thread.MIN_PRIORITY,为1
threadPriority: 5
#自创建父线程
threadsInheritContextClassLoaderOfInitializingThread: true
#数据库方式
job-store-type: JDBC
#初始化表结构
jdbc:
initialize-schema: NEVER
实体类
@Data
public class QuartzJob {
@Excel(name = "任务名称")
private String jobName;
@Excel(name = "任务分组")
private String jobGroup;
@Excel(name = "任务描述")
private String description;
@Excel(name = "执行类")
private String jobClassName;
@Excel(name = "执行时间")
private String cronExpression;
private String triggerName;
@Excel(name = "任务状态")
private String triggerState;
private String oldJobName;//任务名称 用于修改
private String oldJobGroup;//任务分组 用于修改
private List<Map<String, Object>> jobDataParam;
public QuartzJob() {
super();
}
public QuartzJob(String jobName, String jobGroup, String description, String jobClassName, String cronExpression, String triggerName) {
super();
this.jobName = jobName;
this.jobGroup = jobGroup;
this.description = description;
this.jobClassName = jobClassName;
this.cronExpression = cronExpression;
this.triggerName = triggerName;
}
}
xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="tech.niua.quartz.mapper.JobMapper">
<select id="listJob" resultType="tech.niua.quartz.domain.QuartzJob">
SELECT
job.JOB_NAME as jobName,job.JOB_GROUP as jobGroup,job.DESCRIPTION as description,job.JOB_CLASS_NAME as jobClassName,
cron.CRON_EXPRESSION as cronExpression,tri.TRIGGER_NAME as triggerName,tri.TRIGGER_STATE as triggerState,
job.JOB_NAME as oldJobName,job.JOB_GROUP as oldJobGroup
FROM qrtz_job_details AS job LEFT JOIN qrtz_triggers AS tri ON job.JOB_NAME = tri.JOB_NAME
LEFT JOIN qrtz_cron_triggers AS cron ON cron.TRIGGER_NAME = tri.TRIGGER_NAME
WHERE tri.TRIGGER_TYPE = 'CRON'
<if test="jobName != null and jobName != '' ">
AND job.JOB_NAME like concat('%',#{jobName},'%')
</if>
</select>
</mapper>
Mapper
@Mapper
public interface JobMapper {
IPage<QuartzJob> listJob(@Param("jobName") String jobName, IPage<QuartzJob> page);
}
Service
public interface IJobService {
IPage<QuartzJob> listQuartzJob(String jobName, IPage<QuartzJob> page);
/**
* 新增job
* @param quartz
* @return
*/
ResultJson saveJob(QuartzJob quartz);
/**
* 触发job
* @param jobName
* @param jobGroup
* @return
*/
ResultJson triggerJob(String jobName, String jobGroup);
/**
* 暂停job
* @param jobName
* @param jobGroup
* @return
*/
ResultJson pauseJob(String jobName, String jobGroup);
/**
* 恢复job
* @param jobName
* @param jobGroup
* @return
*/
ResultJson resumeJob(String jobName, String jobGroup);
/**
* 移除job
* @param jobName
* @param jobGroup
* @return
*/
ResultJson removeJob(String jobName, String jobGroup);
}
ServiceImpl
@Service
public class JobServiceImpl implements IJobService {
private static final String TRIGGER_IDENTITY = "trigger";
private static final String PARAM_NAME = "paramName";
private static final String PARAM_VALUE = "paramValue";
private static final String SCHEDULER_INSTANCE_NAME = "schedulerInstanceName";
@Value("${spring.quartz.properties.org.quartz.scheduler.instanceName}")
// @Value("schedulerInstanceName}")
private String schedulerInstanceName;
@Autowired
private Scheduler scheduler;
@Autowired
private JobMapper jobMapper;
@Override
public IPage<QuartzJob> listQuartzJob(String jobName, IPage<QuartzJob> page) {
IPage<QuartzJob> pageList = jobMapper.listJob(jobName, page);
fillJobData(pageList.getRecords());
return pageList;
}
private void fillJobData(List<QuartzJob> jobList) {
jobList.forEach(job -> {
JobKey key = new JobKey(job.getJobName(), job.getJobGroup());
try {
JobDetail jobDetail = scheduler.getJobDetail(key);
JobDataMap jobDataMap = jobDetail.getJobDataMap();
List<Map<String,Object>> jobDataParam = new ArrayList<>();
jobDataMap.forEach((k,v) -> {
Map<String, Object> jobData = new LinkedHashMap<>(2);
jobData.put(PARAM_NAME,k);
jobData.put(PARAM_VALUE,v);
jobDataParam.add(jobData);
});
job.setJobDataParam(jobDataParam);
} catch (SchedulerException e) {
e.printStackTrace();
}
});
}
@Override
public ResultJson saveJob(QuartzJob quartz){
try {
//如果是修改 展示旧的 任务
if(quartz.getOldJobGroup() != null && !"".equals(quartz.getOldJobGroup())){
JobKey key = new JobKey(quartz.getOldJobName(),quartz.getOldJobGroup());
scheduler.deleteJob(key);
}
//构建job信息
Class cls = Class.forName(quartz.getJobClassName()) ;
cls.newInstance();
JobDetail job = JobBuilder.newJob(cls).withIdentity(quartz.getJobName(),
quartz.getJobGroup())
.withDescription(quartz.getDescription()).build();
putDataMap(job, quartz);
// 触发时间点
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(quartz.getCronExpression().trim());
Trigger trigger = TriggerBuilder.newTrigger().withIdentity(TRIGGER_IDENTITY + quartz.getJobName(), quartz.getJobGroup())
.startNow().withSchedule(cronScheduleBuilder).build();
//交由Scheduler安排触发
scheduler.scheduleJob(job, trigger);
} catch (Exception e) {
e.printStackTrace();
return ResultJson.failure(ResultCode.SERVER_ERROR);
}
return ResultJson.ok();
}
@Override
public ResultJson triggerJob(String jobName, String jobGroup) {
JobKey key = new JobKey(jobName,jobGroup);
try {
scheduler.triggerJob(key);
} catch (SchedulerException e) {
e.printStackTrace();
return ResultJson.failure(ResultCode.SERVER_ERROR);
}
return ResultJson.ok();
}
@Override
public ResultJson pauseJob(String jobName, String jobGroup) {
JobKey key = new JobKey(jobName,jobGroup);
try {
scheduler.pauseJob(key);
} catch (SchedulerException e) {
e.printStackTrace();
return ResultJson.failure(ResultCode.SERVER_ERROR);
}
return ResultJson.ok();
}
@Override
public ResultJson resumeJob(String jobName, String jobGroup) {
JobKey key = new JobKey(jobName,jobGroup);
try {
scheduler.resumeJob(key);
} catch (SchedulerException e) {
e.printStackTrace();
return ResultJson.failure(ResultCode.SERVER_ERROR);
}
return ResultJson.ok();
}
@Override
public ResultJson removeJob(String jobName, String jobGroup) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(TRIGGER_IDENTITY + jobName, jobGroup);
// 停止触发器
scheduler.pauseTrigger(triggerKey);
// 移除触发器
scheduler.unscheduleJob(triggerKey);
// 删除任务
scheduler.deleteJob(JobKey.jobKey(jobName, jobGroup));
System.out.println("removeJob:"+JobKey.jobKey(jobName));
} catch (Exception e) {
e.printStackTrace();
return ResultJson.failure(ResultCode.SERVER_ERROR);
}
return ResultJson.ok();
}
private void putDataMap(JobDetail job, QuartzJob quartz) {
// 将scheduler instanceName存入jobDataMap,方便业务job中进行数据库操作
JobDataMap jobDataMap = job.getJobDataMap();
jobDataMap.put(SCHEDULER_INSTANCE_NAME, schedulerInstanceName);
List<Map<String, Object>> jobDataParam = quartz.getJobDataParam();
if (jobDataParam == null || jobDataParam.isEmpty()) {
return;
}
jobDataParam.forEach(jobData -> jobDataMap.put(String.valueOf(jobData.get(PARAM_NAME)), jobData.get(PARAM_VALUE)));
}
}
RestController
@RestController
@Api(value = "获取权限角色")
@ApiVersion(1)
@RequestMapping("/{version}/quartz")
public class JobController {
private final static Logger LOGGER = LoggerFactory.getLogger(JobController.class);
@Autowired
private IJobService jobService;
@PreAuthorize("hasAuthority('/quartz')")
@ApiImplicitParams({@ApiImplicitParam(name = "Authorization", value = "Authorization token", required = true, dataType = "string", paramType = "header")})
@PostMapping("/list/{currentPage}/{pageSize}")
public ResultJson list(@RequestBody QuartzJob quartz, @PathVariable Integer currentPage, @PathVariable Integer pageSize) {
LOGGER.info("任务列表");
IPage<QuartzJob> pageInfo = jobService.listQuartzJob(quartz.getJobName(), new Page<>(currentPage, pageSize));
return ResultJson.ok(pageInfo);
}
@PreAuthorize("hasAuthority('/quartz/add')")
@ApiImplicitParams({@ApiImplicitParam(name = "Authorization", value = "Authorization token", required = true, dataType = "string", paramType = "header")})
@PostMapping("/add")
public ResultJson save(@RequestBody QuartzJob quartz) {
LOGGER.info("新增任务");
ResultJson result = jobService.saveJob(quartz);
return result;
}
@PreAuthorize("hasAuthority('/quartz/trigger')")
@ApiImplicitParams({@ApiImplicitParam(name = "Authorization", value = "Authorization token", required = true, dataType = "string", paramType = "header")})
@PostMapping("/trigger")
public ResultJson trigger(@RequestBody QuartzJob quartz) {
LOGGER.info("触发任务");
ResultJson result = jobService.triggerJob(quartz.getJobName(), quartz.getJobGroup());
return result;
}
@PreAuthorize("hasAuthority('/quartz/pause')")
@ApiImplicitParams({@ApiImplicitParam(name = "Authorization", value = "Authorization token", required = true, dataType = "string", paramType = "header")})
@PostMapping("/pause")
public ResultJson pause(@RequestBody QuartzJob quartz) {
LOGGER.info("停止任务");
ResultJson result = jobService.pauseJob(quartz.getJobName(), quartz.getJobGroup());
return result;
}
@PreAuthorize("hasAuthority('/quartz/resume')")
@ApiImplicitParams({@ApiImplicitParam(name = "Authorization", value = "Authorization token", required = true, dataType = "string", paramType = "header")})
@PostMapping("/resume")
public ResultJson resume(@RequestBody QuartzJob quartz) {
LOGGER.info("恢复任务");
ResultJson result = jobService.resumeJob(quartz.getJobName(), quartz.getJobGroup());
return result;
}
@PreAuthorize("hasAuthority('/quartz/delete')")
@ApiImplicitParams({@ApiImplicitParam(name = "Authorization", value = "Authorization token", required = true, dataType = "string", paramType = "header")})
@PostMapping("/delete")
public ResultJson remove(@RequestBody QuartzJob quartz) {
LOGGER.info("移除任务");
ResultJson result = jobService.removeJob(quartz.getJobName(), quartz.getJobGroup());
return result;
}
}
Job
/**
* 测试任务
*/
public class TestJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("正在执行中.......");
}
}
前端页面(Vue)
<template>
<div>
<el-row>
<el-form :inline="true" :model="searchForm" ref="searchForm" class="demo-form-inline" >
<el-form-item label="任务名称" prop="jobName">
<el-input v-model.trim="searchForm.jobName" placeholder="">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">查 询</el-button>
<el-button @click="resetSearchForm">重 置</el-button>
</el-form-item>
</el-form>
</el-row>
<!-- 添加和删除按钮区域 -->
<el-row>
<el-button type="primary" @click="addHandleClick" v-auth="['/quartz/add']">添加</el-button>
<!-- <el-button type="primary" @click="exportExcel" v-auth="['/quartz/export']">-->
<!-- 导出数据-->
<!-- </el-button>-->
<p />
</el-row>
<!-- 数据列表表单区域 -->
<el-table :data="tableData" border style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"> </el-table-column>
<th width="40"></th>
<el-table-column
prop="jobName"
label="任务名称"
></el-table-column>
<th width="40"></th>
<el-table-column
prop="jobGroup"
label="任务分组"
></el-table-column>
<th width="40"></th>
<el-table-column
prop="description"
label="描述"
></el-table-column>
<th width="40"></th>
<el-table-column
prop="jobClassName"
label="执行类"
></el-table-column>
<th width="40"></th>
<el-table-column
prop="triggerState"
label="状态"
:formatter="triggerStateFilter"
></el-table-column>
<th width="40"></th>
<el-table-column
prop="cronExpression"
label="执行时间"
></el-table-column>
<th width="40"></th>
<el-table-column fixed="right" label="操作" width="220">
<template slot-scope="scope">
<el-button @click="triggerHandleClick(scope.row)" type="primary" icon="el-icon-video-play" title="执行一次" circle v-auth="['/quartz/trigger']" size="mini"></el-button>
<el-button @click="resumeHandleClick(scope.row)" type="success" icon="el-icon-refresh" title="恢复任务" circle v-auth="['/quartz/resume']" size="mini"></el-button>
<el-button @click="stopHandleClick(scope.row)" type="warning" icon="el-icon-video-pause" title="停止" circle v-auth="['/quartz/pause']" size="mini"></el-button>
<el-button @click="deleteHandleClick(scope.row)" type="danger" icon="el-icon-delete" v-auth="['/quartz/delete']" circle size="mini"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 列表分页操作区域 -->
<el-pagination :current-page="currentPage" :page-size="pageSize" @current-change="handleCurrentChange" background
layout="prev, pager, next" :total="totalCount">
</el-pagination>
<!-- 编辑和添加页面定义 -->
<el-dialog :title="isEditor ? '编辑' : '添加'" :visible.sync="dialogAddFormVisible">
<el-form :model="dialogForm" :rules="rules" ref="dialogForm">
<el-input type="hidden" v-model="dialogForm.id" autocomplete="off"></el-input>
<el-form-item prop="jobName" label="任务名称" :label-width="dialogFormLabelWidth">
<el-input v-model="dialogForm.jobName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="jobGroup" label="任务分组" :label-width="dialogFormLabelWidth">
<el-input v-model="dialogForm.jobGroup" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="description" label="任务描述" :label-width="dialogFormLabelWidth">
<el-input v-model="dialogForm.description" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="jobClassName" label="执行类" :label-width="dialogFormLabelWidth">
<el-input v-model="dialogForm.jobClassName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="cronExpression" label="执行时间" :label-width="dialogFormLabelWidth">
<el-popover v-model="cronPopover">
<cron @change="changeCron" @close="cronPopover=false" i18n="cn"></cron>
<el-input slot="reference" @click="cronPopover=true" v-model="dialogForm.cronExpression" placeholder="请输入定时策略"></el-input>
</el-popover>
<!-- <el-button @click="addJobData()">添加job参数</el-button>-->
</el-form-item>
<!-- <el-form-item v-for="(item, index) in dialogForm.jobDataParam" :key="index">-->
<!-- <input v-model="item.paramName" placeholder="参数名"/>-->
<!-- <input v-model="item.paramValue" placeholder="参数值" />-->
<!-- <button @click="delJobParam(index)" icon="close">移除</button>-->
<!-- </el-form-item>-->
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="resetDialogFormData('dialogForm')" v-if="!isEditor">重 置</el-button>
<el-button @click="dialogAddFormVisible = false" v-if="isEditor">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import moment from "moment";
import {cron} from 'vue-cron';
import { BASE_URL } from "../../utils/api.js";
export default {
components: { cron },
data() {
return {
cronPopover:false,
searchDate:"",
//数据分页
currentPage: 1,//当前页码
totalCount: 1,// 总条数,根据接口获取数据长度(注意:这里不能为空)
pageSize: 10,//页面数据显示数量
//数据列表表单
tableData: [],//数据列表表单的数据域
tableChecked:[],//批量删除时被选中行
ids:[],//批量删除的id
//数据更新
updateUserId: 0,//编辑时要更新的用户id
//搜索
searchForm: {
},
isEditor: false,//标志弹出框是编辑还是添加
dialogAddFormVisible: false,//编辑表单弹出框是否显示
dialogForm: {//弹出框的数据域
},
dialogFormLabelWidth: '120px',//弹出表单的输入框的宽度
rules: {//弹出表单的输入规则
jobName:[
{
required: true,
message: "请输入任务名称",
trigger: "blur",
}
],
jobGroup:[
{
required: true,
message: "请输入任务分组",
trigger: "blur",
}
],
description:[
{
required: true,
message: "请输入任务描述",
trigger: "blur",
}
],
jobClassName:[
{
required: true,
message: "请输入执行类",
trigger: "blur",
}
],
// cronExpression:[
// {
// required: true,
// message: "请输入执行时间",
// trigger: "blur",
// }
// ],
},
urls:{
saveOrUpdate: BASE_URL + "/v1"+"/quartz/add",
search: BASE_URL + "/v1"+"/quartz/list",
delete: BASE_URL + "/v1"+"/quartz/delete",
find: BASE_URL + "/v1/quartz/findById",
exportExcel: BASE_URL + "/v1/quartz/export",
pause: BASE_URL + "/v1/quartz/pause",
trigger: BASE_URL + "/v1/quartz/trigger",
resume: BASE_URL + "/v1/quartz/resume",
},
};
},
created: function () {
this.pageList();
},
methods: {
//加载列表数据
pageList: function () {
let url = this.$data.urls.search + "/" + this.currentPage + "/" + this.pageSize;
this.axios({
method: "POST",
url: url,
data: this.$data.searchForm,
}).then((res) => {
let code = res.data.code;
if (code == 200) {
this.$data.tableData = res.data.data.records;
this.$data.totalCount = res.data.data.total;
this.$data.currentPage = res.data.data.current;
}
}).catch((error) => {
console.log(error);
});
},
onSearch: function () {
this.pageList();
},
resetSearchForm: function () {
//重置表单数据
this.searchDate = '';
this.resetDialogFormData('searchForm')
this.pageList();
},
//重置编辑弹出框表单的数据
resetDialogFormData: function(formName){
if (this.$refs[formName] !== undefined) {
this.$refs[formName].resetFields();
}
},
addHandleClick: function() {
this.resetDialogFormData('dialogForm');
this.$data.isEditor = false;
this.$data.dialogAddFormVisible = true;
},
saveOrUpdate: function() {
this.$refs['dialogForm'].validate((valid) => {
if (valid && this.checkDialogForm()) {
this.axios({
method: 'POST',
url: this.$data.urls.saveOrUpdate,
data: this.dialogForm
}).then(res => {
let code = res.data.code
if (code == 200) {
this.pageList();
this.$data.dialogAddFormVisible = false
}else if(code == 20004){
this.$message.error('请先修改数据在更新');
}
}).catch(error => {
console.log(error);
});
} else {
console.log('error submit!!');
return false;
}
});
},
editorHandleClick: function (row) {
this.isEditor = true;
let url = this.$data.urls.find + "/" + row.id;
this.axios({
method: "GET",
url: url,
data: {},
}).then((res) => {
let code = res.data.code;
if (code == 200) {
this.resetDialogFormData('dialogForm');
this.$data.dialogForm = res.data.data;
this.dialogAddFormVisible = true;
}
}).catch((error) => {
console.log(error)
});
},
exportExcel: function () {
let url = this.urls.exportExcel;
this.axios({
method: "GET",
url: url,
data: {},
})
.then((res) => {
let code = res.data.code;
if (code == 200) {
console.log(res.data.data);
window.location.href = process.env.VUE_APP_BASEURL + "/profile/download/" + res.data.data;
}
})
.catch((error) => {
console.log(error);
});
},
deleteHandleClick: function(row) {
let url = this.urls.delete;
this.execHandleClick(url, '是否删除此任务?', row);
},
stopHandleClick: function(row) {
let url = this.urls.pause;
this.execHandleClick(url, '是否停止此任务?', row);
},
triggerHandleClick: function(row) {
let url = this.urls.trigger;
this.execHandleClick(url, '执行此任务一次?', row);
},
resumeHandleClick: function(row) {
let url = this.urls.resume;
this.execHandleClick(url, '是否确定恢复此任务?', row);
},
execHandleClick:function (url, msg, row) {
this.$confirm(msg , '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.axios({
method: "POST",
url: url,
data: {
'jobName': row.jobName,
'jobGroup': row.jobGroup
},
}).then((res) => {
let code = res.data.code;
if (code == 200) {
this.$message.success("执行成功");
this.pageList();
}
}).catch((error) => {
console.log(error);
});
}).catch(() => {
});
},
handleSelectionChange:function(val){
this.tableChecked=val;
},
handleCurrentChange: function(val) {
this.$data.currentPage = val;
this.pageList();
},
checkDialogForm(){
return true;
},
carTimeFilter: function (row, column, cellValue) {
if (cellValue != null) {
return moment(cellValue).format("YYYY-MM-DD HH:mm:ss");
}
},
triggerStateFilter: function (row, column, cellValue) {
let backVal = cellValue
if(cellValue == 'PAUSED'){
backVal = "暂停";
}
if(cellValue == 'ACQUIRED'){
backVal = "正在按照规则运行";
}
return backVal;
},
dateChange: function(){
this.searchForm.createTimeBegin = this.searchDate[0];
this.searchForm.createTimeEnd = this.searchDate[1];
console.log(this.searchForm.createTimeBegin + " | " + this.searchForm.createTimeEnd)
},
changeCron(val){
this.dialogForm.cronExpression=val
},
},
};
</script>
<style>
</style>