boot-admin整合Quartz实现动态管理定时任务

淄博烧烤爆红出了圈,当你坐在八大局的烧烤摊,面前是火炉、烤串、小饼和蘸料,音乐响起,啤酒倒满,烧烤灵魂的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>
  <div class="app-container" style="background-color: #FFFFFF;">
    <!--功能按钮区-->
    <div class="cl pd-5 bg-1 bk-gray">
      <div align="left" style="float: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 style="align:right">
          <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" style="width: 100%" :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" style="margin-bottom: 10px">
        <div align="left" style="float:left">
          <h3>定时任务管理</h3>
        </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" style="margin-bottom: 10px">
        <div align="left" style="float:left">
          <h3>修改任务</h3>
        </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());
        // 自定义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基类

public abstract class BaseJob implements Job, Serializable {
    private static final String JOB_MAP_KEY = "self";
    public static final String STATUS_RUNNING = "1";
    public static final String STATUS_NOT_RUNNING = "0";
    public static final String CONCURRENT_IS = "1";
    public static final String CONCURRENT_NOT = "0";
    /**
     * 任务名称
     */
    private String jobName;
    /**
     * 任务分组
     */
    private String jobGroup;
    /**
     * 任务状态 是否启动任务
     */
    private String jobStatus;
    /**
     * cron表达式
     */
    private String cronExpression;
    /**
     * 描述
     */
    private String description;
    /**
     * 任务执行时调用哪个类的方法 包名+类名
     */
    private Class beanClass = this.getClass();
    /**
     * 任务是否有状态
     */
    private String isConcurrent;
    /**
     * Spring bean
     */
    private String springBean;
    /**
     * 任务调用的方法名
     */
    private String methodName;
    /**
     * 为了将执行后的任务持久化到数据库中
     */
    @JsonIgnore
    private JobDataMap dataMap = new JobDataMap();

