一 定時任務介紹
自律是很多人都想擁有的一種能力,或者說素質,但是理想往往很美好,現實卻是無比殘酷的。在現實生活中,我們很難做到自律,或者說做到持續自律。例如,我們經常會做各種學習計劃、儲蓄計劃或減肥計劃等,但無一例外地被各種“意外”打破。這往往使得我們非常沮喪,甚至開始懷疑人生。
但是有一個“家伙”在自律方面做得格外出色。它只要制訂了計劃就會嚴格地執行,而且無論一個任務重復多少遍都不厭其煩,簡直自律到“令人發指”,它就是定時任務。
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服務主動接收任務調度