首页 > 技术,让世界更美好。 > 【DEBUG】Tomcat+Spring下@Scheduled注解定时任务执行两次的解决方案

【DEBUG】Tomcat+Spring下@Scheduled注解定时任务执行两次的解决方案

2016年7月19日

    因为Web项目需要每天定时推送日志到Hadoop之上,遂使用@Scheduled注解编写一个定时任务以达到目的。奈何近一年研究Hadoop生态技术过多,Spring框架和Tomcat其本质了解不够深入,出现了定时任务执行两次的现象。在了解了相关技术后,觉着问题虽然简单,但侧面可以学习到好多TomcatSpring其本质的东西,遂记录下来。

 

1.项目情况

 

下面是定时任务:


1 定时任务

这里使用了@Scheduled标签定义uploadHDFS方法为定时任务,具体执行时间为corn表达式在配置文件中读取。

下面是Spring MVC配置文件:

<?xml version=”1.0″ encoding=”UTF-8″?>

<beans xmlns=”http://www.springframework.org/schema/beans”

    xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:p=”http://www.springframework.org/schema/p”

    xmlns:context=”http://www.springframework.org/schema/context”

    xmlns:mvc=”http://www.springframework.org/schema/mvc”

    xmlns:task=”http://www.springframework.org/schema/task”

    xsi:schemaLocation=”http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-4.1.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-4.1.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd

http://www.springframework.org/schema/task/spring-task-3.1.xsd”>

    <!– 自动扫描该包,使SpringMVC认为包下用了@controller注解的类是控制器 –>

    <context:component-scan base-package=”org.richinfo.logcollection.controller” />

    <!– 指定定时任务所在的包 –>

    <context:component-scan base-package=”org.richinfo.logcollection.task” />

    <!– 定时任务扫描 –>

    <task:annotation-driven />

    <!– @Autowired 注解扫描 –>

    <bean

        class=”org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor” />

    <!– spring的属性加载器,加载hadoop.properties文件中的属性 –>

    <bean id=”propertyConfigurer”

        class=”org.springframework.beans.factory.config.PropertyPlaceholderConfigurer”>

        <property name=”location”>

            <value>classpath:paramter_config.properties</value>

        </property>

        <property name=”fileEncoding” value=”utf-8″ />

    </bean>

    <!– 定义跳转的文件的前后缀
,视图模式配置
–>

    <bean

        class=”org.springframework.web.servlet.view.InternalResourceViewResolver”>

        <!– 这里的配置我的理解是自动给后面action的方法return的字符串加上前缀和后缀,变成一个
可用的url地址
–>

        <property name=”prefix” value=”/WEB-INF/” />

        <property name=”suffix” value=”.jsp” />

    </bean>

    <!– Spring MVC注解扫描 –>

    <mvc:annotation-driven />

</beans>

Web.xml文件如下:


2 web.xml

这里注意,由于我项目较小,只使用了一个Spring-mvc.xml作为配置文件,web.xml加载Spring容器和Spring MVC时候加载的都是Spring-mvc.xml文件。

 

 

2.问题及解决过程

 

 

现在把如上项目发布到Tomcat,设置定时任务5s执行一次,结果发现5s执行了两次,执行时间相同。后来上网查询资料发现只要注释掉web.xml的43到45行后,定时任务就能正常执行一次了。网上给出的理由是Spring监听器加载和Spring MVC的servlet加载都初始化了定时任务,导致定时任务存在两个。

然而,我把相同的War包提交到线上服务器的时候,定时任务仍然执行了2次,于是继续去网上吃书。发现了前面处理方法的风险和学习到了其问题本质。

如上所述,原先导致定时任务执行两次的原因是Spring和Spring MVC的加载都初始化了定时任务,使tomcat中存在两个相同的定时任务。注释掉Spring监听器,自然使定时任务只初始化一次。但只是表面,而且注释掉后就会导致许多隐藏的风险,因为本项目较小,还未暴露出来。

究其本质其实还是自己图省事,把Spring和Spring MVC配置文件写到了一起,而扫描定时任务的工作是写到这个配置文件当中的,当两者分别加载的时候就会重复扫描定时任务,导致最后系统加载了两个。

而根本的解决方法是配置两个配置文件,Spring和Spring MVC分开配置,定时任务在Spring的配置文件中扫描,就不会有重复了。如下图,Spring负责扫描定时任务task和配置文件,Spring MVC负责映射用户访问需求和扫描Controller。

图4 Spring配置文件applicationContent.xml

图5 Spring MVC配置文件Spring-mvc.xml

    然而当发布到线上的时候又出现了定时任务执行两次的情况,这次联想应该是Tomcat服务器配置的原因,遂把线上tomcat服务器的配置下载下来代替线下配置,bug重现。然后逐个对比配置,发现问题出现在Tomcat的配置文件server.xml(目录:tomcat根目录/conf/server.xml)。

出问题的配置如下:

<Host name=”localhost” appBase=”webapps” unpackWARs=”true” autoDeploy=”true”>

<Valve className=”org.apache.catalina.valves.AccessLogValve” directory=”logs”

prefix=”localhost_access_log.” suffix=”.txt”

pattern=”%h %l %u %t &quot;%r&quot; %s %b” />

<Context path=”/log/log_collection” docBase=”log_collection” debug=”0″ reloadable=”true” />

</Host>

而我本机上的tamcat并没有Context标签那一行,删除后定时任务正常运行,但是线上需要做路径映射所以需要Context。研究发现网友把Host标签的appBase=”webapps”改为appBase=””可行,但是在我改了以后启动Tomcat服务器出错,提示找不到项目:

java.lang.IllegalArgumentException: Document base /home/wgd/myTestTomcat7/log_collection does not exist or is not a readable directory

经研究发现可以修改Context标签的docBase的值为docBase=”webapps/log_collection”即可正常启动项目并且定时任务正常执行。原因应该是Host标签会加载项目,而在Context会再次加载导致重复的定时任务。

 

 

3.解决方案总结

 

综上所述,针对定时任务执行多次的原因是因为定时任务被多次初始化,可能是Spring多次扫描或者Tomcat重复加载导致的。针对如上原因,解决方案有如下三种:

  1. 如果你把Spring和Spring MVC使用了一个配置文件,注意是不是在Web.xml中加载了两次,如果是的话可以尝试在Web.xml中注释掉Spring的监听器。删除或注释掉如下图的部分。

  2. 把Spring和Spring MVC配置文件分别编写,并且分别在web.xm中加载,保证两个配置文件的扫描注解的时候不会有交叉。
  3. 如果定时任务仍然执行两次,可检查Tomcat的配置文件server.xml(在tomcat根目录/conf/server.xml)。检查其Host标签,使appBase=””和使host标签下的docBase指向项目的地址。

</beans>

本文的评论功能被关闭了.