    public JobKey getJobKey(){
        return JobKey.jobKey(jobName, jobGroup);// 任务名称和组构成任务key
    }
    public JobDataMap getDataMap(){
        if(dataMap.size() == 0){
            dataMap.put(JOB_MAP_KEY,this);
        }
        return dataMap;
    }
    public String getJobName() {
        return jobName;
    }
    public void setJobName(String jobName) {
        this.jobName = jobName;
    }
    public String getJobGroup() {
        return jobGroup;
    }
    public void setJobGroup(String jobGroup) {
        this.jobGroup = jobGroup;
    }
    public String getJobStatus() {
        return jobStatus;
    }
    public void setJobStatus(String jobStatus) {
        this.jobStatus = jobStatus;
    }
    public String getCronExpression() {
        return cronExpression;
    }
    public void setCronExpression(String cronExpression) {
        this.cronExpression = cronExpression;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public Class getBeanClass() {
        return beanClass;
    }
    public void setBeanClass(Class beanClass) {
        this.beanClass = beanClass;
    }
    public String getIsConcurrent() {
        return isConcurrent;
    }
    public void setIsConcurrent(String isConcurrent) {
        this.isConcurrent = isConcurrent;
    }
    public String getSpringBean() {
        return springBean;
    }
    public void setSpringBean(String springBean) {
        this.springBean = springBean;
    }
    public String getMethodName() {
        return methodName;
    }
    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
}

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

@Service
@Slf4j
public 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<JobDTO> queryJob(int pageNum, int pageSize) throws Exception{
        List<JobDTO> jobList = null;
        try {
            Scheduler scheduler = schedulerFactoryBean.getScheduler();
            GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
            Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
            jobList = new ArrayList<>();
            for (JobKey jobKey : jobKeys) {
                List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                for (Trigger trigger : triggers) {
                    JobDTO jobDetails = new JobDTO();
                    if (trigger instanceof CronTrigger) {
                        CronTrigger cronTrigger = (CronTrigger) trigger;
                        jobDetails.setCronExpression(cronTrigger.getCronExpression());
                        jobDetails.setTimeZoneId(cronTrigger.getTimeZone().getDisplayName());
                    }
                    jobDetails.setTriggerGroup(trigger.getKey().getName());
                    jobDetails.setTriggerName(trigger.getKey().getGroup());
                    jobDetails.setJobGroup(jobKey.getGroup());
                    jobDetails.setJobName(jobKey.getName());
                    jobDetails.setStartTime(trigger.getStartTime());
                    jobDetails.setJobClassName(scheduler.getJobDetail(jobKey).getJobClass().getName());
                    jobDetails.setNextFireTime(trigger.getNextFireTime());
                    jobDetails.setPreviousFireTime(trigger.getPreviousFireTime());
                    jobDetails.setTriggerState(scheduler.getTriggerState(trigger.getKey()).name());
                    jobList.add(jobDetails);
                }
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        IPage<JobDTO> jobDTOPage = new Page<>(pageNum,pageSize);
        jobDTOPage.setRecords(jobList);
        jobDTOPage.setTotal(jobList.size());
        jobDTOPage.setCurrent(1);
        jobDTOPage.setPages(1);
        jobDTOPage.setSize(jobList.size());
        return jobDTOPage;
    }

    /**
     * 添加一个任务
     * @param job
     * @throws SchedulerException
     */
    @Override
    public void addJob(BaseJob job) throws SchedulerException {
        /** 创建JobDetail实例,绑定Job实现类
         * JobDetail 表示一个具体的可执行的调度程序,job是这个可执行调度程序所要执行的内容
         * 另外JobDetail还包含了这个任务调度的方案和策略**/
        // 指明job的名称,所在组的名称,以及绑定job类
        JobDetail jobDetail = JobBuilder.newJob(job.getBeanClass())
                .withIdentity(job.getJobKey())
                .withDescription(job.getDescription())
                .usingJobData(job.getDataMap())
                .build();
        /**
         * Trigger代表一个调度参数的配置,什么时候去调度
         */
        //定义调度触发规则, 使用cronTrigger规则
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(job.getJobName(),job.getJobGroup())
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
                .startNow()
                .build();
        //将任务和触发器注册到任务调度中去
        scheduler.scheduleJob(jobDetail,trigger);
        //判断调度器是否启动
        if(!scheduler.isStarted()){
            scheduler.start();
        }
        log.info(String.format("定时任务:%s.%s-已添加到调度器!", job.getJobGroup(),job.getJobName()));
    }
    /**
     * 根据任务名和任务组名来暂停一个任务
     * @param jobName
     * @param jobGroupName
     * @throws SchedulerException
     */
    @Override
    public void pauseJob(String jobName,String jobGroupName) throws SchedulerException {
        scheduler.pauseJob(JobKey.jobKey(jobName,jobGroupName));
    }
    /**
     * 根据任务名和任务组名来恢复一个任务
     * @param jobName
     * @param jobGroupName
     * @throws SchedulerException
     */
    @Override
    public void resumeJob(String jobName,String jobGroupName) throws SchedulerException {
        scheduler.resumeJob(JobKey.jobKey(jobName,jobGroupName));
    }
    public void rescheduleJob(String jobName,String jobGroupName,String cronExpression,String description) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
        // 表达式调度构建器
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        // 按新的cronExpression表达式重新构建trigger
        trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withDescription(description).withSchedule(scheduleBuilder).build();
        // 按新的trigger重新设置job执行
        scheduler.rescheduleJob(triggerKey, trigger);
    }
    /**
     * 根据任务名和任务组名来删除一个任务
     * @param jobName
     * @param jobGroupName
     * @throws SchedulerException
     */
    @Override
    public boolean deleteJob(String jobName,String jobGroupName) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName,jobGroupName);
        scheduler.pauseTrigger(triggerKey); //先暂停
        scheduler.unscheduleJob(triggerKey); //取消调度
        boolean flag = scheduler.deleteJob(JobKey.jobKey(jobName,jobGroupName)); 
        return flag;
    }
    private JobDTO createJob(String jobName, String jobGroup, Scheduler scheduler, Trigger trigger)
            throws SchedulerException {
        JobDTO job = new JobDTO();
        job.setJobName(jobName);
        job.setJobGroup(jobGroup);
        job.setDescription("触发器:" + trigger.getKey());
        Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
        job.setTriggerState(triggerState.name());
        if(trigger instanceof CronTrigger) {
            CronTrigger cronTrigger = (CronTrigger)trigger;
            String cronExpression = cronTrigger.getCronExpression();
            job.setCronExpression(cronExpression);
        }
        return job;
    }
}

