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及端口号即可连接查看相关信息。

猜你喜欢

转载自blog.csdn.net/ljyhust/article/details/78638076