在以往的 Tomcat 項目中,一直習慣用 Ant 打包,使用 build.xml
配置,經過 ant -buildfile
的方式在機器上執行定時任務。雖然 Spring 自己支持定時任務,但都是服務一直運行時支持。其實在項目中,大多數定時任務,仍是藉助 Linux Crontab 來支持,須要時運行便可,不須要一直佔用機器資源。但 Spring Boot 項目或者普通的 jar 項目,就沒這麼方便了。java
Spring Boot 提供了相似 CommandLineRunner 的方式,很好的執行常駐任務;也能夠藉助 ApplicationListener 和 ContextRefreshedEvent 等事件來作不少事情。藉助該容器事件,同樣能夠作到相似 Ant 運行的方式來運行定時任務,固然須要作一些項目改動。git
藉助容器刷新事件來監聽目標對象便可,能夠認爲,定時任務其實每次只是執行一種操做而已。github
好比這是一個寫好的例子,注意不要直接用 @Service 將其放入容器中,除非容器自己沒有其它自動運行的事件。spring
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; /** * 不自動加入容器, 用於區分是否屬於任務啓動, 不然放入容器中, Spring 沒法選擇性執行 * 須要根據特殊參數在啓動時注入 * 該監聽器自己不能訪問容器變量, 若是須要訪問, 須要從上下文中獲取對象實例後方可繼續訪問實例信息 * 若是其它類中啓動了多線程, 是沒法接管異常拋出的, 須要子線程中正確處理退出操做 * 該監聽器最好不用直接作線程操做, 子類的實現不干預 */ @Slf4j public class TaskApplicationListener implements ApplicationListener<ContextRefreshedEvent> { /** * 任務啓動監聽類標識, 啓動時注入 * 便是 java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar learn.jar */ private static final String SPRING_TASK_CLASS = "spring.task.class"; /** * 支持該註解的方法個數, 目前僅一個 * 能夠理解爲控制檯一次執行一個類, 依賴的任務應該經過其它方式控制依賴 */ private static final int SUPPORT_METHOD_COUNT = 1; /** * 保存當前容器運行上下文 */ private ApplicationContext context; /** * 監聽容器刷新事件 * * @param event 容器刷新事件 */ @Override @SuppressWarnings("unchecked") public void onApplicationEvent(ContextRefreshedEvent event) { context = event.getApplicationContext(); // 不存在時可能爲正常的容器啓動運行, 無需關心 String taskClass = System.getProperty(SPRING_TASK_CLASS); log.info("ScheduleTask spring task Class: {}", taskClass); if (taskClass != null) { try { // 獲取類字節碼文件 Class clazz = findClass(taskClass); // 嘗試從內容上下文中獲取已加載的目標類對象實例, 這個類實例是已經加載到容器內的對象實例, 便可以獲取類的信息 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 { // 須要確保容器正常出發中止事件, 不然容器會殭屍卡死 shutdown(); } } } /** * 根據class路徑名稱查找類文件 * * @param clazz 類名稱 * @return 類對象 * @throws ClassNotFoundException ClassNotFoundException */ private Class findClass(String clazz) throws ClassNotFoundException { return Class.forName(clazz); } /** * 獲取目標對象中符合條件的方法 * * @param object 目標對象實例 * @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); } /** * 執行目標對象方法 * * @param method 目標方法 * @param object 目標對象實例 * @throws IllegalAccessException IllegalAccessException * @throws InvocationTargetException InvocationTargetException */ private void invoke(Method method, Object object) throws IllegalAccessException, InvocationTargetException { method.invoke(object); } /** * 執行完畢退出運行容器, 並將返回值交給執行環節, 好比控制檯等 */ private void shutdown() { log.info("shutdown ..."); System.exit(SpringApplication.exit(context)); } }
其實該處僅須要啓動執行便可,容器啓動完畢事件也是能夠的。bash
目標方法的標識,最方便的是使用註解標註。多線程
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 { }
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("模擬啓動線程操做"); 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 + " 個線程啓動..." + Thread.currentThread().getName()); if (i == 2) { throw new RuntimeException("模擬運行時異常"); } if (i == 3) { // 除數不爲0 int a = i / j; } // 未對字符串對象賦值, 獲取長度報空指針錯誤 if (i == 4) { System.out.println(s.length()); } } }
啓動時須要作一些調整,即跟普通的啓動區分開。這也是爲何不要把監聽目標對象直接放入容器中的緣由,在這裏顯示添加到容器中,這樣就不影響項目中相似 CommandLineRunner 的功能,畢竟這種功能是容器啓動完畢就能運行的。若是要改造,會涉及到不少硬編碼。ide
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); // 根據啓動注入參數判斷是否爲任務動做便可, 不然不干預啓動 if (System.getProperty("spring.task.class") != null) { builder.listeners(new TaskApplicationListener()).run(args); } else { builder.run(args); } } }
-Dspring.task.class
便是啓動注入標識,固然這個標識不要跟默認的參數混淆,須要區分開,不然可能始終獲取到系統參數,而沒法獲取用戶參數。ui
java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar target/learn.jar