SpringTask執行定時任務中呼叫方法中斷問題
背景
使用SpringQuartz輕量級定時任務時,出現任務中的方法呼叫鏈未執行完,也未丟擲異常,然後到下一次時間就繼續執行下一次的任務。剛開始時百度一下,以為是執行緒阻塞、併發設定等(預設是併發執行)。然後順著這個思路一直往下搜尋資料,找到的是執行緒阻塞,然後不理解為什麼阻塞,用了各種方法,包括Java VisualVM監控器來監聽Tomcat的執行緒問題,檢視哪些執行緒waitable;事後證明是我多想了,並沒有等待執行緒,也沒有CPU非常高的現象。耐心再debug幾次發現有幾個異常,可是一直都沒有丟擲來,直到追蹤到一個定時任務執行緒中的異常資訊才發現,是Spring定時任務框架將異常捕獲了,導致控制檯沒有輸出。細想定時任務這麼設計的原因,否則可能會因為異常原因而導致大量阻塞無法進行下一次定時任務。
過程
原因
被以下任務排程執行緒捕獲而未列印到控制檯。這點可以通過eclipse中的Debug除錯線上程棧中找到,執行時主要呼叫類如下:
SpringTask是如何通過註解來@Scheduled來執行定時任務的?
首先要明白的一點是定時任務都是基於多執行緒來執行的,如Timer或TimerTask等都是基於多執行緒的,而在java併發包中有個ScheduledThreadPool是專門用來解決定時任務執行緒的問題。
SpringTask執行定時任務的方法是org.springframework.scheduling.support.ScheduledMethodRunnable.ScheduledMethodRunnable
private final Object target;
private final Method method;
public ScheduledMethodRunnable(Object target, Method method) {
this.target = target;
this.method = method;
}
@Override
public void run() {
try {
ReflectionUtils.makeAccessible(this .method);
this.method.invoke(this.target);
}
catch (InvocationTargetException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getTargetException());
}
catch (IllegalAccessException ex) {
throw new UndeclaredThrowableException(ex);
}
}
因此ScheduledMethodRunnable
類的主要作用就是建立一個執行緒代理執行定時任務方法。並且在執行方法過程中自定義的方法(定時任務)如果發生異常,尤其是執行時異常則會層層丟擲,直到這個run()方法捕獲,因此才會出現本次案例中的錯解,誤以為定時任務執行緒阻塞或其它原因。而在本例中的任務執行中會呼叫mybatis查詢資料庫,如果出現數據庫異常的話,則無法通過run方法丟擲RuntimeException,原因在於SqlException不屬於RuntimeException。
繼續往下看,檢視構造方法的呼叫鏈。
在doWith方法中發現熟悉的postProcessAfterInitialization()實現,這個是Spring生命週期中容器級別的注入方法,介面是BeanPostProcessor,用於在容器初始化所有的bean前後做一些業務處理。postProcessAfterInitialization()業務中具體對所有的bean中的方法搜尋是否有@Scheduled註解,然後通過反射得到類和方法的資訊等。至此我們明白了SpringTask通過@Scheduled獲取執行任務的過程。
@Override
public Object postProcessAfterInitialization(final Object bean, String beanName) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass)) {
final Set<Method> annotatedMethods = new LinkedHashSet<Method>(1);
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
for (Scheduled scheduled :
AnnotationUtils.getRepeatableAnnotation(method, Schedules.class, Scheduled.class)) {
processScheduled(scheduled, method, bean);
annotatedMethods.add(method);
}
}
});
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass);
if (logger.isDebugEnabled()) {
logger.debug("No @Scheduled annotations found on bean class: " + bean.getClass());
}
}
else {
// Non-empty set of methods
if (logger.isDebugEnabled()) {
logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
"': " + annotatedMethods);
}
}
}
return bean;
}
- 解決
定時任務方法要麼拋異常,要麼對整個方法內的業務捕獲異常並處理。本次解決採用的是捕獲異常並列印訊息方便維護。
@Scheduled("0 0/5 * * * *")
void excuteTask() {
try {
system.err.println("測試。。。");
//TODO
} cathch (Exception e) {
logger.error("erroro is {}", e);
}
}
總結
對於eclipse debug模式並不熟練,對於執行緒棧也沒有理清楚。出現問題,先從debug開始耐心一步一步找到問題然後解決。其它
如何通過VisualVM監聽Tomcat執行狀態?
VisualVM要監聽Tomcat需要Tomcat配置可以通過JMX埠被監聽才可以。windows具體方法如下,在catalina.bat檔案中(Linux中是catalina.sh檔案,具體網上搜索)的rem Guess CATALINA_HOME if not defined
位置下新增set JAVA_OPTS=-Dcom.sun.management.jmxremote.port=9090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
一行語句,其中9090是監聽埠,然後開啟VisualVM開始JMX連線,輸入IP及埠號即可連線檢視相關資訊。