Spring Boot支持Crontab任務(wù)改造的方法
在以往的 Tomcat 項(xiàng)目中,一直習(xí)慣用 Ant 打包,使用 build.xml 配置,通過 ant -buildfile 的方式在機(jī)器上執(zhí)行定時(shí)任務(wù)。雖然 Spring 本身支持定時(shí)任務(wù),但都是服務(wù)一直運(yùn)行時(shí)支持。其實(shí)在項(xiàng)目中,大多數(shù)定時(shí)任務(wù),還是借助 Linux Crontab 來支持,需要時(shí)運(yùn)行即可,不需要一直占用機(jī)器資源。但 Spring Boot 項(xiàng)目或者普通的 jar 項(xiàng)目,就沒這么方便了。
Spring Boot 提供了類似 CommandLineRunner 的方式,很好的執(zhí)行常駐任務(wù);也可以借助 ApplicationListener 和 ContextRefreshedEvent 等事件來做很多事情。借助該容器事件,一樣可以做到類似 Ant 運(yùn)行的方式來運(yùn)行定時(shí)任務(wù),當(dāng)然需要做一些項(xiàng)目改動(dòng)。
1. 監(jiān)聽目標(biāo)對(duì)象
借助容器刷新事件來監(jiān)聽目標(biāo)對(duì)象即可,可以認(rèn)為,定時(shí)任務(wù)其實(shí)每次只是執(zhí)行一種操作而已。
比如這是一個(gè)寫好的例子,注意不要直接用 @Service 將其放入容器中,除非容器本身沒有其它自動(dòng)運(yùn)行的事件。
package com.github.zhgxun.learn.common.task;
import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 不自動(dòng)加入容器, 用于區(qū)分是否屬于任務(wù)啟動(dòng), 否則放入容器中, Spring 無法選擇性執(zhí)行
* 需要根據(jù)特殊參數(shù)在啟動(dòng)時(shí)注入
* 該監(jiān)聽器本身不能訪問容器變量, 如果需要訪問, 需要從上下文中獲取對(duì)象實(shí)例后方可繼續(xù)訪問實(shí)例信息
* 如果其它類中啟動(dòng)了多線程, 是無法接管異常拋出的, 需要子線程中正確處理退出操作
* 該監(jiān)聽器最好不用直接做線程操作, 子類的實(shí)現(xiàn)不干預(yù)
*/
@Slf4j
public class TaskApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
/**
* 任務(wù)啟動(dòng)監(jiān)聽類標(biāo)識(shí), 啟動(dòng)時(shí)注入
* 即是 java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar learn.jar
*/
private static final String SPRING_TASK_CLASS = "spring.task.class";
/**
* 支持該注解的方法個(gè)數(shù), 目前僅一個(gè)
* 可以理解為控制臺(tái)一次執(zhí)行一個(gè)類, 依賴的任務(wù)應(yīng)該通過其它方式控制依賴
*/
private static final int SUPPORT_METHOD_COUNT = 1;
/**
* 保存當(dāng)前容器運(yùn)行上下文
*/
private ApplicationContext context;
/**
* 監(jiān)聽容器刷新事件
*
* @param event 容器刷新事件
*/
@Override
@SuppressWarnings("unchecked")
public void onApplicationEvent(ContextRefreshedEvent event) {
context = event.getApplicationContext();
// 不存在時(shí)可能為正常的容器啟動(dòng)運(yùn)行, 無需關(guān)心
String taskClass = System.getProperty(SPRING_TASK_CLASS);
log.info("ScheduleTask spring task Class: {}", taskClass);
if (taskClass != null) {
try {
// 獲取類字節(jié)碼文件
Class clazz = findClass(taskClass);
// 嘗試從內(nèi)容上下文中獲取已加載的目標(biāo)類對(duì)象實(shí)例, 這個(gè)類實(shí)例是已經(jīng)加載到容器內(nèi)的對(duì)象實(shí)例, 即可以獲取類的信息
Object object = context.getBean(clazz);
Method method = findMethod(object);
log.info("start to run task Class: {}, Method: {}", taskClass, method.getName());
invoke(method, object);
} catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
} finally {
// 需要確保容器正常出發(fā)停止事件, 否則容器會(huì)僵尸卡死
shutdown();
}
}
}
/**
* 根據(jù)class路徑名稱查找類文件
*
* @param clazz 類名稱
* @return 類對(duì)象
* @throws ClassNotFoundException ClassNotFoundException
*/
private Class findClass(String clazz) throws ClassNotFoundException {
return Class.forName(clazz);
}
/**
* 獲取目標(biāo)對(duì)象中符合條件的方法
*
* @param object 目標(biāo)對(duì)象實(shí)例
* @return 符合條件的方法
*/
private Method findMethod(Object object) {
Method[] methods = object.getClass().getDeclaredMethods();
List<Method> schedules = Stream.of(methods)
.filter(method -> method.isAnnotationPresent(ScheduleTask.class))
.collect(Collectors.toList());
if (schedules.size() != SUPPORT_METHOD_COUNT) {
throw new IllegalStateException("only one method should be annotated with @ScheduleTask, but found "
+ schedules.size());
}
return schedules.get(0);
}
/**
* 執(zhí)行目標(biāo)對(duì)象方法
*
* @param method 目標(biāo)方法
* @param object 目標(biāo)對(duì)象實(shí)例
* @throws IllegalAccessException IllegalAccessException
* @throws InvocationTargetException InvocationTargetException
*/
private void invoke(Method method, Object object) throws IllegalAccessException, InvocationTargetException {
method.invoke(object);
}
/**
* 執(zhí)行完畢退出運(yùn)行容器, 并將返回值交給執(zhí)行環(huán)節(jié), 比如控制臺(tái)等
*/
private void shutdown() {
log.info("shutdown ...");
System.exit(SpringApplication.exit(context));
}
}
其實(shí)該處僅需要啟動(dòng)執(zhí)行即可,容器啟動(dòng)完畢事件也是可以的。
2. 標(biāo)識(shí)目標(biāo)方法
目標(biāo)方法的標(biāo)識(shí),最方便的是使用注解標(biāo)注。
package com.github.zhgxun.learn.common.task.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ScheduleTask {
}
3. 編寫任務(wù)
package com.github.zhgxun.learn.task;
import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;
import com.github.zhgxun.learn.service.first.LaunchInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class TestTask {
@Autowired
private LaunchInfoService launchInfoService;
@ScheduleTask
public void test() {
log.info("Start task ...");
log.info("LaunchInfoList: {}", launchInfoService.findAll());
log.info("模擬啟動(dòng)線程操作");
for (int i = 0; i < 5; i++) {
new MyTask(i).start();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyTask extends Thread {
private int i;
private int j;
private String s;
public MyTask(int i) {
this.i = i;
}
@Override
public void run() {
super.run();
System.out.println("第 " + i + " 個(gè)線程啟動(dòng)..." + Thread.currentThread().getName());
if (i == 2) {
throw new RuntimeException("模擬運(yùn)行時(shí)異常");
}
if (i == 3) {
// 除數(shù)不為0
int a = i / j;
}
// 未對(duì)字符串對(duì)象賦值, 獲取長度報(bào)空指針錯(cuò)誤
if (i == 4) {
System.out.println(s.length());
}
}
}
4. 啟動(dòng)改造
啟動(dòng)時(shí)需要做一些調(diào)整,即跟普通的啟動(dòng)區(qū)分開。這也是為什么不要把監(jiān)聽目標(biāo)對(duì)象直接放入容器中的原因,在這里顯示添加到容器中,這樣就不影響項(xiàng)目中類似 CommandLineRunner 的功能,畢竟這種功能是容器啟動(dòng)完畢就能運(yùn)行的。如果要改造,會(huì)涉及到很多硬編碼。
package com.github.zhgxun.learn;
import com.github.zhgxun.learn.common.task.TaskApplicationListener;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class LearnApplication {
public static void main(String[] args) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(LearnApplication.class);
// 根據(jù)啟動(dòng)注入?yún)?shù)判斷是否為任務(wù)動(dòng)作即可, 否則不干預(yù)啟動(dòng)
if (System.getProperty("spring.task.class") != null) {
builder.listeners(new TaskApplicationListener()).run(args);
} else {
builder.run(args);
}
}
}
5. 啟動(dòng)注入
-Dspring.task.class 即是啟動(dòng)注入標(biāo)識(shí),當(dāng)然這個(gè)標(biāo)識(shí)不要跟默認(rèn)的參數(shù)混淆,需要區(qū)分開,否則可能始終獲取到系統(tǒng)參數(shù),而無法獲取用戶參數(shù)。
java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar target/learn.jar
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- spring schedule配置多任務(wù)動(dòng)態(tài)cron(增刪啟停)
- SpringMVC Cron定時(shí)器Demo常見問題解決方案
- springboot實(shí)現(xiàn)多實(shí)例crontab搶占定時(shí)任務(wù)(實(shí)例代碼)
- springtask 的使用方法和 cron 表達(dá)式解析
- springboot Quartz動(dòng)態(tài)修改cron表達(dá)式的方法
- Spring @Scheduler使用cron表達(dá)式時(shí)的執(zhí)行問題詳解
- Spring的@Scheduled 如何動(dòng)態(tài)更新cron表達(dá)式
相關(guān)文章
基于java計(jì)算買賣股票的最佳時(shí)機(jī)
這篇文章主要介紹了基于java計(jì)算買賣股票的最佳時(shí)機(jī),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
MyBatis?如何使項(xiàng)目兼容多種數(shù)據(jù)庫的解決方案
要想做兼容多種數(shù)據(jù)庫,那毫無疑問,我們首先得明確我們要兼容哪些數(shù)據(jù)庫,他們的數(shù)據(jù)庫產(chǎn)品名稱是什么,本次我們講解了一套使項(xiàng)目兼容多種數(shù)據(jù)庫的方案,對(duì)MyBatis項(xiàng)目兼容多種數(shù)據(jù)庫操作方法感興趣的朋友一起看看吧2024-05-05
Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之二手書商城系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開發(fā)的二手書商城系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有在線書城該有的所有功能,感興趣的朋友快來看看吧2022-01-01
JavaWeb學(xué)習(xí)筆記之Filter和Listener
這篇文章主要給大家介紹了關(guān)于JavaWeb學(xué)習(xí)筆記之Filter和Listener的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
SpringCloud之Zuul服務(wù)網(wǎng)關(guān)詳解
這篇文章主要介紹了SpringCloud之Zuul服務(wù)網(wǎng)關(guān)詳解,服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)中一個(gè)不可或缺的部分,通過服務(wù)網(wǎng)關(guān)統(tǒng)一向外系統(tǒng)提供REST?API的過程中,除了具備服務(wù)路由、均衡負(fù)載功能之外,它還具備了權(quán)限控制(鑒權(quán))等功能,需要的朋友可以參考下2023-08-08
java string類型轉(zhuǎn)換boolean類型的方法
下面小編就為大家?guī)硪黄猨ava string類型轉(zhuǎn)換boolean類型的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-11-11
Java編程之jdk1.4,jdk1.5和jdk1.6的區(qū)別分析(經(jīng)典)
這篇文章主要介紹了Java編程之jdk1.4,jdk1.5和jdk1.6的區(qū)別分析,結(jié)合實(shí)例形式較為詳細(xì)的分析說明了jdk1.4,jdk1.5和jdk1.6版本的使用區(qū)別,需要的朋友可以參考下2015-12-12
詳解java之redis篇(spring-data-redis整合)
本篇文章主要介紹了java之redis篇,主要詳細(xì)的介紹了spring-data-redis整合,有興趣的可以了解一下。2017-01-01

