SpringBoot集成Quzrtz實現定時任務

一 定時任務介紹

自律是很多人都想擁有的一種能力,或者說素質,但是理想往往很美好,現實卻是無比殘酷的。在現實生活中,我們很難做到自律,或者說做到持續自律。例如,我們經常會做各種學習計劃、儲蓄計劃或減肥計劃等,但無一例外地被各種“意外”打破。這往往使得我們非常沮喪,甚至開始懷疑人生。

但是有一個“家伙”在自律方面做得格外出色。它只要制訂了計劃就會嚴格地執行,而且無論一個任務重復多少遍都不厭其煩,簡直自律到“令人發指”,它就是定時任務。

1.1 什么時候需要定時任務

哪些業務場景適合使用定時任務呢?簡單概括一下就是:at sometime to do something.凡是在某一時刻需要做某件事情時,都可以考慮使用定時任務(非實時性需求)。
定時任務常見業務場景

  • 銀行月底匯總賬單
  • 電信公司月底結算話費
  • 訂單在30分鐘內未支付會自動取消(延時任務)
  • 商品詳情、文章的緩存定時更新
  • 定時同步跨庫的數據庫表數據

1.2 java中的定時任務

1.2.1 單機環境

  • Timer:來自JDK,從JDK 1.3開始引入。JDK自帶,不需要引入外部依賴,簡單易用,但是功能相對單一。
  • ScheduledExecutorService:同樣來自JDK,比Timer晚一些,從JDK 1.5開始引入,它的引入彌補了Timer的一些缺陷。
  • Spring Task:來自Spring,Spring環境中單機定時任務的不二之選。

1.2.2 分布式環境

  • Quartz:一個完全由 Java 編寫的開源作業調度框架,分布式定時任務的基石,功能豐富且強大,既能與簡單的單體應用結合,又能支撐起復雜的分布式系統。
  • ElasticJob:來自當當網,最開始是基于Quartz開發的,后來改用ZooKeeper來實現分布式協調。它具有完整的定時任務處理流程,很多國內公司都在使用(目前登記在冊的有80多家),并且支持云開發。
  • XXL-JOB:來自大眾點評,同樣是基于Quartz開發的,后來改用自研的調度組件。它是一個輕量級的分布式任務調度平臺,簡單易用,很多國內公司都在使用(目前登記在冊的有400多家)。
  • PowerJob:號稱“全新一代分布式調度與計算框架”,采用無鎖化設計,支持多種報警通知方式(如WebHook、郵件、釘釘及自定義)。它比較重量級,適合做公司公共的任務調度中間件。

二 Quartz介紹

2.1 核心概念

  • Job:是一個接口,表示一個工作,要具體執行的內容,任務的核心邏輯。該接口只有一個excute方法
  • JobDetail:對Job進一步封裝,一個具體的可執行的調度程序。包含了任務的調度方案和策略。JobDetail既然是通用任務,用于接受任務,所以我們需要定義一個自己的任務類(例如叫做QuartzDetailJob),這個任務類需要實現 Job接口,這個任務類QuartzDetailJob,要執行具體的任務,具體的任務,一般都是我們寫的自己的一些方法
  • Trigger:觸發器,調度參數的配置,配置什么時候去調定時任務。主要用來指定Job的觸發規則,分為SimpleTrigger和CronTrigger(這里使用的是常用的CronTrigger)
  • Scheduler:調度容器(調度中心,任務交給它就行了),一個調度容器中可以注冊多個 JobDetail 和 Trigger。當 Trigger 與 JobDetail 組合,就可以被 Scheduler 容器調度了。。用來維護Job的生命周期(創建、刪除、暫停、調度等)

2.1 SpringBoot單機版-整合Quartz代碼實戰

數據庫表結構官網已經提供,我們可以直接訪問Quartz對應的官網下載,找到對應的版本,然后將其下載!目前最新的穩定版是2.3.0,我們就下載這個版本

在這里插入圖片描述
下載完成之后將其解壓,在文件中搜索sql,在里面選擇適合當前環境的數據庫腳本文件,然后將其初始化到數據庫中即可!我這里使用的是mysql,所以使用tables_mysql_innodb.sql這個腳本
在這里插入圖片描述
把里邊的sql語句,在mysql庫里執行即可。共涉及到11張表,每張表的含義如下
在這里插入圖片描述
其中,QRTZ_LOCKS 就是 Quartz 集群實現同步機制的行鎖表

引入依賴

<!--定時任務-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--druid 數據連接池-->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version>
</dependency>

新建quartz.properties 配置文件(數據庫信息換為自己的數據庫即可)

