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及端口號即可連接查看相關信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章