主頁 > 後端開發 > boot-admin整合Quartz實作動態管理定時任務

boot-admin整合Quartz實作動態管理定時任務

2023-04-27 07:29:47 後端開發

淄博燒烤爆紅出了圈,當你坐在八大局的燒烤攤,面前是火爐、烤串、小餅和蘸料,音樂響起,啤酒倒滿,燒烤靈魂的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  style="background-color: #FFFFFF;">
    <!--功能按鈕區-->
    <div >
      <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="https://www.cnblogs.com/soft1314/archive/2023/04/26/BaseTableData.table" style="width: 100%" :border="true">
      <el-table-column type="index" :index="indexMethod" />
      <el-table-column prop="jobName" label="任務名稱"  />
      <el-table-column prop="jobGroup" label="任務所在組"  />
      <el-table-column prop="jobClassName" label="任務類名" />
      <el-table-column prop="cronExpression" label="運算式"  />
      <el-table-column prop="timeZoneId" label="時區"  />
      <el-table-column prop="startTime" label="開始"  :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"/>
      <el-table-column prop="nextFireTime" label="下次"  :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"/>
      <el-table-column prop="previousFireTime" label="上次"  :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"/>
      <el-table-column prop="triggerState" label="狀態" >
        <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="操作" >
        <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  /></el-button>
          <el-button type="text" title="以彈出視窗形式顯示" @click="resizeInputBaseInfoDialogNormal()"><i  /></el-button>
          <el-button type="text" title="關閉" @click="closeInputBaseInfoDialog()"><i  /></el-button>
        </div>
      </div>
      <!--彈窗頭部header-->
      <!--彈窗表單-->
      <el-form
        ref="InputBaseInfoForm"
        :status-icon="InputBaseInfoDialogData.statusIcon"
        :model="InputBaseInfoDialogData.data"
        
      >
        <el-form-item label="原任務名稱" :label- prop="jobName">
          {{ InputBaseInfoDialogData.data.oldJobName }}【修改任務時使用】
        </el-form-item>
        <el-form-item label="原任務分組" :label- prop="jobGroup">
          {{ InputBaseInfoDialogData.data.oldJobGroup }}【修改任務時使用】
        </el-form-item>
        <el-form-item label="任務名稱" :label- prop="jobName">
          <el-input v-model="InputBaseInfoDialogData.data.jobName" auto-complete="off" />
        </el-form-item>
        <el-form-item label="任務分組" :label- prop="jobGroup">
          <el-input v-model="InputBaseInfoDialogData.data.jobGroup" auto-complete="off" />
        </el-form-item>
        <el-form-item label="類名" :label- prop="jobClassName">
          <el-input v-model="InputBaseInfoDialogData.data.jobClassName" auto-complete="off" />
        </el-form-item>
        <el-form-item label="運算式" :label- prop="cronExpression">
          <el-input v-model="InputBaseInfoDialogData.data.cronExpression" auto-complete="off" />
        </el-form-item>
      </el-form>
      <!--彈窗表單-->
      <!--彈窗尾部footer-->
      <div slot="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  title="全屏顯示" /></el-button>
          <el-button type="text" @click="dialogResize('ViewBaseInfoDialog',false)"><i
            
            title="以彈出視窗形式顯示"
          /></el-button>
          <el-button type="text" @click="dialogClose('ViewBaseInfoDialog')"><i  title="關閉" /></el-button>
        </div>
      </div>
      <!--彈窗頭部header-->
      <!--彈窗表單-->
      <el-form
        ref="ViewBaseInfoForm"
        :status-icon="ViewBaseInfoDialogData.statusIcon"
        :model="ViewBaseInfoDialogData.data"
        
      >
        <el-form-item label="運算式" :label- 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 = https://www.cnblogs.com/soft1314/archive/2023/04/26/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 = https://www.cnblogs.com/soft1314/archive/2023/04/26/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 = "https://www.cnblogs.com/soft1314/archive/2023/04/26/保存添加或修改任務",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 = "https://www.cnblogs.com/soft1314/archive/2023/04/26/查詢任務",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 = "https://www.cnblogs.com/soft1314/archive/2023/04/26/暫停任務",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 = "https://www.cnblogs.com/soft1314/archive/2023/04/26/恢復任務",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 = "https://www.cnblogs.com/soft1314/archive/2023/04/26/洗掉任務",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

本文來自博客園,作者:超然樓,轉載請注明原文鏈接:https://www.cnblogs.com/soft1314/p/17357443.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551289.html

標籤:其他

上一篇:Android監聽事件

下一篇:返回列表

