因工作需求,需要能修改定时的任务,前端vue3,后端是springboot
看看页面效果:
首先maven加上引入
<dependency><groupId>org.quartz-scheduler</groupId><artifactId>quartz</artifactId><version>2.3.1</version></dependency>
然后yaml加上配置(Quartz就这点好,自动给你建表了)
#服务器配置
server:port: 8080undertow:threads:# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程io: 16# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载worker: 400# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理buffer-size: 1024# 是否分配的直接内存direct-buffers: truespring:datasource:driver-class-name: com.mysql.cj.jdbc.Driver#driver-class-name: org.postgresql.Driver#driver-class-name: oracle.jdbc.OracleDriver#driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriverdruid:# MySql、PostgreSQL、SqlServer、DaMeng校验validation-query: select 1# Oracle、YashanDB校验#oracle: true#validation-query: select 1 from dualvalidation-query-timeout: 2000initial-size: 5max-active: 20min-idle: 5max-wait: 60000test-on-borrow: falsetest-on-return: falsetest-while-idle: truetime-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000stat-view-servlet:enabled: truelogin-username: login-password: web-stat-filter:enabled: trueurl-pattern: /*exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'session-stat-enable: truesession-stat-max-count: 10quartz:# 任务存储类型job-store-type: "jdbc"# 关闭时等待任务完成wait-for-jobs-to-complete-on-shutdown: false# 是否覆盖已有的任务overwrite-existing-jobs: true# 是否自动启动计划程序auto-startup: true# 延迟启动startup-delay: 0sjdbc:# 数据库架构初始化模式(never:从不进行初始化;always:每次都清空数据库进行初始化;embedded:只初始化内存数据库(默认值))initialize-schema: "always"#todo 后续改# 相关属性配置properties:org:quartz:scheduler:# 调度器实例名称instanceName: QuartzScheduler# 分布式节点ID自动生成instanceId: AUTOjobStore:class: org.springframework.scheduling.quartz.LocalDataSourceJobStoredriverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate# 表前缀tablePrefix: QRTZ_# 是否开启集群isClustered: true# 数据源别名(自定义)dataSource: quartz# 分布式节点有效性检查时间间隔(毫秒)clusterCheckinInterval: 10000useProperties: false# 线程池配置threadPool:class: org.quartz.simpl.SimpleThreadPoolthreadCount: 10threadPriority: 5threadsInheritContextClassLoaderOfInitializingThread: true
然后开始正式后台代码:
package org.springblade.etl.source.controller;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.etl.source.entity.JobInfo;
import org.springblade.etl.source.service.QuartzService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/quartz")
public class QuartzController {@Autowiredprivate QuartzService quartzService;//创建任务@PostMapping("/save")public R createJob(@RequestBody JobInfo jobInfo) {return quartzService.addCronJob(jobInfo.getJobName(), jobInfo.getCron(), "org.springblade.etl.source.task.TaskJob").equals("SUCCESS")?R.status(true):R.status(false);//todo 类名写在这}//删除任务@PostMapping("/remove")public R deleteJob(@RequestParam("jobName")String jobName) {return quartzService.deleteCronJob(jobName, null, null, null).equals("SUCCESS")?R.status(true):R.status(false);}//执行一次@PostMapping("/executeImmediately")public String executeImmediately(@RequestBody JobInfo jobInfo) {return quartzService.executeImmediately(jobInfo.getJobName(), "org.springblade.etl.source.task.TaskJob");}//获取任务状态@PostMapping("/detail")public R<JobInfo> getJobStatus(@RequestParam("jobName")String jobName) {return R.data(quartzService.getJobStatus(jobName, null));}//获取所有任务@PostMapping("/list")public R getAllJob() {// 创建分页对象,指定当前页码和每页显示的数量long currentPage = 1; // 当前页码long pageSize = 10; // 每页显示数量Page<JobInfo> page = new Page<>(currentPage, pageSize);page.setRecords(quartzService.getAllJob());return R.data(page);}//修改定时任务时间@PostMapping("/submit")public R updateJob(@RequestBody JobInfo jobInfo) {quartzService.deleteCronJob(jobInfo.getJobName(), jobInfo.getJobGroup(), jobInfo.getTriggerName(), jobInfo.getTriggerGroup());return quartzService.addCronJob(jobInfo.getJobName(), jobInfo.getCron(), "org.springblade.etl.source.task.TaskJob").equals("SUCCESS")?R.status(true):R.status(false);}}
package org.springblade.etl.source.service;import org.springblade.etl.source.entity.JobInfo;import java.util.List;public interface QuartzService {/*** 新增** @param jobName* @param cron* @param jobClassName* @return*/String addCronJob(String jobName, String cron, String jobClassName);/*** 停止** @param jobName* @param jobGroup* @param triggerName* @param triggerGroup* @return*/String deleteCronJob(String jobName, String jobGroup, String triggerName, String triggerGroup);/*** 立即执行,不定时** @param jobName* @param jobClassName* @return*/String executeImmediately(String jobName, String jobClassName);// 暂停// 获取状态JobInfo getJobStatus(String jobName, String jobGroup);List<JobInfo> getAllJob();
}
package org.springblade.etl.source.service.impl;import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.etl.source.entity.JobInfo;
import org.springblade.etl.source.service.QuartzService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
@Slf4j
public class QuartzServiceImpl implements QuartzService {@Autowiredprivate Scheduler scheduler;private static final String DEFAULT_JOB_GROUP = "default_job_group";private static final String DEFAULT_TRIGGER_GROUP = "default_trigger_group";private static final String TRIGGER_PRE = "Trigger_";@Overridepublic String addCronJob(String jobName, String cron, String jobClassName) {try {// 当前任务不存在才进行添加JobKey jobKey = JobKey.jobKey(jobName, DEFAULT_JOB_GROUP);if (scheduler.checkExists(jobKey)) {log.info("[添加定时任务]已存在该作业,jobkey为:{}", jobKey);return "已存在该作业";}// 构建 JobJobDetail job = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobKey).build();// cron表达式定时构造器CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron);// 构建 TriggerTrigger trigger = TriggerBuilder.newTrigger().withIdentity(TriggerKey.triggerKey(TRIGGER_PRE + jobName, DEFAULT_TRIGGER_GROUP))
// .startAt(DateUtil.parseDate(start))
// .endAt(DateUtil.parseDate(end)).withSchedule(cronScheduleBuilder).build();// 启动调度器scheduler.scheduleJob(job, trigger);scheduler.start();return "SUCCESS";} catch (Exception e) {log.error("[新增定时任务]失败,报错:", e);return "FAIL";}}@Overridepublic String deleteCronJob(String jobName, String jobGroup, String triggerName, String triggerGroup) {try {JobKey jobKey = JobKey.jobKey(jobName, jobGroup);//如果triggerName和triggerGroup为空,则使用默认值if (StringUtil.isBlank(triggerName)&&StringUtil.isBlank(triggerGroup)){triggerName = TRIGGER_PRE + jobName;triggerGroup = DEFAULT_TRIGGER_GROUP;}TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroup);Trigger trigger = scheduler.getTrigger(triggerKey);if (null == trigger) {log.info("[停止定时任务]根据triggerName:{}和triggerGroup:{}未查询到相应的trigger!");return "SUCCESS";}//暂停触发器scheduler.pauseTrigger(triggerKey);// 移除触发器scheduler.unscheduleJob(triggerKey);// 删除任务scheduler.deleteJob(jobKey);log.info("[停止定时任务]jobName:{},jobGroup:{}, triggerName:{}, triggerGroup:{},停止--------------", jobName, jobGroup, triggerName, triggerGroup);return "SUCCESS";} catch (SchedulerException e) {log.error("[停止定时任务]失败,报错:", e);return "FAIL";}}public static Job getClass(String className) throws Exception {Class<?> classTemp = Class.forName(className);return (Job) classTemp.newInstance();}@Overridepublic String executeImmediately(String jobName, String jobClassName) {try {JobKey jobKey = JobKey.jobKey(jobName, DEFAULT_JOB_GROUP);JobDetail job = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobKey).build();Trigger trigger = TriggerBuilder.newTrigger().withIdentity(TriggerKey.triggerKey(TRIGGER_PRE + jobName, DEFAULT_TRIGGER_GROUP)).build();// 启动调度器scheduler.scheduleJob(job, trigger);scheduler.start();return "SUCCESS";} catch (Exception e) {log.error("[立即执行一次任务,不定时]失败,报错:", e);return "FAIL";}}@Overridepublic JobInfo getJobStatus(String jobName, String jobGroup) {try {// 当前任务不存在才进行添加JobKey jobKey = JobKey.jobKey(jobName, DEFAULT_JOB_GROUP);// 利用JobKey查找任务是否存在if (scheduler.checkExists(jobKey)) {log.info("查找到该任务,任务名:{}, 状态为:{}", jobName, scheduler.getTriggerState(TriggerKey.triggerKey(TRIGGER_PRE + jobName, DEFAULT_TRIGGER_GROUP)));// 获取Cron触发器,从而获得Cron表达式CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(TriggerKey.triggerKey(TRIGGER_PRE + jobName, DEFAULT_TRIGGER_GROUP));String cronExpression = cronTrigger.getCronExpression();JobInfo jobInfo = new JobInfo();jobInfo.setJobName(jobKey.getName());jobInfo.setJobGroup(jobKey.getGroup());jobInfo.setStatus( scheduler.getTriggerState(TriggerKey.triggerKey(TRIGGER_PRE + jobName, DEFAULT_TRIGGER_GROUP)).toString());jobInfo.setTriggerName(TriggerKey.triggerKey(TRIGGER_PRE + jobName, DEFAULT_TRIGGER_GROUP).getName());jobInfo.setTriggerGroup(DEFAULT_TRIGGER_GROUP);jobInfo.setCron(cronExpression);return jobInfo;} else {throw new RuntimeException("任务不存在");}} catch (SchedulerException e) {log.error("[查询任务状态]失败,报错:", e);throw new RuntimeException("任务不存在");}}@Overridepublic List<JobInfo> getAllJob() {ArrayList<JobInfo> jobInfos = new ArrayList<JobInfo>();try {// 获取所有的触发器组for (String triggerGroup : scheduler.getTriggerGroupNames()) {// 获取指定触发器组下的所有触发器for (TriggerKey triggerKey : scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(triggerGroup))) {Trigger trigger = scheduler.getTrigger(triggerKey);// 判断触发器类型是否为 CronTriggerif (trigger instanceof CronTrigger) {CronTrigger cronTrigger = (CronTrigger) trigger;JobKey jobKey = cronTrigger.getJobKey();JobInfo jobInfo = new JobInfo();jobInfo.setJobName(jobKey.getName());jobInfo.setJobGroup(jobKey.getGroup());jobInfo.setTriggerName(triggerKey.getName());jobInfo.setTriggerGroup(triggerKey.getGroup());//获取一下cron表达式jobInfo.setCron(cronTrigger.getCronExpression());jobInfo.setStatus( scheduler.getTriggerState(TriggerKey.triggerKey(TRIGGER_PRE + jobKey.getName(), DEFAULT_TRIGGER_GROUP)).toString());jobInfos.add(jobInfo);}}}if (jobInfos.size() == 0) {log.info("暂无定时任务");return null;}} catch (SchedulerException e) {log.error("[查询所有定时任务状态]失败,报错:", e);throw new RuntimeException("查询所有定时任务状态失败");}return jobInfos;}}
这个时间转换cron是我的自己的业务需要(因为前段不能让用户输入cron表达式,所以我让用户输入时间即可转换适合的cron表达式)
package org.springblade.common.utils;import org.quartz.CronExpression;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class CronTimeConverter {public static String convertCronToTime(String cronExpression) {try {CronExpression cron = new CronExpression(cronExpression);Date nextExecutionTime = cron.getTimeAfter(new Date());// 转换为指定的时间格式,比如 HH:mm// 这里使用 SimpleDataFormat 进行格式化SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");return sdf.format(nextExecutionTime);} catch (ParseException e) {e.printStackTrace();return ""; // 处理异常情况,返回空字符串或其他默认值}}// public static void main(String[] args) {
// String cronExpression = "0 40 14 * * ?";
// String time = convertCronToTime(cronExpression);
// System.out.println("Time based on cron expression " + cronExpression + " is: " + time);
// }public static String convertTimeToCron(String time) {String[] timeParts = time.split(":");if (timeParts.length != 2) {return ""; // 处理异常情况,返回空字符串或其他默认值}String cronExpression = String.format("0 %s %s * * ?", timeParts[1], timeParts[0]);return cronExpression;}public static void main(String[] args) {String time = "14:40";String cronExpression = convertTimeToCron(time);System.out.println("Cron expression for time " + time + " is: " + cronExpression);}
}
你需要一个entity对象:
package org.springblade.etl.source.entity;import lombok.Data;
import org.springblade.common.utils.CronTimeConverter;
import org.springblade.core.tool.utils.StringUtil;import java.time.*;
import java.time.format.DateTimeFormatter;@Data
public class JobInfo {private String jobName;private String time = "2024-03-19T08:26:58.000Z" ;private String cron = "0 16 10 ? * *";private String jobGroup;private String triggerName;private String triggerGroup;private String status;public void setCron(String cron) {//顺便将时间写入if (StringUtil.isNotBlank(cron)) {this.cron = cron;String time = CronTimeConverter.convertCronToTime(cron);LocalTime localTime = LocalTime.parse(time);// 将当天的本地时间与当天的日期结合,并转换为ZonedDateTime对象ZonedDateTime localDateTime = ZonedDateTime.now().withHour(localTime.getHour()).withMinute(localTime.getMinute());// 将本地时间转换为UTC时间,并进行格式化ZonedDateTime utcDateTime = localDateTime.withZoneSameInstant(ZoneOffset.UTC);DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");this.time = utcDateTime.format(formatter);}}public void setTime(String utcTime){if (StringUtil.isNotBlank(utcTime)) {this.time =utcTime;//顺便将cron表达式写入// 将UTC时间字符串转换为Instant对象Instant instant = Instant.parse(utcTime);// 将Instant对象转换为本地时间ZonedDateTime localTime = instant.atZone(ZoneId.systemDefault());// 格式化本地时间为 "HH:mm" 形式DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");String formattedTime = localTime.format(formatter);String cron = CronTimeConverter.convertTimeToCron(formattedTime);this.cron = cron;}}public static void main(String[] args) {// UTC时间字符串String utcTime = "2024-03-19T08:26:58.000Z";// 将UTC时间字符串转换为Instant对象Instant instant = Instant.parse(utcTime);// 将Instant对象转换为本地时间ZonedDateTime localTime = instant.atZone(ZoneId.systemDefault());// 格式化本地时间为 "HH:mm" 形式DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");String formattedTime = localTime.format(formatter);System.out.println("本地时间(HH:mm):" + formattedTime);System.out.println("UTC时间:" + instant);System.out.println("本地时间:" + formattedTime);}}
下面是前端vue
<template><basic-container><avue-crud :option="option"v-model:search="search"v-model:page="page"v-model="form":table-loading="loading":data="data":permission="permissionList":before-open="beforeOpen"ref="crud"@row-update="rowUpdate"@row-save="rowSave"@row-del="rowDel"@search-change="searchChange"@search-reset="searchReset"@selection-change="selectionChange"@current-change="currentChange"@size-change="sizeChange"@refresh-change="refreshChange"@on-load="onLoad"><template #menu-left><el-button type="primary"icon="el-icon-s-promotion"@click="handleManualTrigger">手动触发</el-button>
<!-- <el-button type="danger"-->
<!-- icon="el-icon-delete"-->
<!-- plain-->
<!-- v-if="permission.jobInfo_delete"-->
<!-- @click="handleDelete">删 除-->
<!-- </el-button>-->
<!-- <el-button type="warning"-->
<!-- plain-->
<!-- icon="el-icon-download"-->
<!-- @click="handleExport">导 出-->
<!-- </el-button>--></template></avue-crud></basic-container>
</template><script>import {getList, getDetail, add, update, remove,startETLForSYNL} from "@/api/source/jobInfo";import option from "@/option/source/jobInfo";import {mapGetters} from "vuex";import {exportBlob} from "@/api/common";import {getToken} from '@/utils/auth';import {downloadXls} from "@/utils/util";import {dateNow} from "@/utils/date";import NProgress from 'nprogress';import 'nprogress/nprogress.css';export default {data() {return {form: {},query: {},search: {},loading: true,page: {pageSize: 10,currentPage: 1,total: 0},selectionList: [],option: option,data: []};},computed: {...mapGetters(["permission"]),permissionList() {return {addBtn: this.validData(this.permission.jobInfo_add, false),viewBtn: this.validData(this.permission.jobInfo_view, false),delBtn: this.validData(this.permission.jobInfo_delete, false),editBtn: this.validData(this.permission.jobInfo_edit, false)};},ids() {let ids = [];this.selectionList.forEach(ele => {ids.push(ele.id);});return ids.join(",");}},methods: {rowSave(row, done, loading) {row.cron = null;//表达式不往后端传add(row).then(() => {this.onLoad(this.page);this.$message({type: "success",message: "操作成功!"});done();}, error => {loading();window.console.log(error);});},rowUpdate(row, index, done, loading) {row.cron = null;表达式不往后端传update(row).then(() => {this.onLoad(this.page);this.$message({type: "success",message: "操作成功!"});done();}, error => {loading();console.log(error);});},rowDel(row) {this.$confirm("确定将选择数据删除?", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {return remove(row.jobName);}).then(() => {this.onLoad(this.page);this.$message({type: "success",message: "操作成功!"});});},handleDelete() {if (this.selectionList.length === 0) {this.$message.warning("请选择至少一条数据");return;}this.$confirm("确定将选择数据删除?", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {return remove(this.jobName);}).then(() => {this.onLoad(this.page);this.$message({type: "success",message: "操作成功!"});this.$refs.crud.toggleSelection();});},handleExport() {let downloadUrl = `/blade-jobInfo/jobInfo/export-jobInfo?${this.website.tokenHeader}=${getToken()}`;const {} = this.query;let values = {};this.$confirm("是否导出数据?", "提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {NProgress.start();exportBlob(downloadUrl, values).then(res => {downloadXls(res.data, `自动任务表${dateNow()}.xlsx`);NProgress.done();})});},beforeOpen(done, type) {if (["edit", "view"].includes(type)) {getDetail(this.form.jobName).then(res => {this.form = res.data.data;});}done();},searchReset() {this.query = {};this.onLoad(this.page);},searchChange(params, done) {this.query = params;this.page.currentPage = 1;this.onLoad(this.page, params);done();},selectionChange(list) {this.selectionList = list;},selectionClear() {this.selectionList = [];this.$refs.crud.toggleSelection();},currentChange(currentPage){this.page.currentPage = currentPage;},sizeChange(pageSize){this.page.pageSize = pageSize;},refreshChange() {this.onLoad(this.page, this.query);},onLoad(page, params = {}) {this.loading = true;const {} = this.query;let values = {};getList(page.currentPage, page.pageSize, values).then(res => {console.log("定时任务得到的数据=",res);const data = res.data.data;this.page.total = data.total;this.data = data.records;this.loading = false;this.selectionClear();});},handleManualTrigger() {this.$confirm("确定手动触发定时任务?", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(() => {this.$message({type: "success",message: "操作成功!"});// 调用 startETLForSYNL 方法startETLForSYNL().then(response => {const result = response.data.data; // 获取返回值console.log("返回值=",response)this.$message({type: "info",message: ` ${result}`});}).catch(error => {this.$message.error("操作失败");});});}}};
</script><style>
</style>
export default {height:'auto',calcHeight: 30,tip: false,searchShow: true,searchMenuSpan: 6,// border: true,// index: true,// viewBtn: true,selection: true,// dialogClickModal: false,column: [{label: "",prop: "id",type: "input",addDisplay: false,editDisplay: false,viewDisplay: false,hide: true,},{label: "任务名称",prop: "jobName",type: "input",},{label: "执行时间",prop: "time",type: "time",format: "HH:mm" // 设置时间格式,例如 "HH:mm"},{label: "定时表达式",prop: "cron",type: "input",editDisplay: false,addDisplay: false,},{label: "任务分组",prop: "jobGroup",type: "input",addDisplay: false,editDisplay: false,hide: true},{label: "触发器名称",prop: "triggerName",type: "input",addDisplay: false,editDisplay: false,hide: true},{label: "触发器分组",prop: "triggerGroup",type: "input",addDisplay: false,editDisplay: false,hide: true},{label: "状态",prop: "status",type: "input",addDisplay: false,editDisplay: false,}]
}
import request from '@/axios';export const getList = (current, size, params) => {return request({url: '/quartz/list',method: 'post',params: {...params,current,size,}})
}export const getDetail = (jobName) => {return request({url: '/quartz/detail',method: 'post',params: {jobName}})
}export const remove = (jobName) => {return request({url: '/quartz/remove',method: 'post',params: {jobName,}})
}export const add = (row) => {return request({url: '/quartz/submit',method: 'post',data: row})
}export const update = (row) => {return request({url: '/quartz/submit',method: 'post',data: row})
}export const startETLForSYNL = () => {return request({url: '/startETLForSYNL',method: 'get'})
}