#調度配置
#調度器實例名稱
org.quartz.scheduler.instanceName=SsmScheduler
#調度器實例編號自動生成
org.quartz.scheduler.instanceId=AUTO
#是否在Quartz執行一個job前使用UserTransaction
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false#線程池配置
#線程池的實現類
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#線程池中的線程數量
org.quartz.threadPool.threadCount=10
#線程優先級
org.quartz.threadPool.threadPriority=5
#配置是否啟動自動加載數據庫內的定時任務,默認true
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
#是否設置為守護線程,設置后任務將不會執行
#org.quartz.threadPool.makeThreadsDaemons=true#持久化方式配置
#JobDataMaps是否都為String類型
org.quartz.jobStore.useProperties=true
#數據表的前綴,默認QRTZ_
org.quartz.jobStore.tablePrefix=QRTZ_
#最大能忍受的觸發超時時間
org.quartz.jobStore.misfireThreshold=60000
#是否以集群方式運行
org.quartz.jobStore.isClustered=true
#調度實例失效的檢查時間間隔,單位毫秒
org.quartz.jobStore.clusterCheckinInterval=2000
#數據保存方式為數據庫持久化
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
#數據庫代理類,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以滿足大部分數據庫
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#數據庫別名 隨便取
org.quartz.jobStore.dataSource=qzDS#數據庫連接池,將其設置為druid
org.quartz.dataSource.qzDS.connectionProvider.class=com.ts.hjbz.quartz.DruidConnectionProvider
#數據庫引擎
org.quartz.dataSource.qzDS.driver=com.mysql.jdbc.Driver
#數據庫連接
org.quartz.dataSource.qzDS.URL=jdbc:mysql://192.168.119.128:3306/hjbz?serverTimezone=GMT%2B8&characterEncoding=utf-8
#數據庫用戶
org.quartz.dataSource.qzDS.user=root
#數據庫密碼
org.quartz.dataSource.qzDS.password=123456
#允許最大連接
org.quartz.dataSource.qzDS.maxConnection=5
#驗證查詢sql,可以不設置
org.quartz.dataSource.qzDS.validationQuery=select 0 from dual