標籤雲
其他(158173) Python(38107) JavaScript(25394) Java(18001) C(15217) 區塊鏈(8260) C#(7972) AI(7469) 爪哇(7425) MySQL(7148) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5870) 数组(5741) R(5409) Linux(5329) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4562) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2431) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1960) Web開發(1951) HtmlCss(1927) python-3.x(1918) 弹簧靴(1913) C++(1912) xml(1889) PostgreSQL(1874) .NETCore(1855) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • boot-admin整合Quartz實作動態管理定時任務

    淄博燒烤爆紅出了圈,當你坐在八大局的燒烤攤,面前是火爐、烤串、小餅和蘸料,音樂響起,啤酒倒滿,燒烤靈魂的party即將開場的時候,你系統中的Scheduler(除錯器),也自動根據設定的Trigger(觸發器),從容優雅的啟動了一系列的Job(后臺定時任務)。作業一切早有安排,又何須費心勞神呢?因為 ......

    uj5u.com 2023-04-27 07:29:47 more
  • Android監聽事件

    監聽事件 ? 監聽事件機制由事件源,事件和事件監聽器三類物件組成,事件源一般就是activity中的UI控制元件。 下面參考別人整理的圖來更加形象的表達這些關系。 ? 事件監聽機制的意義就是讓事件源的行為委托給事件監聽器,讓監聽器控制事件的發生。 ? 1.實作監聽事件的方法 [ ] 通過內部類實作 [ ......

    uj5u.com 2023-04-27 07:29:38 more
  • Django筆記三十一之全域例外處理

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記三十一之全域例外處理 這一篇筆記介紹 Django 的全域例外處理。 當我們在處理一個 request 請求時,會盡可能的對介面資料的格式,內部呼叫的函式做一些例外處理,但可能還是會有一些意想不到的漏網之魚,造成程式的例外導致不能正常運行 ......

    uj5u.com 2023-04-27 07:29:30 more
  • elastic-job原始碼(1)- job自動裝配

    版本:3.1.0-SNAPSHOT git地址:https://github.com/apache/shardingsphere-elasticjob Maven 坐標 <dependency> <groupId>org.apache.shardingsphere.elasticjob</group ......

    uj5u.com 2023-04-27 07:29:24 more
  • java 多執行緒的start()和run()的理解

    run()方法中是各個執行緒要執行的具體內容。所以當一個執行緒直接呼叫run()時那么直接開始執行方法體,這是在main執行緒中的多個執行緒只能時按照順序的等待前面的執行緒結束run()方法的執行。 而呼叫start方法只是執行緒進入準備階段(Ready),并沒有真正執行,這需要JVM進行分配時間片進行輪轉執行緒 ......

    uj5u.com 2023-04-27 07:29:20 more
  • 面向物件可視化工具:UML類圖

    1. UML類圖 UML(Unified Modeling Language,統一建模語言),用來描述軟體模型和架構的圖形化語言。 常用的UML工具軟體有PowerDesinger、Rose和Enterprise Architect。 UML工具軟體不僅可以繪制軟體開發中所需的各種圖表,還可以生成對 ......

    uj5u.com 2023-04-27 07:29:14 more
  • Java中關于String類以及字串拼接的問題

    String類部分原始碼 //被final修飾不可被繼承 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { //String維護char[] 所以不可修改 priv ......

    uj5u.com 2023-04-27 07:29:06 more
  • Python生成亂數的一個標準庫-random

    1.介紹 Random庫Python中用于生成亂數的一個標準庫。計算機沒有辦法產生真正的亂數,但它可以產生偽亂數。 偽亂數是計算機按照一定的運算規則產生的一些資料,只不過這些資料表現為亂數的形式。計算機中采用梅森旋轉演算法生成為隨機序列,序列中的每一個元素就是偽亂數,由于計算機不能產生真正 ......

    uj5u.com 2023-04-27 07:29:01 more
  • go中 for回圈的坑

    go中 for回圈的坑 在使用for回圈修改結構體切片中的值時,發現并沒有修改成功。 type Dog struct { name string } func (d *Dog) setNewName(name string) { d.name = name } func main() { d := ......

    uj5u.com 2023-04-27 07:28:56 more
  • sourceTree合并一次提交的內容

    sourceTree合并一次提交的內容 在基于git的開發中,經常遇到不同分支需要合并某一次特定的提交的代碼,而不是合并整個代碼。 場景:A分支是通用分支,B分支是私有化分支,現在A分支修改了一個通用的功能,需要合并到B分支上,功能在一次提交上。B分支只需要這次提交的代碼,對A分支上改動的其他代碼都 ......

    uj5u.com 2023-04-27 07:28:49 more