至此,烤串完毕,火侯正好,外酥里嫩!
在这里插入图片描述
项目源码仓库github
项目源码仓库gitee

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/14434.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【操作系统】第一章

文章目录 &#x1f337; 一、操作系统的概念1、定义2、功能 和 目标 &#x1f337; 二、操作系统的特征1、**并发**2、 **共享**3、 **虚拟**4、 **不确定性** &#x1f337; 三、操作系统的发展与分类1、 手工操作阶段2、 批处理阶段3、 分时操作系统4、 实时操作系统5、 网络…

【移动端网页布局】移动端网页布局基础概念 ④ ( 物理像素 | 物理像素比 | 代码示例 - 100 像素在 PC浏览器 / 移动端浏览器 显示效果 )

文章目录 一、物理像素 / 物理像素比二、代码示例 - 100 像素在 PC浏览器 / 移动端浏览器 显示效果 一、物理像素 / 物理像素比 移动端 网页开发 与 PC 端开发有很多不同之处 , 在图片处理方向需要采用 二倍图 / 三倍图 / 多倍图 方式进行图片处理 ; 图片处理的方式与如下的 物…

4月26号软件更新资讯合集....

Tpflow V7.0.2&#xff0c;PHP 工作流引擎新版发布 ​欢迎使用 Tpflow V7.0.1 工作流引擎 TpFlow 工作流引擎是一套规范化的流程管理系统&#xff0c;基于业务而驱动系统生命力的一套引擎。彻底释放整个信息管理系统的的活力&#xff0c;让系统更具可用性&#xff0c;智能应用型…

一文学会VSCode代码同步至GitHub

一、上手GitHub 1. 了解GitHub 上手GItHub之前首先要了解一下GItHub的关键词&#xff0c;如下&#xff1a; (1) 仓库 (Repository) 仓库是用来存放项目代码&#xff0c;每一项目对应一个仓库。(2) 收藏 (Star) 收藏别人的仓库&#xff0c;方便自己查找。(3) 复制/克隆项目 (…

数据库基础篇 《12.MySQL数据类型精讲》

目录 1. MySQL中的数据类型 2. 整数类型 2.1 类型介绍 2.2 可选属性 2.2.1 M 2.2.2 UNSIGNED 2.2.3 ZEROFILL 2.3 适用场景 2.4 如何选择&#xff1f; 3. 浮点类型 3.1 类型介绍 3.2 数据精度说明 ​编辑 3.3 精度误差说明 4. 定点数类型 4.1 类型介绍 4.2 开发中…

Ingonyama团队的ZKP加速

1. PipeMSM&#xff08;cloud-ZK&#xff09;&#xff1a;ZKPFPGA Ingonyama团队2022年发表了论文《PipeMSM: Hardware Acceleration for Multi-Scalar Multiplication》&#xff0c;尝试将ZK操作与FPGA结合&#xff0c;并为未来ZK与ASIC&#xff08;Application Specific Int…

java_集合统计

1.代码实现&#xff1a; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ListUtilEme {public static Map<String,Integer> frequencyOfListElements(List<String> items ) {if (items null…

什么!appium安装不上???快来试试这种方法吧!——appium的手动安装步骤教程

前言 相信你不少软件测试行业小伙伴应该在用npm安装appuim或者是cpm安装appuim途中也碰到下面一些报错吧&#xff0c;接下来小陈教你改为手动安装appium吧。 一、手动下载appium安装包 appuim手动安装包下载链接&#xff1a; appium / Appium.app / Downloads — Bitbucket &a…

< 封装公共导出模块:配合element实现提示 >

封装公共导出模块 &#x1f449; 前言&#x1f449; 一、原理&#x1f449; 二、实现案例&#x1f449; 三、效果演示往期内容 &#x1f4a8; &#x1f449; 前言 在 Vue elementUi 开发中&#xff0c;我们偶尔会遇到需要导出的列表&#xff0c;或者指定位置的导出内容。在一…

瑞萨开发环境搭建