注冊 Quartz 任務工廠

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;/*** @Author:sgw* @Date:2023/9/1* @Description: 注冊 Quartz 任務工廠*/
@Component
public class QuartzJobFactory extends AdaptableJobFactory {@Autowiredprivate AutowireCapableBeanFactory capableBeanFactory;@Overrideprotected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {//調用父類的方法Object jobInstance = super.createJobInstance(bundle);//進行注入capableBeanFactory.autowireBean(jobInstance);return jobInstance;}
}

注冊調度工廠

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;import java.io.IOException;/*** @Author:sgw* @Date:2023/9/1* @Description:注冊調度工廠*/
@Configuration
public class QuartzConfig {@Autowiredprivate QuartzJobFactory jobFactory;@Beanpublic SchedulerFactoryBean schedulerFactoryBean() throws IOException {//獲取配置屬性PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));//在quartz.properties中的屬性被讀取并注入后再初始化對象propertiesFactoryBean.afterPropertiesSet();//創建SchedulerFactoryBeanSchedulerFactoryBean factory = new SchedulerFactoryBean();factory.setQuartzProperties(propertiesFactoryBean.getObject());factory.setJobFactory(jobFactory);//支持在JOB實例中注入其他的業務對象factory.setApplicationContextSchedulerContextKey("applicationContextKey");factory.setWaitForJobsToCompleteOnShutdown(true);//這樣當spring關閉時,會等待所有已經啟動的quartz job結束后spring才能完全shutdown。factory.setOverwriteExistingJobs(false);//是否覆蓋己存在的Jobfactory.setStartupDelay(10);//QuartzScheduler 延時啟動,應用啟動完后 QuartzScheduler 再啟動return factory;}/*** 通過SchedulerFactoryBean獲取Scheduler的實例* @return* @throws IOException* @throws SchedulerException*/@Bean(name = "scheduler")public Scheduler scheduler() throws IOException, SchedulerException {Scheduler scheduler = schedulerFactoryBean().getScheduler();return scheduler;}
}

重新設置 Quartz 數據連接池
默認 Quartz 的數據連接池是 c3p0,由于性能不太穩定,不推薦使用,因此我們將其改成driud數據連接池,配置如下:

import com.alibaba.druid.pool.DruidDataSource;
import org.quartz.SchedulerException;
import org.quartz.utils.ConnectionProvider;import java.sql.Connection;
import java.sql.SQLException;/*** @Author:sgw* @Date:2023/9/1* @Description: 重新設置 Quartz 數據連接池。默認 Quartz 的數據連接池是 c3p0,由于性能不太穩定,不推薦使用,因此我們將其改成driud數據連接池*/
public class DruidConnectionProvider implements ConnectionProvider {/*** 常量配置,與quartz.properties文件的key保持一致(去掉前綴),同時提供set方法,Quartz框架自動注入值。* @return* @throws SQLException*///JDBC驅動public String driver;//JDBC連接串public String URL;//數據庫用戶名public String user;//數據庫用戶密碼public String password;//數據庫最大連接數public int maxConnection;//數據庫SQL查詢每次連接返回執行到連接池,以確保它仍然是有效的。public String validationQuery;private boolean validateOnCheckout;private int idleConnectionValidationSeconds;public String maxCachedStatementsPerConnection;private String discardIdleConnectionsSeconds;public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120;//Druid連接池private DruidDataSource datasource;@Overridepublic Connection getConnection() throws SQLException {return datasource.getConnection();}@Overridepublic void shutdown() throws SQLException {datasource.close();}@Overridepublic void initialize() throws SQLException {if (this.URL == null) {throw new SQLException("DBPool could not be created: DB URL cannot be null");}if (this.driver == null) {throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");}if (this.maxConnection < 0) {throw new SQLException("DBPool maxConnectins could not be created: Max connections must be greater than zero!");}datasource = new DruidDataSource();try{datasource.setDriverClassName(this.driver);} catch (Exception e) {try {throw new SchedulerException("Problem setting driver class name on datasource: " + e.getMessage(), e);} catch (SchedulerException e1) {}}datasource.setUrl(this.URL);datasource.setUsername(this.user);datasource.setPassword(this.password);datasource.setMaxActive(this.maxConnection);datasource.setMinIdle(1);datasource.setMaxWait(0);datasource.setMaxPoolPreparedStatementPerConnectionSize(DEFAULT_DB_MAX_CONNECTIONS);if (this.validationQuery != null) {datasource.setValidationQuery(this.validationQuery);if(!this.validateOnCheckout)datasource.setTestOnReturn(true);elsedatasource.setTestOnBorrow(true);datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);}}public String getDriver() {return driver;}public void setDriver(String driver) {this.driver = driver;}public String getURL() {return URL;}public void setURL(String URL) {this.URL = URL;}public String getUser() {return user;}public void setUser(String user) {this.user = user;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public int getMaxConnection() {return maxConnection;}public void setMaxConnection(int maxConnection) {this.maxConnection = maxConnection;}public String getValidationQuery() {return validationQuery;}public void setValidationQuery(String validationQuery) {this.validationQuery = validationQuery;}public boolean isValidateOnCheckout() {return validateOnCheckout;}public void setValidateOnCheckout(boolean validateOnCheckout) {this.validateOnCheckout = validateOnCheckout;}public int getIdleConnectionValidationSeconds() {return idleConnectionValidationSeconds;}public void setIdleConnectionValidationSeconds(int idleConnectionValidationSeconds) {this.idleConnectionValidationSeconds = idleConnectionValidationSeconds;}public DruidDataSource getDatasource() {return datasource;}public void setDatasource(DruidDataSource datasource) {this.datasource = datasource;}public String getDiscardIdleConnectionsSeconds() {return discardIdleConnectionsSeconds;}public void setDiscardIdleConnectionsSeconds(String discardIdleConnectionsSeconds) {this.discardIdleConnectionsSeconds = discardIdleConnectionsSeconds;}
}

創建完成之后,還需要在quartz.properties配置文件中設置一下即可!

#數據庫連接池,將其設置為druid
org.quartz.dataSource.qzDS.connectionProvider.class=com.ts.hjbz.quartz.DruidConnectionProvider

編寫 Job 具體任務類(不同的任務,需要定義不同的job類)

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.text.SimpleDateFormat;
import java.util.Date;/*** @Author:sgw* @Date:2023/9/1* @Description: 具體要執行的的job*/
public class TfCommandJob implements Job {private static final Logger log = LoggerFactory.getLogger(TfCommandJob.class);@Overridepublic void execute(JobExecutionContext context) {try {System.out.println("開始執行:"+context.getScheduler().getSchedulerInstanceId() + "--" + new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()));} catch (SchedulerException e) {log.error("任務執行失敗",e);}}
}

編寫 Quartz 服務層接口

import java.util.Map;public interface QuartzJobService {/*** 添加任務可以傳參數* @param clazzName* @param jobName* @param groupName* @param cronExp* @param param*/void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param);/*** 暫停任務* @param jobName* @param groupName*/void pauseJob(String jobName, String groupName);/*** 恢復任務* @param jobName* @param groupName*/void resumeJob(String jobName, String groupName);/*** 立即運行一次定時任務* @param jobName* @param groupName*/void runOnce(String jobName, String groupName);/*** 更新任務* @param jobName* @param groupName* @param cronExp* @param param*/void updateJob(String jobName, String groupName, String cronExp, Map<String, Object> param);/*** 刪除任務* @param jobName* @param groupName*/void deleteJob(String jobName, String groupName);/*** 啟動所有任務*/void startAllJobs();/*** 暫停所有任務*/void pauseAllJobs();/*** 恢復所有任務*/void resumeAllJobs();/*** 關閉所有任務*/void shutdownAllJobs();
}

