• 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-schedulergroupId>
      <artifactId>quartzartifactId>
      <version>2.3.2version>
    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"
        >
          
          <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>
          
          
          <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>
          
          
          <div slot="footer" class="dialog-footer">
            <el-button type="primary" @click="saveInputBaseInfoForm()">保 存el-button>
          div>
          
        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"
        >
          
          <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>
          
          
          <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
    
    
    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 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 queryJob(int pageNum, int pageSize) throws Exception{
            List jobList = null;
            try {
                Scheduler scheduler = schedulerFactoryBean.getScheduler();
                GroupMatcher matcher = GroupMatcher.anyJobGroup();
                Set jobKeys = scheduler.getJobKeys(matcher);
                jobList = new ArrayList<>();
                for (JobKey jobKey : jobKeys) {
                    Listextends 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 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

  • 相关阅读:
    Python教程之正则表达式实现拼音与中文提取并存储excel(教程含源码)
    前端笔记(7) Vue3 插槽slot的使用案例
    解构华为云HE2E项目中的容器技术应用
    CSS 三栏布局
    2022年,都在说软件测试饱和了,大环境不好?为何每年还会增加40万测试员?
    如何在vscode中編輯和預覽markdown文檔
    飞行动力学 - 第31节-荷兰滚模态机理 之 基础点摘要
    低代码开发平台:赋予不同角色应用开发技能
    Android学习笔记 27. Fragment + ViewPager
    根据轮廓创建旋转框和椭圆
  • 原文地址:https://www.cnblogs.com/soft1314/p/17357443.html