在以往的 tomcat 項目中,一直習慣用 ant 打包,使用 build.xml 配置,通過 ant -buildfile 的方式在機器上執行定時任務。雖然 spring 本身支持定時任務,但都是服務一直運行時支持。其實在項目中,大多數定時任務,還是借助 linux crontab 來支持,需要時運行即可,不需要一直占用機器資源。但 spring boot 項目或者普通的 jar 項目,就沒這么方便了。
spring boot 提供了類似 commandlinerunner 的方式,很好的執行常駐任務;也可以借助 applicationlistener 和 contextrefreshedevent 等事件來做很多事情。借助該容器事件,一樣可以做到類似 ant 運行的方式來運行定時任務,當然需要做一些項目改動。
1. 監聽目標對象
借助容器刷新事件來監聽目標對象即可,可以認為,定時任務其實每次只是執行一種操作而已。
比如這是一個寫好的例子,注意不要直接用 @service 將其放入容器中,除非容器本身沒有其它自動運行的事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
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)); } } |
其實該處僅需要啟動執行即可,容器啟動完畢事件也是可以的。
2. 標識目標方法
目標方法的標識,最方便的是使用注解標注。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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. 編寫任務
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
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()); } } } |
4. 啟動改造
啟動時需要做一些調整,即跟普通的啟動區分開。這也是為什么不要把監聽目標對象直接放入容器中的原因,在這里顯示添加到容器中,這樣就不影響項目中類似 commandlinerunner 的功能,畢竟這種功能是容器啟動完畢就能運行的。如果要改造,會涉及到很多硬編碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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); } } } |
5. 啟動注入
-dspring.task.class 即是啟動注入標識,當然這個標識不要跟默認的參數混淆,需要區分開,否則可能始終獲取到系統參數,而無法獲取用戶參數。
1
|
java -dspring.task. class =com.github.zhgxun.learn.task.testtask -jar target/learn.jar |
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://segmentfault.com/a/1190000017946999