Scheduled
只適合處理簡單的計划任務,不能處理分布式計划任務。優勢:是spring框架提供的計划任務,開發簡單,執行效率比較高。且在計划任務數量太多的時候,可能出現阻塞,崩潰,延遲啟動等問題。
Scheduled定時任務是spring3.0版本之后自帶的一個定時任務。其所屬Spring的資源包為:spring-context-support。所以需要使用Scheduled定時任務機制時,需要在工程中依賴對應資源,具體如下:
<!-- scheduled所屬資源為spring-context-support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency>
如果在spring應用中需要啟用Scheduled定時任務,則需要在啟動類上增加注解@EnableScheduling,代表啟用Scheduled定時任務機制。具體如下:
@SpringBootApplication @EnableScheduling public class AppStarter { public static void main(String[] args) { SpringApplication.run(AppStarter.class, args); } }
Scheduled定時任務的核心在於注解@Scheduled,這個注解的核心屬性是cron,代表定時任務的觸發計划表達式。這個表達式的格式為:
@Scheduled(cron="seconds minutes hours day month week")
或
@Scheduled(cron="seconds minutes hours day month week year")
推薦使用第一種表達式形式,因為在很多其他技術中都有不同的定時任務機制,其中用於設置觸發計划的表達式都是第一種cron表達式。第二種表達式不能說是Spring Scheduled特有的,也是只有少數技術支持的。
cron表達式中,每個位置的約束如下:
星號(*):可用在所有字段中,表示對應時間域的每一個時刻,例如,*在分鍾字段時,表示“每分鍾”;
問號(?):該字符只在日期和星期字段中使用,它通常指定為“無意義的值”,相當於占位符;
減號(-):表達一個范圍,如在小時字段中使用“10-12”,則表示從10到12點,即10,11,12;
逗號(,):表達一個列表值,如在星期字段中使用“MON,WED,FRI”,則表示星期一,星期三和星期五;
斜杠(/):x/y表達一個等步長序列,x為起始值,y為增量步長值。如在秒數字段中使用0/15,則表示為0,15,30和45秒,而5/15在分鍾字段中表示5,20,35,50,你也可以使用*/y,它等同於0/y;
L:該字符只在日期和星期字段中使用,代表“Last”的意思,但它在兩個字段中意思不同。L在日期字段中,表示這個月份的最后一天,如一月的31號,非閏年二月的28號;如果L用在星期中,則表示星期六,等同於7。但是,如果L出現在星期字段里,而且在前面有一個數值X,則表示“這個月的最后X天”,例如,6L表示該月的最后星期五;
W:該字符只能出現在日期字段里,是對前導日期的修飾,表示離該日期最近的工作日。例如15W表示離該月15號最近的工作日,如果該月15號是星期六,則匹配14號星期五;如果15日是星期日,則匹配16號星期一;如果15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不能夠跨月,如你指定1W,如果1號是星期六,結果匹配的是3號星期一,而非上個月最后的那天。W字符串只能指定單一日期,而不能指定日期范圍;
LW組合:在日期字段可以組合使用LW,它的意思是當月的最后一個工作日;
井號(#):該字符只能在星期字段中使用,表示當月某個工作日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;
C:該字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是計划所關聯的日期,如果日期沒有被關聯,則相當於日歷中所有日期。例如5C在日期字段中就相當於日歷5日以后的第一天。1C在星期字段中相當於星期日后的第一天。
Cron表達式對特殊字符的大小寫不敏感,對代表星期的縮寫英文大小寫也不敏感。
計划任務Scheduled是通過一個線程池實現的。是一個多線程的調度。SpringBoot會初始化一個線程池,線程池默認大小為1,專門用於執行計划任務。每個計划任務啟動的時候,都從線程池中獲取一個線程執行,如果發生異常,只是執行當前任務的線程發生異常,而不是計划任務調度線程發生異常。如果當前定時任務還未執行完成,當相同的定時任務又進入到執行周期時,不會觸發新的定時任務。如:
@Scheduled(cron="* * * * * ?") public void test1(){ Random r = new Random(); /*int i = r.nextInt(100); if(i % 3 == 0){ throw new RuntimeException("error"); }*/ System.out.println(Thread.currentThread().getName() + " cron=* * * * * ? --- " + new Date()); try{ Thread.sleep(2000); }catch(Exception e){ e.printStackTrace(); } }
如結果所示(每次的線程名稱一致,由於前一個定時任務未執行完成,因此造成后一個任務的推遲,而不是1秒執行一次,而是3秒):
pool-1-thread-1 cron=* * * * * ? --- Thu Sep 19 02:23:20 CST 2019 pool-1-thread-1 cron=* * * * * ? --- Thu Sep 19 02:23:23 CST 2019 pool-1-thread-1 cron=* * * * * ? --- Thu Sep 19 02:23:26 CST 2019 pool-1-thread-1 cron=* * * * * ? --- Thu Sep 19 02:23:29 CST 2019
quartz
Quartz是OpenSymphony開源組織在Job scheduling領域又一個開源項目,它可以與J2EE與J2SE應用程序相結合也可以單獨使用。Quartz可以用來創建簡單或為運行十個,百個,甚至是好幾萬個Jobs這樣復雜的程序。
Quartz是一個完全由java編寫的開源作業調度框架。不要讓作業調度這個術語嚇着你。盡管Quartz框架整合了許多額外功能, 但就其簡易形式看,你會發現它易用得簡直讓人受不了!
在開發Quartz相關應用時,只要定義了Job(任務),Trigger(觸發器)和Scheduler(調度器),即可實現一個定時調度能力。其中Scheduler是Quartz中的核心,Scheduler負責管理Quartz應用運行時環境,Scheduler不是靠自己完成所有的工作,是根據Trigger的觸發標准,調用Job中的任務執行邏輯,來完成完整的定時任務調度。
Job - 定時任務內容是什么。
Trigger - 在什么時間上執行job。
Scheduler - 維護定時任務環境,並讓觸發器生效。
在SpringBoot中應用Quartz,需要依賴下述資源:
<dependencies> <!-- scheduled所屬資源為spring-context-support,在Spring中對Quartz的支持,是集成在spring-context-support包中。 org.springframework.scheduling.quartz --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!-- Quartz坐標 --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> <!-- Quartz默認需要slf4j支持。springboot中,提供了更高版本的slf4j --> <exclusions> <exclusion> <artifactId>slf4j-api</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <!-- Spring tx 坐標,quartz可以提供分布式定時任務環境。多個分布點上的Quartz任務,是通過數據庫實現任務信息傳遞的。 通過數據庫中的數據,保證一個時間點上,只有一個分布環境執行定時任務。 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> </dependencies>
啟動器添加注解@EnableScheduling:
/** * @EnableScheduling 必要 * 開啟定時任務機制。 */ @SpringBootApplication @EnableScheduling public class AppStarter { public static void main(String[] args) { SpringApplication.run(AppStarter.class, args); } }
定義JOB任務以及JOB任務調用的模擬業務對象:
1 public class SpringBootQuartzJobDemo implements Job { 2 3 // 用於模擬任務中的業務對象。 也可能是數據訪問對象,或其他類型的對象。 4 @Autowired 5 private CommonsUtil4Quartz commonsUtil4Quartz; 6 7 @Override 8 public void execute(JobExecutionContext context) throws JobExecutionException { 9 System.out.println("SpringBootQuartzJobDemo : " + new Date()); 10 this.commonsUtil4Quartz.testMethod(); 11 } 12 13 }
@Component public class CommonsUtil4Quartz { public void testMethod(){ System.out.println("CommonsUtil4Quartz testMethod run..."); } }
創建Trigger以及JobDetail對象,並用Schedule配置定時任務:
/** * 初始化類 * Quartz環境初始化。 * */ @Configuration public class QuartzConfiguration { /** * 創建Job對象。在Spring環境中,創建一個類型的對象的時候,很多情況下,都是通過FactoryBean來間接創建的。 * 如果有多個Job對象,定義多次方法。 * * 在JobDetailFactoryBean類型中,用於創建JobDetail對象的方法,其底層使用的邏輯是:Class.newInstance() * 也就是說,JobDetail對象不是通過Spring容器管理的。 * 因為Spring容器不管理JobDetail對象,那么Job中需要自動裝配的屬性,就無法實現自動狀態。如上JOB的第10行會報空指針異常。 * * 解決方案是: 將JobDetail加入到Spring容器中,讓Spring容器管理JobDetail對象。 * 需要重寫Factory相關代碼。實現Spring容器管理JobDetail。 * @return */ @Bean public JobDetailFactoryBean initJobDetailFactoryBean(){ JobDetailFactoryBean factoryBean = new JobDetailFactoryBean(); // 提供job類型。 factoryBean.setJobClass(SpringBootQuartzJobDemo.class); return factoryBean; } /** * 管理Trigger對象 * CronTrigger - 就是Trigger的一個實現類型。 其中用於定義周期時間的是CronSchedulerBuilder * 實際上,CronTrigger是用於管理一個Cron表達式的類型。 * @param jobDetailFactoryBean - 上一個方法初始化的JobDetailFactoryBean * @return */ @Bean(name="cronTriggerFactoryBean1") public CronTriggerFactoryBean initCronTriggerFactoryBean( ){ CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean(); JobDetailFactoryBean jobDetailFactoryBean = this.initJobDetailFactoryBean(); factoryBean.setJobDetail(jobDetailFactoryBean.getObject()); factoryBean.setCronExpression("0/3 * * * * ?"); return factoryBean; } /** * 初始化Scheduler * @param cronTriggerFactoryBean - 上一個方法初始化的CronTriggerFactoryBean * @return */ @Bean public SchedulerFactoryBean initSchedulerFactoryBean( CustomJobFactory customJobFactory, CronTriggerFactoryBean[] cronTriggerFactoryBean){ SchedulerFactoryBean factoryBean = new SchedulerFactoryBean(); CronTrigger[] triggers = new CronTrigger[cronTriggerFactoryBean.length]; for(int i = 0; i < cronTriggerFactoryBean.length; i++){ triggers[i] = cronTriggerFactoryBean[i].getObject(); } // 注冊觸發器,一個Scheduler可以注冊若干觸發器。 factoryBean.setTriggers(triggers); // 為Scheduler設置JobDetail的工廠。可以覆蓋掉SpringBoot提供的默認工廠,保證JobDetail中的自動裝配有效。 factoryBean.setJobFactory(customJobFactory); return factoryBean; } }
重寫JobFactory:
/** * 重寫的工廠對象。 */ @Component public class CustomJobFactory extends AdaptableJobFactory { /** * AutowireCapableBeanFactory : 簡單理解為Spring容器,是Spring容器Context的一個Bean對象管理工程。 * 可以實現自動裝配邏輯,和對象創建邏輯。 * 是SpringIoC容器的一個重要組成部件。 */ @Autowired private AutowireCapableBeanFactory autowireCapableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 通過父類型中的方法,創建JobDetail對象。 Object obj = super.createJobInstance(bundle); // 將JobDetail對象加入到Spring容器中,讓Spring容器管理,並實現自動裝配邏輯。 this.autowireCapableBeanFactory.autowireBean(obj); return obj; } }
分布式quartz配置
1、資源依賴配置:由於分布式的原因,Quartz中提供分布式處理的jar包以及數據庫及連接相關的依賴。
<dependencies> <!-- scheduled所屬資源為spring-context-support --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!-- Quartz坐標 --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> <exclusions> <exclusion> <artifactId>slf4j-api</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <!-- 是Quartz中提供分布式處理的jar包。 --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.2.1</version> </dependency> <!-- Sprng tx 坐標 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> <!-- web啟動器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--jdbc --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- druid數據庫連接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.9</version> </dependency> </dependencies>
2、提供數據庫相關配置:
spring.datasource.url=jdbc:mysql://localhost:3306/quartz?autoReconnect=true&useUnicode=true&characterEncoding=utf-8 spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.initialSize=5 spring.datasource.minIdle=5 spring.datasource.maxActive=20 spring.datasource.maxWait=600000 spring.datasource.timeBetweenEvictionRunsMillis=600000 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=SELECT 1 spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false server.port=8080
3、提供Quartz配置信息,為Quartz提供數據庫配置信息,如數據庫,表格的前綴之類的。
# 是否使用properties作為數據存儲 org.quartz.jobStore.useProperties=false # 數據庫中的表格命名前綴 org.quartz.jobStore.tablePrefix = QRTZ_ # 是否是一個集群,是不是分布式的任務 org.quartz.jobStore.isClustered = true # 集群檢查周期,單位毫秒。可以自定義縮短時間。 當某一個節點宕機的時候,其他節點等待多久后開始執行任務。 org.quartz.jobStore.clusterCheckinInterval = 5000 # 單位毫秒, 集群中的節點退出后,再次檢查進入的時間間隔。 org.quartz.jobStore.misfireThreshold = 60000 # 事務隔離級別 org.quartz.jobStore.txIsolationLevelReadCommitted = true # 存儲的事務管理類型 org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX # 使用的Delegate類型 org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 集群的命名,一個集群要有相同的命名。 org.quartz.scheduler.instanceName = ClusterQuartz # 節點的命名,可以自定義。 AUTO代表自動生成。 org.quartz.scheduler.instanceId= AUTO # rmi遠程協議是否發布 org.quartz.scheduler.rmi.export = false # rmi遠程協議代理是否創建 org.quartz.scheduler.rmi.proxy = false # 是否使用用戶控制的事務環境觸發執行job。 org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
4、初始化數據庫
建表語句可以自己在官方網站中查找(Quartz-lib中),使用tables-mysql.sql建表。
5、定義JOB類
啟動器和普通quartz無差異,但是JOB自身定義有些許差異:
/** * 使用Spring提供的Quartz相關Job類型實現Job的定義。 * 父類型QuartzJobBean中,提供了分布式環境中任務的配置定義。 * 保證分布式環境中的任務是有效的。 */ @PersistJobDataAfterExecution // 當job執行結束,持久化job信息到數據庫 @DisallowConcurrentExecution // 保證job的唯一性(單例) public class SpringBootQuartzJobDemo extends QuartzJobBean { @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { System.out.println("SpringBootQuartzJobDemo : " + new Date()); } }
6、QuartzConfiguration類型定義
@Configuration public class QuartzConfiguration { @Autowired private DataSource dataSource; /** * 創建調度器, 可以省略的。 * @return * @throws Exception */ @Bean public Scheduler scheduler() throws Exception { Scheduler scheduler = schedulerFactoryBean().getScheduler(); scheduler.start(); return scheduler; } /** * 創建調度器工廠bean對象。 * @return * @throws IOException */ @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setSchedulerName("Cluster_Scheduler"); factory.setDataSource(dataSource); factory.setApplicationContextSchedulerContextKey("applicationContext"); // 設置調度器中的線程池。 factory.setTaskExecutor(schedulerThreadPool()); // 設置觸發器 factory.setTriggers(trigger().getObject()); // 設置quartz的配置信息 factory.setQuartzProperties(quartzProperties()); return factory; } /** * 讀取quartz.properties配置文件的方法。 * @return * @throws IOException */ @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties")); // 在quartz.properties中的屬性被讀取並注入后再初始化對象 propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); } /** * 創建Job對象的方法。 * @return */ @Bean public JobDetailFactoryBean job() { JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean(); jobDetailFactoryBean.setJobClass(SpringBootQuartzJobDemo.class); // 是否持久化job內容 jobDetailFactoryBean.setDurability(true); // 設置是否多次請求嘗試任務。 jobDetailFactoryBean.setRequestsRecovery(true); return jobDetailFactoryBean; } /** * 創建trigger factory bean對象。 * @return */ @Bean public CronTriggerFactoryBean trigger() { CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean(); cronTriggerFactoryBean.setJobDetail(job().getObject()); cronTriggerFactoryBean.setCronExpression("0/2 * * * * ?"); return cronTriggerFactoryBean; } /** * 創建一個調度器的線程池。 * @return */ @Bean public Executor schedulerThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(15); executor.setMaxPoolSize(25); executor.setQueueCapacity(100); return executor; } }
若JOB任務有定義調用業務等內容,也需要重寫JobFactory,如上述常規quartz,此處不再贅述。