1. 程式人生 > >SpringTask執行定時任務中呼叫方法中斷問題

SpringTask執行定時任務中呼叫方法中斷問題

背景

使用SpringQuartz輕量級定時任務時,出現任務中的方法呼叫鏈未執行完,也未丟擲異常,然後到下一次時間就繼續執行下一次的任務。剛開始時百度一下,以為是執行緒阻塞、併發設定等(預設是併發執行)。然後順著這個思路一直往下搜尋資料,找到的是執行緒阻塞,然後不理解為什麼阻塞,用了各種方法,包括Java VisualVM監控器來監聽Tomcat的執行緒問題,檢視哪些執行緒waitable;事後證明是我多想了,並沒有等待執行緒,也沒有CPU非常高的現象。耐心再debug幾次發現有幾個異常,可是一直都沒有丟擲來,直到追蹤到一個定時任務執行緒中的異常資訊才發現,是Spring定時任務框架將異常捕獲了,導致控制檯沒有輸出。細想定時任務這麼設計的原因,否則可能會因為異常原因而導致大量阻塞無法進行下一次定時任務。

過程

  • 原因
    被以下任務排程執行緒捕獲而未列印到控制檯。這點可以通過eclipse中的Debug除錯線上程棧中找到,執行時主要呼叫類如下:
    springTask用到的類

  • SpringTask是如何通過註解來@Scheduled來執行定時任務的?
    首先要明白的一點是定時任務都是基於多執行緒來執行的,如Timer或TimerTask等都是基於多執行緒的,而在java併發包中有個ScheduledThreadPool是專門用來解決定時任務執行緒的問題。
    SpringTask執行定時任務的方法是org.springframework.scheduling.support.ScheduledMethodRunnable.ScheduledMethodRunnable

    類中的run()方法,該類實現了Runnable方法;構造方法與原始碼如下:

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及埠號即可連線檢視相關資訊。