一小时搞定一个可视化定时任务在线管理系统(基于Spring + Vue)
前端时间看到的一篇博文中的一个很有意思的可视化项目管理系统,借这时间根据自己的理解敲了一下,下面分享给大家 如果想直接看源码的移步到源码地址:github.com/lenve/sched…
技术选型
SpringBoot
Jpa
MySql
vue
Spring Job
Element UI
*成品展示:
采用
SpringBoot + Jpa开发,开发起来更简易,下面来看详细
创建工程
先创建项目,用yml格式写配置文件 application.yml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
jpa:
database: mysql
database-platform: mysql
hibernate:
ddl-auto: update
properties:
hibernate:
# 指定数据库InnoDB存储引擎
dialect: org.hibernate.dialect.MySQL57Dialect
#logging:
# level: debug
基本功能实现
先写一个工具类,用来根据用户传入Bean名称去Spring容器中查找相应的Bean,这个工具类可以去实现ApplicationContextAware接口。下面说下实现这个接口的好处
-
不用类似
new ClassPathXmlApplicationContext()的方式,从已有的spring上下文取得已实例化的bean。 -
当一个类实现了这个接口之后,这个类就可以方便获得ApplicationContext中的所有bean。换句话说,就是这个类可以直接获取spring配置文件中,所有有引用到的bean对象。
-
加载Spring配置文件时,如果Spring配置文件中所定义的Bean类实现了
ApplicationContextAware接口,那么在加载Spring配置文件时,会自动调用ApplicationContextAware接口中的setApplicationContext()方法获得ApplicationContext对象, -
ApplicationContext对象是由spring注入的。前提必须在Spring配置文件中指定该类。
使用场景: 从ApplicationContextAware获取ApplicationContext上下文的情况,仅仅适用于当前运行的代码和已启动的Spring代码处于同一个Spring上下文,否则获取到的ApplicationContext是空的。
SpringContextUtils
@Component
public class SpringContextUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
/**
* 根据Bean的名称去查找一个bean
*/
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
/**
* 根据类型去spring容器查找bean
* @param requiredType
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
/**
* 判断一个Bean是否是单例的
* @param name
* @return
*/
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
/**
* 传入bean的名字,判断bean的类型
* @param name
* @return
*/
public static Class<? extends Object> getType(String name) {
return applicationContext.getType(name);
}
}
每一个定时任务都对应一个子线程,但这个子线程不能直接拿去用,下面对它封装 SchedulingRunnable
public class SchedulingRunnable implements Runnable{
private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);
private String beanName;
private String methodName;
private String params;
private Object targetBean;
private Method method;
public SchedulingRunnable(String beanName, String methodName) {
this(beanName,methodName,null);
}
public SchedulingRunnable(String beanName, String methodName, String params) {
this.beanName = beanName;
this.methodName = methodName;
this.params = params;
init();
}
// 初始化
private void init() {
try {
targetBean = SpringContextUtils.getBean(beanName);
if (StringUtils.hasText(params)){
// 假设只有定时任务只有一个参数,并且参数类型是String
method = targetBean.getClass().getDeclaredMethod(methodName,String.class);
}else {
method = targetBean.getClass().getDeclaredMethod(methodName);
}
// void makeAccessible(Method method) 将一个方法设置为可调用,主要针对private方法;
ReflectionUtils.makeAccessible(method);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
/**
* 定时任务,时间到了就执行run方法
*/
@Override
public void run() {
logger.info("定时任务开始执行 -bean:{},方法:{},参数:{}",beanName,methodName,params);
long startTime = System.currentTimeMillis();
try {
if (StringUtils.hasText(params)){
// 如果方法有参数
method.invoke(targetBean,params);
}else {
method.invoke(targetBean);
}
} catch (Exception e) {
e.printStackTrace();
logger.info(String.format("定时任务执行异常 - bean: %s,方法: %s,参数: %s",beanName,methodName,params),e);
}
long times = System.currentTimeMillis() - startTime;
logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);
}
// 防止用户重复添加
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SchedulingRunnable)) return false;
SchedulingRunnable that = (SchedulingRunnable) o;
return Objects.equals(beanName, that.beanName) && Objects.equals(methodName, that.methodName) && Objects.equals(params, that.params);
}
@Override
public int hashCode() {
return Objects.hash(beanName, methodName, params);
}
}
springboot中有一个bean,ThreadPoolTaskScheduler,可以很方便的对重复执行的任务进行调度管理;相比于通过java自带的周期性任务线程池ScheduleThreadPoolExecutor,此bean对象支持根据cron表达式创建周期性任务。ThreadPoolTaskScheduler其实底层使用也是java自带的线程池,所以下面直接使用该线程池
SchedulingConfig
@Configuration
public class SchedulingConfig {
@Bean
TaskScheduler taskScheduler(){
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(4);
// 取消任务时移除线程
threadPoolTaskScheduler.setRemoveOnCancelPolicy(true);
// 线程池任务调度器。设置线程名称前缀
threadPoolTaskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
return threadPoolTaskScheduler;
}
}
处理定时任务执行完返回结果
ScheduledTask
public class ScheduledTask {
// 定时任务返回结果
volatile ScheduledFuture future;
public void cancel(){
ScheduledFuture<?> future = this.future;
if (future != null){
future.cancel(true);
}
}
}
下面配置最核心的配置类,作用包括:定时任务的添加、执行与取消
实现org.springframework.beans.factory.DisposableBean接口的bean允许在容器销毁该bean的时候获得一次回调。DisposableBean接口也只规定了一个方法:
void destroy() throws Exception;
通常,要避免使用DisposableBean标志接口而且不鼓励使用该接口,因为这样会将代码与Spring耦合在一起,有一个可选的方案是,在bean定义中指定一个普通的析构方法,然后在XML配置文件中通过指定destroy-method属性来完成。
CronTaskRegistrar
package com.qinghong.scheduling.config;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class CronTaskRegistrar implements DisposableBean {
// 这个变量中保存着所有的定时任务
private final Map<Runnable,ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);
@Autowired
TaskScheduler taskScheduler;
public TaskScheduler getTaskScheduler(){
return this.taskScheduler;
}
public void addCronTask(Runnable task, String cronExpression) {
addCronTask(new CronTask(task, cronExpression));
}
/**
* 添加一个定时任务
*/
public void addCronTask(CronTask cronTask) {
if (cronTask != null) {
Runnable task = cronTask.getRunnable();
if (this.scheduledTasks.containsKey(task)) {
// 说明要添加的定时任务已经存在
// 先把已经存在的定时任务移除,然后再添加新的定时任务
removeCronTask(task);
}
// 添加一个定时任务
this.scheduledTasks.put(task, scheduleCronTask(cronTask));
}
}
public ScheduledTask scheduleCronTask(CronTask cronTask) {
ScheduledTask scheduledTask = new ScheduledTask();
scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
return scheduledTask;
}
public void removeCronTask(Runnable task) {
// 1.从Map集合中移除
ScheduledTask scheduledTask = this.scheduledTasks.remove(task);
// 2.取消正在执行的定时任务
if (scheduledTask != null)
scheduledTask.cancel();
}
@Override
public void destroy() {
// 1、让所有定时任务停止执行
for (ScheduledTask task : this.scheduledTasks.values()) {
task.cancel();
}
// 2、清空集合
this.scheduledTasks.clear();
}
}
下面创建一个系统任务实体类:
由于采用Jpa,只需要加上数据库表创建策略注解,数据库表就会自己生成
package com.qinghong.scheduling.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;
import java.util.Objects;
/**
* 一个SysJob 对象就代表一个定时任务
*/
@Data
@Entity(name = "t_sys_job")
public class SysJob {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer jobId;
private String beanName;
private String methodName;
private String methodParams;
// Cron表达式
private String cronExpression;
// 定时任务的状态,0表示暂停 1表示运行
private Integer jobStatus;
// 备注信息
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* beanName methodName methodParams cronExpression全部都相同就认为是同一个任务
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SysJob)) return false;
SysJob sysJob = (SysJob) o;
return Objects.equals(getBeanName(), sysJob.getBeanName()) && Objects.equals(getMethodName(), sysJob.getMethodName()) && Objects.equals(getMethodParams(), sysJob.getMethodParams()) && Objects.equals(getCronExpression(), sysJob.getCronExpression());
}
@Override
public int hashCode() {
return Objects.hash(getBeanName(), getMethodName(), getMethodParams(), getCronExpression());
}
}
接下来写数据访问层(dao)
SysJobRepository
package com.qinghong.scheduling.dao;
import com.qinghong.scheduling.model.SysJob;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface SysJobRepository extends JpaRepository<SysJob,Integer> {
List<SysJob> findAllByJobStatus(Integer status);
}
业务层代码:
package com.qinghong.scheduling.service;
import com.qinghong.scheduling.config.CronTaskRegistrar;
import com.qinghong.scheduling.config.SchedulingRunnable;
import com.qinghong.scheduling.dao.SysJobRepository;
import com.qinghong.scheduling.model.SysJob;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysJobService {
@Autowired
SysJobRepository sysJobRepository;
@Autowired
CronTaskRegistrar cronTaskRegistrar;
public List<SysJob> getSysJobByStatus(int status) {
return sysJobRepository.findAllByJobStatus(status);
}
public List<SysJob> getAllJobs() {
return sysJobRepository.findAll();
}
public Boolean updateSysJob(SysJob sysJob) {
SysJob job = sysJobRepository.saveAndFlush(sysJob);
if (job != null){
// 更新成功
SchedulingRunnable runnable = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
if (sysJob.getJobStatus() == 1){
// 定时任务是开启状态
cronTaskRegistrar.addCronTask(runnable,sysJob.getCronExpression());
}else {
// 定时任务是关闭状态,移除定时任务
cronTaskRegistrar.removeCronTask(runnable);
}
return true;
}else {
// 定时任务
return false;
}
}
}
由于项目启动要将运行态的定时任务跑起来,可以通过CommandLineRunner接口实现,下面先来说一说该接口使用。
在应用服务启动时,需要在所有Bean生成之后,加载一些数据和执行一些应用的初始化。例如:删除临时文件,清楚缓存信息,读取配置文件,数据库连接,这些工作类似开机自启动的概念,CommandLineRunner、ApplicationRunner 接口是在容器启动成功后的最后一步回调(类似开机自启动)。
CommandLineRunner源码实现如下:
package org.springframework.boot;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* Interface used to indicate that a bean should <em>run</em> when it is contained within
* a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined
* within the same application context and can be ordered using the {@link Ordered}
* interface or {@link Order @Order} annotation.
* <p>
* If you need access to {@link ApplicationArguments} instead of the raw String array
* consider using {@link ApplicationRunner}.
*
* @author Dave Syer
* @see ApplicationRunner
*/
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;
}
对该接口的注释可以看到如下的含义:
该接口用来指明:当一个bean包含在SpringApplication内,该bean就应当执行。可以在相同的应用上下文定义多个这样的bean。多个bean的先后执行顺序使用@Order注解确定。
下面是项目启动初始化执行的任务:
InitTask
package com.qinghong.scheduling.config;
import com.qinghong.scheduling.model.SysJob;
import com.qinghong.scheduling.service.SysJobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
// 初始化定时任务,获取数据库
@Component
public class InitTask implements CommandLineRunner {
@Autowired
CronTaskRegistrar cronTaskRegistrar;
@Autowired
SysJobService sysJobService;
@Override
public void run(String... args) throws Exception {
// 获取需要运行的定时任务
List<SysJob> list = sysJobService.getSysJobByStatus(1);
for (SysJob sysJob : list) {
cronTaskRegistrar.addCronTask(new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams()),sysJob.getCronExpression());
}
}
}
下面写个类和方法进行测试以下:
package com.qinghong.scheduling.taskdemo;
import org.springframework.stereotype.Component;
// 出入spring容器
@Component("schedulingTaskDemo")
public class SchedulingTaskDemo {
public void taskWithParams(String params){
System.out.println("执行带参数的定时任务..."+params);
}
public void taskWithoutParams(){
System.out.println("执行不带参数的定时任务");
}
}
可以看到该方法每秒执行了一次。也可以多添加几条数据测试测试;
可视化页面实现
封装返回前端的数据:
RespBean
package com.qinghong.scheduling.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
public static RespBean ok(String msg,Object obj) {
return new RespBean(200, msg, obj);
}
public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}
public static RespBean error(String msg,Object obj) {
return new RespBean(500, msg, obj);
}
public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}
private Integer status;
private String msg;
private Object data;
}
前端用vue和Element UI:
单页面开发,直接引入各cdn即可
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>定时任务在线管理系统</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-table border strip size="mini" :data="jobs">
<el-table-column prop="jobId" label="作业编号" width="80px"></el-table-column>
<el-table-column prop="beanName" label="Bean名称"></el-table-column>
<el-table-column prop="methodName" label="方法名称"></el-table-column>
<el-table-column prop="methodParams" label="方法参数"></el-table-column>
<el-table-column label="Cron表达式">
<template slot-scope="scope">
<el-tag size="mini">{{scope.row.cronExpression}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="jobStatus" label="作业状态">
<template slot-scope="scope">
<el-switch
@change="jobStatusChange(scope.row)"
size="mini"
v-model="scope.row.jobStatus"
active-color="#13ce66"
inactive-color="#ff4949"
active-text="开启"
:active-value="1"
:inactive-value="0"
inactive-text="禁用">
</el-switch>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="updateTime" label="更新时间"></el-table-column>
<el-table-column label="操作" width="200px">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="showUpdateTaskView(scope.row)">作业编辑</el-button>
<el-button type="danger" size="mini" @click="deleteTask(scope.row)">作业删除</el-button>
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" type="success" @click="showEditTaskView">添加作业</el-button>
<el-dialog
:title="dialogTitle"
size="mini"
:visible.sync="dialogVisible"
width="30%">
<div>
<el-form ref="form" label-width="140px" size="mini">
<el-form-item label="Bean名称" required>
<el-input v-model="job.beanName"></el-input>
</el-form-item>
<el-form-item label="方法名称" required>
<el-input v-model="job.methodName"></el-input>
</el-form-item>
<el-form-item label="方法参数">
<el-input v-model="job.methodParams"></el-input>
</el-form-item>
<el-form-item label="Cron表达式" required>
<el-input v-model="job.cronExpression"></el-input>
</el-form-item>
<el-form-item label="作业状态">
<el-radio v-model="job.jobStatus" :label="1">开启</el-radio>
<el-radio v-model="job.jobStatus" :label="0">禁用</el-radio>
</el-form-item>
<el-form-item label="备注信息">
<el-input v-model="job.remark"></el-input>
</el-form-item>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="dialogVisible = false">取 消</el-button>
<el-button size="mini" type="primary" @click="commitData">确 定</el-button>
</span>
</el-dialog>
</div>
<script>
new Vue({
el: '#app',
data: {
jobs: [],
dialogVisible: false,
dialogTitle: '添加作业',
job: {
beanName: '',
methodName: '',
methodParams: '',
cronExpression: '',
jobStatus: 1,
remark: ''
}
},
mounted() {
this.initJobs();
},
methods: {
initJobs() {
// 加载数据
axios.get('/jobs/').then(resp=>{
if (resp.status == 200){
this.jobs = resp.data;
}
})
},
deleteTask(data){
console.log(this.job)
console.log(data)
this.$confirm('此操作将永久删除该作业, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete('/jobs/?jobId='+data.jobId).then(resp=>{
console.log(resp)
// 更新成功
this.$notify({
title: resp.data.status == 200 ? '成功':'失败',
message: resp.data.msg,
type: resp.data.status == 200 ? 'success':'error'
})
// 刷新表格
this.initJobs();
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
jobStatusChange(data){
// 深复制
console.log(data)
console.log(this.job)
Object.assign(this.job,data);
this.updateJob();
},
updateJob() {
axios.put('/jobs/',this.job).then(resp=>{
console.log(resp)
if (resp.status == 200){
// 更新成功
this.$notify({
title: resp.data.status == 200 ? '成功':'失败',
message: resp.data.msg,
type: resp.data.status == 200 ? 'success':'error'
})
// 刷新表格
this.initJobs();
}
})
},
resetJob() {
this.job = {
beanName: '',
methodName: '',
methodParams: '',
cronExpression: '',
jobStatus: 1,
remark: ''
};
},
commitData() {
if (!this.job.beanName || !this.job.cronExpression || !this.job.methodName) {
this.$message.error('带 * 表示必填字段!');
return;
}
if (this.job.jobId) {
//更新
this.updateJob();
return;
}
axios.post("/jobs/",this.job).then(resp=>{
if (resp.status == 200) {
this.$notify({
title: resp.data.status==200?'成功':'失败',
message: resp.data.msg,
type: resp.data.status == 200 ? 'success' : 'error'
});
this.dialogVisible = false;
this.initJobs();
this.resetJob();
}
});
},
showEditTaskView() {
this.dialogTitle = '添加作业';
this.dialogVisible = true;
},
showUpdateTaskView(data) {
this.dialogTitle = '修改作业';
Object.assign(this.job, data);
this.dialogVisible = true;
}
}
})
</script>
</body>
</html>
创建控制层:
SysJobController
package com.qinghong.scheduling.Controller;
import com.qinghong.scheduling.model.RespBean;
import com.qinghong.scheduling.model.SysJob;
import com.qinghong.scheduling.service.SysJobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/jobs")
public class SysJobController {
@Autowired
SysJobService sysJobService;
/**
* 获取所有任务
* @return
*/
@GetMapping("/")
public List<SysJob> getAllJobs() {
return sysJobService.getAllJobs();
}
// 更新任务状态
@PutMapping("/")
public RespBean updateSysJob(@RequestBody SysJob sysJob){
Boolean result = sysJobService.updateSysJob(sysJob);
if (result){
return RespBean.ok("更新作业成功");
}
return RespBean.error("更新作业失败");
}
// 编辑作业
/**
* 添加作业
* @param sysJob
* @return
*/
@PostMapping("/")
public RespBean addJob(@RequestBody SysJob sysJob) {
Boolean flag = sysJobService.addJob(sysJob);
if (flag) {
return RespBean.ok("作业添加成功");
}
return RespBean.error("作业重复,添加失败");
}
// 删除作业
@DeleteMapping("/")
public RespBean deleteSysJob(int jobId){
Boolean flag = sysJobService.deleteJobsById(jobId);
if (flag) {
return RespBean.ok("删除成功");
}
return RespBean.error("删除失败");
}
}
在业务层添上添加和删除功能:
/**
* 添加作业
* @param sysJob
* @return
*/
public Boolean addJob(SysJob sysJob) {
List<SysJob> all = sysJobRepository.findAll();
for (SysJob job : all) {
if (job.equals(sysJob)) {
//作业重复,添加失败
return false;
}
}
//添加
SysJob sj = sysJobRepository.save(sysJob);
if (sj != null) {
//添加成功,如果新加的job是开启状态,就顺便开启
SchedulingRunnable schedulingRunnable = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
if (sj.getJobStatus() == 1) {
cronTaskRegistrar.addCronTask(schedulingRunnable, sysJob.getCronExpression());
}
//添加成功
return true;
}
return false;
}
public Boolean deleteJobsById(Integer id) {
SysJob sysJob = sysJobRepository.findById(id).get();
SchedulingRunnable schedulingRunnable = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
cronTaskRegistrar.removeCronTask(schedulingRunnable);
sysJobRepository.delete(sysJob);
return true;
}
启动项目,效果如下图