使用keil环境&#xff0c;开发瑞萨renase A4M2 下载MDK 下载MDK&#xff0c;5.37 其它版本 最好使用5.30以上 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5k3XGSK5-1682182139410)(https://secure2.wostatic.cn/static/reEunrWa2vsfrcpVZC1nbo…

【错误:A component required a bean of type ‘xxx‘ that could not be found.解决办法】

在学谷粒商城项目的时候出现了以下问题&#xff1a; *************************** APPLICATION FAILED TO START *************************** Description: A component required a bean of type org.redisson.Redisson that could not be found. Action: Consider defining a…

103. 二叉树的锯齿形层序遍历【191】

难度等级&#xff1a;中等 上一篇算法&#xff1a; 104. 二叉树的最大深度【75】 力扣此题地址&#xff1a; 103. 二叉树的锯齿形层序遍历 - 力扣&#xff08;Leetcode&#xff09; 1.题目&#xff1a;103. 二叉树的锯齿形层序遍历 给你二叉树的根节点 root &#xff0c;返回其…

24、LLVM编译流程

一、LLVM 1.1 LLVM概述 LLVM是构架编译器(compiler)的框架系统,以C编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本.LLVM计划启动于2000年,最初由美国…

家用洗地机要怎么选?平价洗地机推荐

国内大多数家庭比较注重地面清洁&#xff0c;不仅是要扫的干净&#xff0c;更要拖的干净&#xff0c;尤其追求地板锃亮的视觉效果&#xff0c;因此家用洗地机因其清洁效率高、能吸除干湿垃圾以及自清洁拖布等优点&#xff0c;成为很多家庭用于替代扫帚拖把等传统清洁工具的清洁…

被优化了怎么办?他苦学仨月拿到11koffer

网上有个段子叫做“生活就是起起落落落落落落”。人生在世&#xff0c;本就不易&#xff0c;再加上最近大环境影响&#xff0c;各行各业都在内卷&#xff0c;身为芸芸众生的一员&#xff0c;我们也难免受到影响&#xff0c;面临福利裁剪、降薪、甚至被优化的风险。 大环境我们…

云擎未来 万象共生:2023移动云万象生态峰会来袭

云融万象&#xff0c;赋能千行百业&#xff0c;云是万物智能的源泉&#xff0c;生态是移动云与万千伙伴共同发展的沃土。 2023移动云万象生态峰会将于4月25日下午在苏州金鸡湖国际会议中心隆重举行&#xff0c;大会荟聚众多重量级嘉宾&#xff0c;共话生态新发展&#xff0c;同…

Nacos简介 安装 配置

简介 什么是注册中心 注册中心在微服务项目中扮演着非常重要的角色&#xff0c;是微服务架构中的纽带&#xff0c;类似于通讯录&#xff0c;它记录了服务和服务地址的映射关系。在分布式架构中&#xff0c;服务会注册到这里&#xff0c;当服务需要调用其它服务时&#xff0c;…

给你们讲个笑话——低代码会取代程序员

今天是正经男&#xff0c;我们严肃讨论一下一直以来争吵不休的取代问题。 低代码开发平台&#xff0c;低代码技术会取代开发人员么&#xff1f; 一、背景 低代码开发平台的普及&#xff0c;让很多公司对快速生成应用抱有很大期望。甚至有人认为&#xff0c;低代码开发平台未来…

关于Java注解的一些理解 小结

目录 1. 常用注解和理解 2. 自定义注解 2.1 案例背景 2.2 设计思路 3 总结 1. 常用注解和理解 注解在我的理解下&#xff0c;就是代码中的特殊标记&#xff0c;这些标记可以在编译、类加载、运行时被读取&#xff0c;并执行相对应的处理。 可能有些抽象&#xff0c;简单…

JavaWeb学习笔记

文章目录 一. HTML二. CSS三. JavaScript1. 引入2.语法/输出语句3. 变量/数据类型4. 运算符5. 流程控制语句6. 函数7. 对象8. 事件监听 四. Servlet1.执行流程2. 生命周期3. 常用方法4. 体系结构5. 配置Servlet 五. JSP1. 简介2. JSP原理3.脚本4.JSP缺点5. EL表达式6. JSTL标签…