對應的實現類QuartzJobServiceImpl如下:

import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Map;/*** @Author:sgw* @Date:2023/9/1* @Description:*/
@Service
public class QuartzJobServiceImpl implements QuartzJobService {private static final Logger log = LoggerFactory.getLogger(QuartzJobServiceImpl.class);@Autowiredprivate Scheduler scheduler;@Overridepublic void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param) {try {// 啟動調度器,默認初始化的時候已經啟動
//            scheduler.start();//構建job信息Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(clazzName);JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, groupName).build();//表達式調度構建器(即任務執行的時間)CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExp);//按新的cronExpression表達式構建一個新的triggerCronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, groupName).withSchedule(scheduleBuilder).build();//獲得JobDataMap,寫入數據if (param != null) {trigger.getJobDataMap().putAll(param);}scheduler.scheduleJob(jobDetail, trigger);} catch (Exception e) {log.error("創建任務失敗", e);}}@Overridepublic void pauseJob(String jobName, String groupName) {try {scheduler.pauseJob(JobKey.jobKey(jobName, groupName));} catch (SchedulerException e) {log.error("暫停任務失敗", e);}}@Overridepublic void resumeJob(String jobName, String groupName) {try {scheduler.resumeJob(JobKey.jobKey(jobName, groupName));} catch (SchedulerException e) {log.error("恢復任務失敗", e);}}@Overridepublic void runOnce(String jobName, String groupName) {try {scheduler.triggerJob(JobKey.jobKey(jobName, groupName));} catch (SchedulerException e) {log.error("立即運行一次定時任務失敗", e);}}@Overridepublic void updateJob(String jobName, String groupName, String cronExp, Map<String, Object> param) {try {TriggerKey triggerKey = TriggerKey.triggerKey(jobName, groupName);CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);if (cronExp != null) {// 表達式調度構建器CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExp);// 按新的cronExpression表達式重新構建triggertrigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();}//修改mapif (param != null) {trigger.getJobDataMap().putAll(param);}// 按新的trigger重新設置job執行scheduler.rescheduleJob(triggerKey, trigger);} catch (Exception e) {log.error("更新任務失敗", e);}}@Overridepublic void deleteJob(String jobName, String groupName) {try {//暫停、移除、刪除scheduler.pauseTrigger(TriggerKey.triggerKey(jobName, groupName));scheduler.unscheduleJob(TriggerKey.triggerKey(jobName, groupName));scheduler.deleteJob(JobKey.jobKey(jobName, groupName));} catch (Exception e) {log.error("刪除任務失敗", e);}}@Overridepublic void startAllJobs() {try {scheduler.start();} catch (Exception e) {log.error("開啟所有的任務失敗", e);}}@Overridepublic void pauseAllJobs() {try {scheduler.pauseAll();} catch (Exception e) {log.error("暫停所有任務失敗", e);}}@Overridepublic void resumeAllJobs() {try {scheduler.resumeAll();} catch (Exception e) {log.error("恢復所有任務失敗", e);}}@Overridepublic void shutdownAllJobs() {try {if (!scheduler.isShutdown()) {// 需謹慎操作關閉scheduler容器// scheduler生命周期結束,無法再 start() 啟動schedulerscheduler.shutdown(true);}} catch (Exception e) {log.error("關閉所有的任務失敗", e);}}
}

創建一個請求參數實體類

import lombok.Data;import java.io.Serializable;
import java.util.Map;/*** @Author:sgw* @Date:2023/9/1* @Description:*/
@Data
public class QuartzConfigDTO implements Serializable {private static final long serialVersionUID = 1L;/*** 任務名稱*/private String jobName;/*** 任務所屬組*/private String groupName;/*** 任務執行類*/private String jobClass;/*** 任務調度時間表達式*/private String cronExpression;/*** 附加參數*/private Map<String, Object> param;
}

編寫 contoller 服務

import com.ts.hjbz.quartz.QuartzConfigDTO;
import com.ts.hjbz.quartz.QuartzJobService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
/*** @Author:sgw* @Date:2023/9/1* @Description: 定時任務入口類*/
@RestController
@RequestMapping("/job")
public class QuartzController {private static final Logger log = LoggerFactory.getLogger(QuartzController.class);@Autowiredprivate QuartzJobService quartzJobService;/*** 添加新任務* @param configDTO* @return*/@RequestMapping("/addJob")public Object addJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.addJob(configDTO.getJobClass(), configDTO.getJobName(), configDTO.getGroupName(), configDTO.getCronExpression(), configDTO.getParam());return HttpStatus.OK;}/*** 暫停任務* @param configDTO* @return*/@RequestMapping("/pauseJob")public Object pauseJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.pauseJob(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 恢復任務* @param configDTO* @return*/@RequestMapping("/resumeJob")public Object resumeJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.resumeJob(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 立即運行一次定時任務* @param configDTO* @return*/@RequestMapping("/runOnce")public Object runOnce(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.runOnce(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 更新任務* @param configDTO* @return*/@RequestMapping("/updateJob")public Object updateJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.updateJob(configDTO.getJobName(), configDTO.getGroupName(), configDTO.getCronExpression(), configDTO.getParam());return HttpStatus.OK;}/*** 刪除任務* @param configDTO* @return*/@RequestMapping("/deleteJob")public Object deleteJob(@RequestBody QuartzConfigDTO configDTO) {quartzJobService.deleteJob(configDTO.getJobName(), configDTO.getGroupName());return HttpStatus.OK;}/*** 啟動所有任務* @return*/@RequestMapping("/startAllJobs")public Object startAllJobs() {quartzJobService.startAllJobs();return HttpStatus.OK;}/*** 暫停所有任務* @return*/@RequestMapping("/pauseAllJobs")public Object pauseAllJobs() {quartzJobService.pauseAllJobs();return HttpStatus.OK;}/*** 恢復所有任務* @return*/@RequestMapping("/resumeAllJobs")public Object resumeAllJobs() {quartzJobService.resumeAllJobs();return HttpStatus.OK;}/*** 關閉所有任務* @return*/@RequestMapping("/shutdownAllJobs")public Object shutdownAllJobs() {quartzJobService.shutdownAllJobs();return HttpStatus.OK;}
}

使用postman進行測試,新增一個定時任務,每隔五秒執行一次
在這里插入圖片描述
具體參數如下

{"jobName":"測試任務1","groupName":"組1","cronExpression":"0/5 * * * * ? ","jobClass":"com.ts.hjbz.quartz.TfCommandJob","param":{"hello":"hello啊"}
}

其中cronExpression,直接上網查詢在線cron表達式轉換即可,如https://www.bejson.com/othertools/cron/
在這里插入圖片描述
上圖是配置每5秒執行一次,生成的cron表達式就是我們postman里需要的cronExpression參數。執行postman的調用后,可以看到控制臺每隔5秒打印一次,如下

在這里插入圖片描述

并且服務重啟后,這個定時任務依然存在,依然會每隔5秒執行一次。

注冊監聽器(選用)

如果你想在 SpringBoot 里面集成 Quartz 的監聽器,操作也很簡單

創建任務調度監聽器

@Component
public class SimpleSchedulerListener extends SchedulerListenerSupport {@Overridepublic void jobScheduled(Trigger trigger) {System.out.println("任務被部署時被執行");}@Overridepublic void jobUnscheduled(TriggerKey triggerKey) {System.out.println("任務被卸載時被執行");}@Overridepublic void triggerFinalized(Trigger trigger) {System.out.println("任務完成了它的使命,光榮退休時被執行");}@Overridepublic void triggerPaused(TriggerKey triggerKey) {System.out.println(triggerKey + "(一個觸發器)被暫停時被執行");}@Overridepublic void triggersPaused(String triggerGroup) {System.out.println(triggerGroup + "所在組的全部觸發器被停止時被執行");}@Overridepublic void triggerResumed(TriggerKey triggerKey) {System.out.println(triggerKey + "(一個觸發器)被恢復時被執行");}@Overridepublic void triggersResumed(String triggerGroup) {System.out.println(triggerGroup + "所在組的全部觸發器被回復時被執行");}@Overridepublic void jobAdded(JobDetail jobDetail) {System.out.println("一個JobDetail被動態添加進來");}@Overridepublic void jobDeleted(JobKey jobKey) {System.out.println(jobKey + "被刪除時被執行");}@Overridepublic void jobPaused(JobKey jobKey) {System.out.println(jobKey + "被暫停時被執行");}@Overridepublic void jobsPaused(String jobGroup) {System.out.println(jobGroup + "(一組任務)被暫停時被執行");}@Overridepublic void jobResumed(JobKey jobKey) {System.out.println(jobKey + "被恢復時被執行");}@Overridepublic void jobsResumed(String jobGroup) {System.out.println(jobGroup + "(一組任務)被恢復時被執行");}@Overridepublic void schedulerError(String msg, SchedulerException cause) {System.out.println("出現異常" + msg + "時被執行");cause.printStackTrace();}@Overridepublic void schedulerInStandbyMode() {System.out.println("scheduler被設為standBy等候模式時被執行");}@Overridepublic void schedulerStarted() {System.out.println("scheduler啟動時被執行");}@Overridepublic void schedulerStarting() {System.out.println("scheduler正在啟動時被執行");}@Overridepublic void schedulerShutdown() {System.out.println("scheduler關閉時被執行");}@Overridepublic void schedulerShuttingdown() {System.out.println("scheduler正在關閉時被執行");}@Overridepublic void schedulingDataCleared() {System.out.println("scheduler中所有數據包括jobs, triggers和calendars都被清空時被執行");}
}

創建任務觸發監聽器

@Component
public class SimpleTriggerListener extends TriggerListenerSupport {/*** Trigger監聽器的名稱* @return*/@Overridepublic String getName() {return "mySimpleTriggerListener";}/*** Trigger被激發 它關聯的job即將被運行* @param trigger* @param context*/@Overridepublic void triggerFired(Trigger trigger, JobExecutionContext context) {System.out.println("myTriggerListener.triggerFired()");}/*** Trigger被激發 它關聯的job即將被運行, TriggerListener 給了一個選擇去否決 Job 的執行,如果返回TRUE 那么任務job會被終止* @param trigger* @param context* @return*/@Overridepublic boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {System.out.println("myTriggerListener.vetoJobExecution()");return false;}/*** 當Trigger錯過被激發時執行,比如當前時間有很多觸發器都需要執行,但是線程池中的有效線程都在工作,* 那么有的觸發器就有可能超時,錯過這一輪的觸發。* @param trigger*/@Overridepublic void triggerMisfired(Trigger trigger) {System.out.println("myTriggerListener.triggerMisfired()");}/*** 任務完成時觸發* @param trigger* @param context* @param triggerInstructionCode*/@Overridepublic void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {System.out.println("myTriggerListener.triggerComplete()");}
}

創建任務執行監聽器

@Component
public class SimpleJobListener extends JobListenerSupport {/*** job監聽器名稱* @return*/@Overridepublic String getName() {return "mySimpleJobListener";}/*** 任務被調度前* @param context*/@Overridepublic void jobToBeExecuted(JobExecutionContext context) {System.out.println("simpleJobListener監聽器,準備執行:"+context.getJobDetail().getKey());}/*** 任務調度被拒了* @param context*/@Overridepublic void jobExecutionVetoed(JobExecutionContext context) {System.out.println("simpleJobListener監聽器,取消執行:"+context.getJobDetail().getKey());}/*** 任務被調度后* @param context* @param jobException*/@Overridepublic void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {System.out.println("simpleJobListener監聽器,執行結束:"+context.getJobDetail().getKey());}
}

最后,在QuartzConfig中將監聽器注冊到Scheduler

@Autowired
private SimpleSchedulerListener simpleSchedulerListener;@Autowired
private SimpleJobListener simpleJobListener;@Autowired
private SimpleTriggerListener simpleTriggerListener;@Bean(name = "scheduler")
public Scheduler scheduler() throws IOException, SchedulerException {Scheduler scheduler = schedulerFactoryBean().getScheduler();//全局添加監聽器//添加SchedulerListener監聽器scheduler.getListenerManager().addSchedulerListener(simpleSchedulerListener);// 添加JobListener, 支持帶條件匹配監聽器scheduler.getListenerManager().addJobListener(simpleJobListener, KeyMatcher.keyEquals(JobKey.jobKey("myJob", "myGroup")));// 添加triggerListener,設置全局監聽scheduler.getListenerManager().addTriggerListener(simpleTriggerListener, EverythingMatcher.allTriggers());return scheduler;
}

采用項目數據源(選用)
在上面的 Quartz 數據源配置中,我們使用了自定義的數據源,目的是和項目中的數據源實現解耦,當然有的同學不想單獨建庫,想和項目中數據源保持一致,配置也很簡單!
quartz.properties配置文件中,去掉org.quartz.jobStore.dataSource配置,即

#注釋掉quartz的數據源配置
#org.quartz.jobStore.dataSource=qzDS

在QuartzConfig配置類中加入dataSource數據源,并將其注入到quartz中

@Autowired
private DataSource dataSource;@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {//...SchedulerFactoryBean factory = new SchedulerFactoryBean();factory.setQuartzProperties(propertiesFactoryBean.getObject());//使用數據源,自定義數據源factory.setDataSource(dataSource);//...return factory;
}

2.2 SpringBoot集群版-整合Quartz代碼實戰

Quartz 提供了極為廣用的特性,如任務持久化、集群部署和分布式調度任務等等,正因如此,基于 Quartz 任務調度功能在系統開發中應用極為廣泛!

在集群環境下,Quartz 集群中的每個節點是一個獨立的 Quartz 應用,沒有負責集中管理的節點,而是通過數據庫表來感知另一個應用,利用數據庫鎖的方式來實現集群環境下進行并發控制,每個任務當前運行的有效節點有且只有一個!

特別需要注意的是:分布式部署時需要保證各個節點的系統時間一致!

在實際的部署中,項目都是集群進行部署,因此為了和正式環境一致,我們再新建兩個相同的項目來測試一下在集群環境下 quartz 是否可以實現分布式調度,保證任何一個定時任務只有一臺機器在運行?理論上,我們只需要將剛剛新建好的項目,重新復制一份,然后修改一下端口號就可以實現本地測試!

因為curd服務只需要一個,因此我們在新的服務里,不需要再編寫QuartzJobService等增、刪、改服務,僅僅保持QuartzConfig、DruidConnectionProvider、QuartzJobFactory、TfCommandJob、quartz.properties類和配置都是相同的就可以了!

依次啟動服務quartz-001、quartz-002、quartz-003,看看效果如何

第一個啟動的服務quartz-001會優先加載數據庫中已經配置好的定時任務,其他兩個服務quartz-002、quartz-003都沒有主動調度服務;

當我們主動關閉quartz-001時,quartz-002服務主動接收任務調度

當我們主動關閉quartz-002,同樣quartz-003服務主動接收任務調度

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91076.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91076.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91076.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java中的異常判斷以及文件中的常用方法及功能

目錄 異常 作用 異常的處理方式 JVM&#xff08;虛擬機&#xff09;默認的處理方式 自己處理&#xff08;捕獲異常&#xff09; 拋出異常&#xff08;也就是交給調用者處理&#xff09; 自定義異常 file File中常見成員方法 判斷和獲取 創建和刪除 獲取并遍歷 異常…

【C++算法】74.優先級隊列_最后一塊石頭的重量

文章目錄題目鏈接&#xff1a;題目描述&#xff1a;解法C 算法代碼&#xff1a;題目鏈接&#xff1a; 1046. 最后一塊石頭的重量 題目描述&#xff1a; 解法 每次取出最重的兩塊石頭進行碰撞&#xff0c;將剩余的石頭重新放入堆中。 C 算法代碼&#xff1a; class Solution …

中興云電腦W101D2-晶晨S905L3A-2G+8G-安卓9-線刷固件包

中興云電腦W101D2-晶晨S905L3A-2G8G-WIFI-藍牙5.0-3個USB2.0-線刷包線刷方法&#xff1a;1、準備好一根雙公頭USB線刷刷機線&#xff0c;長度30-50CM長度最佳&#xff0c;同時準備一臺電腦&#xff1b;2、電腦上安裝好刷機工具Amlogic USB Burning Tool 軟件 →打開軟件 →文件…

Android OkHttp 底層原理和實戰完全教程(責任鏈模式詳解)

目錄 1. OkHttp 入門:從一個請求開始 1.1 基本 GET 請求:三步走 1.2 同步 vs 異步:選擇你的風格 1.3 為什么選 OkHttp? 2. 配置 OkHttpClient:打造你的專屬網絡引擎 2.1 超時設置:別讓請求卡死 2.2 添加攔截器:窺探請求全過程 2.3 緩存:讓請求更快更省流量 3. …

【RK3588部署yolo】算法篇

簡歷描述收集并制作軍事偽裝目標數據集&#xff0c;包含真實與偽裝各種類型軍事目標共計60余類。其中&#xff0c;包含最新戰場充氣偽裝軍事裝備30余類&#xff0c;并為每一張圖片制作了詳細的標注。針對軍事偽裝目標的特點&#xff0c;在YOLOv8的Backbone與Neck部分分別加…

【Spring Boot 快速入門】一、入門

目錄Spring Boot 簡介Web 入門Spring Boot 快速入門HTTP 協議概述請求協議響應協議解析協議TomcatSpring Boot 簡介 Spring Boot 是由 Pivotal 團隊&#xff08;后被 VMware 收購&#xff09;開發的基于 Spring 框架的開源項目&#xff0c;于 2014 年首次發布。其核心目標是簡…

如何調整服務器的內核參數?-哈爾濱云前沿

調整服務器內核參數是一項較為專業的操作&#xff0c;不同的操作系統調整方式略有不同&#xff0c;以下以常見的 Linux 系統為例&#xff0c;介紹一些調整服務器內核參數的一般步驟和常用參數&#xff1a;一般步驟 備份當前配置&#xff1a;在修改內核參數之前&#xff0c;先備…

C++基礎:模擬實現queue和stack。底層:適配器

引言模擬實現queue和stack&#xff0c;理解適配器&#xff0c;實現起來非常簡單。一、適配器 適配器是一種能讓原本不兼容的接口協同工作的設計模式或者組件。它的主要作用是對一個類的接口進行轉換&#xff0c;使其符合另一個類的期望接口&#xff0c;進而實現適配和復用。&am…

OI 雜題

OI 雜題字符串括號匹配例 1&#xff1a;與之前的類似&#xff0c;就是講一點技巧&#xff0c;但是比較亂&#xff0c;湊合著看吧。 字符串 括號匹配 幾何意義&#xff1a;考慮令 ( 為 111 變換&#xff0c;令 ) 為 ?1-1?1 變換&#xff0c;然后對這個 1/?11/-11/?1 構成…

【論文閱讀】Safety Alignment Should Be Made More Than Just a Few Tokens Deep

Safety Alignment Should Be Made More Than Just a Few Tokens Deep原文摘要問題提出現狀與漏洞&#xff1a;當前LLMs的安全對齊機制容易被攻破&#xff0c;即使是簡單的攻擊&#xff08;如對抗性后綴攻擊&#xff09;或良性的微調也可能導致模型越獄。核心論點&#xff1a; 作…

Generative AI in Game Development

如有侵權或其他問題&#xff0c;歡迎留言聯系更正或刪除。 出處&#xff1a;CHI 20241. 一段話總結本研究通過對來自 Reddit 和 Facebook 群組的 3,091 條獨立游戲開發者的在線帖子和評論進行定性分析&#xff0c;探討了他們對生成式 AI在游戲開發中多方面作用的認知與設想。研…

【C++算法】72.隊列+寬搜_二叉樹的最大寬度

文章目錄題目鏈接&#xff1a;題目描述&#xff1a;解法C 算法代碼&#xff1a;題目鏈接&#xff1a; 662. 二叉樹最大寬度 題目描述&#xff1a; 解法 這里的寬度指的是一層的最右邊的非空節點到一層的最左邊的非空節點&#xff0c;一共的節點數。 解法一&#xff1a;硬來&am…

什么是3DVR?VR技術有哪些應用場景?

VR與3D技術解析及應用在高科技領域&#xff0c;VR和3D是兩個常被提及的名詞。那么&#xff0c;這兩者之間究竟存在著怎樣的區別與聯系呢&#xff1f;簡而來說&#xff0c;VR技術是3D技術的一種高級延展和深化應用。3D技術&#xff0c;即將二維設計圖轉化為立體、逼真的視覺效果…

棧與隊列:數據結構核心解密

棧和隊列的基本 棧(Stack)是一種后進先出(LIFO, Last In First Out)的數據結構。元素的插入和刪除操作只能在棧頂進行。常見的操作包括壓棧(push)和彈棧(pop)。 隊列(Queue)是一種先進先出(FIFO, First In First Out)的數據結構。元素的插入在隊尾進行,刪除在隊…

《C++初階之STL》【list容器:詳解 + 實現】

【list容器&#xff1a;詳解 實現】目錄前言------------標準接口介紹------------標準模板庫中的list容器是什么樣的呢&#xff1f;1. 常見的構造2. 迭代器操作std::list::beginstd::list::endstd::list::rbeginstd::list::rend3. 容量的操作std::list::sizestd::list::empty…

【灰度實驗】——圖像預處理(OpenCV)

目錄 1 灰度圖 2 最大值法 3 平均值法 4 加權均值法 5 兩個極端的灰度值 將彩色圖轉為灰度圖地過程稱為灰度化。 灰度圖是單通道圖像&#xff0c;灰度化本質就是將彩色圖的三通道合并成一個通道的過程。三種合并方法&#xff1a;最大值法&#xff0c;平均值法和加權均值法…

【linux驅動開發】編譯linux驅動程序報錯:ERROR: Kernel configuration is invalid.

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄一、報錯二、解決方法1.先編譯linux內核源碼2.再重新編譯驅動程序一、報錯 在編譯驅動程序過程中&#xff0c;經常碰到的一個小問題&#xff1a; make -C /home/lu…

Java面試寶典:MySQL中的鎖

InnoDB中鎖的類型非常多,總體上可以如下分類: 這些鎖都是做什么的?具體含義是什么?我們現在來一一學習。 1. 解決并發事務問題 我們已經知道事務并發執行時可能帶來的各種問題。最大的一個難點是:一方面要最大程度地利用數據庫的并發訪問能力,另一方面又要確保每個用戶…

設備識別最佳實踐:四維交叉驗證框架

設備識別最佳實踐&#xff1a;四維交叉驗證框架 1. MAC地址分析&#xff08;40%權重&#xff09; - 設備身份核驗 核心方法&#xff1a; # MAC地址標準化&#xff08;OUI提取&#xff09; mac"B4:2E:99:FB:9D:78" oui$(echo $mac | tr -d : | cut -c 1-6 | tr a-f A-…

《Java 程序設計》第 9 章 - 內部類、枚舉和注解

大家好&#xff0c;今天我們來學習《Java 程序設計》第 9 章的內容 —— 內部類、枚舉和注解。這三個知識點是 Java 中提升代碼靈活性和可讀性的重要工具&#xff0c;在實際開發中非常常用。接下來我們逐一展開講解&#xff0c;每個知識點都會配上可直接運行的代碼示例&#xf…