淄博烧烤爆红出了圈,当你坐在八大局的烧烤摊,面前是火炉、烤串、小饼和蘸料,音乐响起,啤酒倒满,烧烤灵魂的party即将开场的时候,你系统中的Scheduler(调试器),也自动根据设定的Trigger(触发器),从容优雅的启动了一系列的Job(后台定时任务)。工作一切早有安排,又何须费心劳神呢?因为boot-admin早已将Quartz这块肉串在了烤签上!
项目源码仓库github
项目源码仓库gitee

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>
-
-
-
-
- <el-button size="mini" type="primary" @click="search()">查询</el-button>
- <el-button size="mini" type="primary" @click="handleadd()">添加</el-button>
-
-
-
-
- <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"
- />
-
-
-
-
-
-
- <el-table max-height="100%" :data="BaseTableData.table" :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"
- >
-
-
-
- <h3>定时任务管理</h3>
-
-
- <el-button type="text" title="全屏显示" @click="resizeInputBaseInfoDialogMax()"><i /></el-button>
- <el-button type="text" title="以弹出窗口形式显示" @click="resizeInputBaseInfoDialogNormal()"><i /></el-button>
- <el-button type="text" title="关闭" @click="closeInputBaseInfoDialog()"><i /></el-button>
-
-
-
-
- <el-form
- ref="InputBaseInfoForm"
- :status-icon="InputBaseInfoDialogData.statusIcon"
- :model="InputBaseInfoDialogData.data"
-
- >
- <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>
-
-
-
- <el-button type="primary" @click="saveInputBaseInfoForm()">保 存</el-button>
-
-
- </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"
- >
-
-
-
- <h3>修改任务</h3>
-
-
- <el-button type="text" @click="dialogResize('ViewBaseInfoDialog',true)"><i title="全屏显示" /></el-button>
- <el-button type="text" @click="dialogResize('ViewBaseInfoDialog',false)"><i
-
- title="以弹出窗口形式显示"
- /></el-button>
- <el-button type="text" @click="dialogClose('ViewBaseInfoDialog')"><i title="关闭" /></el-button>
-
-
-
-
- <el-form
- ref="ViewBaseInfoForm"
- :status-icon="ViewBaseInfoDialogData.statusIcon"
- :model="ViewBaseInfoDialogData.data"
-
- >
- <el-form-item label="表达式" :label-width="ViewBaseInfoDialogData.formLabelWidth" prop="cronExpression">
- {{ this.BaseTableData.currentRow.cronExpression }}
- </el-form-item>
- </el-form>
-
- </el-dialog>
-
- </template>
复制代码 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、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
- 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
复制代码 调度器配置类内容如下:- @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();
- }
- }
复制代码 任务模板
Job基类
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);
- }
复制代码 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
- @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);
- }
- }
复制代码 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;
- }
复制代码 JobUpdateDTO
- @Data
- public class JobUpdateDTO extends JobDTO{
- private String oldJobName;
- private String oldJobGroup;
- }
复制代码 Service
[code]@Service@Slf4jpublic 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 queryJob(int pageNum, int pageSize) throws Exception{ List jobList = null; try { Scheduler scheduler = schedulerFactoryBean.getScheduler(); GroupMatcher matcher = GroupMatcher.anyJobGroup(); Set jobKeys = scheduler.getJobKeys(matcher); jobList = new ArrayList(); for (JobKey jobKey : jobKeys) { List |