这篇文章主要介绍“Springboot-admin怎么整合Quartz实现动态管理定时任务”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Springboot-admin怎么整合Quartz实现动态管理定时任务”文章能帮助大家解决问题。
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" > <!--功能按钮区--> <div class="cl pd-5 bg-1 bk-gray"> <div align="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 > <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" :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" > <div align="left" > <h4>定时任务管理</h4> </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" > <div align="left" > <h4>修改任务</h4> </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、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
调度器配置类内容如下:
@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()); &