今天在做一个项目的时候用到了Spring的定时计划任务。这是Spring的特色功能,可以根据设置在特定的时间或间隔时间做特定的事。

下面给出一个例子:

[java] view plaincopy在CODE上查看代码片派生到我的代码片 


03.import java.text.SimpleDateFormat; 

04.import java.util.Calendar; 

05.import java.util.Locale; 

06. 

07.public class TimerTask { 

08. public void printTimeStamp(){ 

09. Calendar ca= Calendar.getInstance(); 

10. ca.setTimeInMillis(System.currentTimeMillis()); 

11. SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ", Locale.CHINA); 

12. //显示当前时间 精确到毫秒 

13. System.out.print(sdf.format(ca.getTime())); 

14. } 

15. public TimerTask(){ 

16. this.printTimeStamp(); 

17. System.out.println("计划任务被初始化了"); 

18. } 

19. public void doTask(){ 

20. this.printTimeStamp(); 

21. System.out.print("计划任务被执行,线程id:"); 

22. System.out.println(Thread.currentThread().getId()); 

23. } 

24.} 



根据Spring关于定时任务的规范,任务执行方法应为无参数无返回的方法,因此按照规范上面的例子中声明了doTask方法。上面的例子很简单,Spring作为IoC容器,构造TimerTask实例时会调用无参构造函数,此类会在实例化时在控制台输出当前时间和构造信息。当定时任务被触发时,也会在控制台显示当前时间和任务被执行的提示信息。 


下面是配置(需要声明的是,本实例基于J2EE工程,使用了log4j,配置文件只是工程中的一部分): 



[java] view plaincopy在CODE上查看代码片派生到我的代码片 

01.<?xml version="1.0" encoding="utf-8"?> 

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

03. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

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

05. http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> 

06. <!-- 注册定时器 --> 

07. <bean id="timer" 

08. class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> 

09. <property name="triggers"> 

10. <list> 

11. <ref bean="timerTaskTrigger" /> 

12. </list> 

13. </property> 

14. </bean> 

15. <!-- 指定何时触发定时任务 --> 

16. <bean id="timerTaskTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> 

17. <property name="jobDetail"> 

18. <ref bean="timerTaskJobDetail" /> 

19. </property> 

20. <property name="cronExpression"> 

21. <!-- 每3秒钟触发一次 --> 

22. <value>0/3 * * * * ?</value> 

23. </property> 

24. </bean> 

25. <!-- 指定定时任务细节 调用哪个类 哪个方法 --> 

26. <bean id="timerTaskJobDetail" 

27. class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> 

28. <property name="targetObject"> 

29. <ref bean="timerTaskInstance" /> 

30. </property> 

31. <property name="targetMethod"> 

32. <value>doTask</value> 

33. </property> 

34. <property name="concurrent" value="false" /> 

35. </bean> 

36. <!-- 实例化定时任务类 --> 

37. <bean id="timerTaskInstance" class="net.csdn.blog.chaijunkukn.TimerTask" /> 

38.</beans> 



 web.xml的配置: 


[java] view plaincopy在CODE上查看代码片派生到我的代码片 

01.<?xml version="1.0" encoding="UTF-8"?> 

02.<web-app id="WebApp_ID" version="2.4" 

03. xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

04. xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> 

05. <display-name>TaskTest</display-name> 

06. <servlet> 

07. <servlet-name>springapp</servlet-name> 

08. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 

09. <init-param> 

10. <param-name>contextConfigLocation</param-name> 

11. <param-value>/WEB-INF/classes/applicationContext*.xml</param-value> 

12. </init-param> 

13. <load-on-startup>1</load-on-startup> 

14. </servlet> 

15. 

16. <servlet-mapping> 

17. <servlet-name>springapp</servlet-name> 

18. <url-pattern>*.htm</url-pattern> 

19. </servlet-mapping> 

20. 

21. <filter> 

22. <filter-name>EncodingFilter</filter-name> 

23. <filter-class>com.ku6.tech.wap.filter.EncodingFilter</filter-class> 

24. <init-param> 

25. <param-name>encoding</param-name> 

26. <param-value>utf-8</param-value> 

27. </init-param> 

28. <init-param> 

29. <param-name>forceEncoding</param-name> 

30. <param-value>true</param-value> 

31. </init-param> 

32. </filter> 

33. 

34. <filter-mapping> 

35. <filter-name>EncodingFilter</filter-name> 

36. <url-pattern>*.htm</url-pattern> 

37. </filter-mapping> 

38. 

39. <error-page> 

40. <error-code>404</error-code> 

41. <location>/error.jsp</location> 

42. </error-page> 

43. 

44. <welcome-file-list> 

45. <welcome-file>index.jsp</welcome-file> 

46. </welcome-file-list> 

47. 

48. <listener> 

49. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 

50. </listener> 

51. 

52. <context-param> 

53. <param-name>contextConfigLocation</param-name> 

54. <param-value>/WEB-INF/classes/applicationContext*.xml</param-value> 

55. </context-param> 

56.</web-app> 



•load-on-startup标记容器是否在启动的时候实例化并调用其init()方法的优先级。 



•它的值表示servlet应该被载入的顺序 



•当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet; 



•如果值小于0或未指定时,则表示只有在第一次请求的容器才在该servlet调用初始化函数 



•正值越小,servlet的优先级越高,应用启动时就越先加载。 



•值相同时,容器就会自己选择顺序来加载。 



配置的部分就是这样,然后我使用MyEclipse 9.1关联上Tomcat服务器。一切都是默认的设置,然后将本引用部署并启动Tomcat服务器。 


这时候问题来了,我的任务类居然被创建了两次,下面是截取的部分日志数据: 


[java] view plaincopy在CODE上查看代码片派生到我的代码片 

01.2011-11-01 19:09:02,568 INFO [main] - org.springframework.orm.hibernate3.HibernateTransactionManager.afterPropertiesSet(421) | Using DataSource [org.apache.commons.dbcp.BasicDataSource@f2ff9b] of Hibernate SessionFactory for HibernateTransactionManager 

02.2011-11-01 19:09:02,756 INFO [main] - org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer.postProcessTemplateLoaders(124) | ClassTemplateLoader for Spring macros added to FreeMarker configuration 

03.2011-11-01 19:09:03.878 计划任务被初始化了 

04.2011-11-01 19:09:03,987 INFO [main] - org.quartz.core.SchedulerSignalerImpl.<init>(63) | Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 

05.2011-11-01 19:09:03,987 INFO [main] - org.quartz.core.QuartzScheduler.<init>(214) | Quartz Scheduler v.1.6.1-RC1 created. 

06.... 

07.2011-11-01 19:09:05,140 WARN [main] - org.hibernate.cache.EhCacheProvider.buildCache(86) | Could not find configuration [org.hibernate.cache.StandardQueryCache]; using defaults. 

08.2011-11-01 19:09:05,218 INFO [main] - org.springframework.orm.hibernate3.HibernateTransactionManager.afterPropertiesSet(421) | Using DataSource [org.apache.commons.dbcp.BasicDataSource@85b4c5] of Hibernate SessionFactory for HibernateTransactionManager 

09.2011-11-01 19:09:05,218 INFO [main] - org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer.postProcessTemplateLoaders(124) | ClassTemplateLoader for Spring macros added to FreeMarker configuration 

10.2011-11-01 19:09:05.249 计划任务被初始化了 

11.2011-11-01 19:09:05,249 INFO [main] - org.quartz.core.SchedulerSignalerImpl.<init>(63) | Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 

12.2011-11-01 19:09:05,249 INFO [main] - org.quartz.core.QuartzScheduler.<init>(214) | Quartz Scheduler v.1.6.1-RC1 created. 

13.... 

14.2011-11-1 19:09:05 org.apache.catalina.startup.Catalina start 

15.信息: Server startup in 9451 ms 

16.2011-11-01 19:09:06.013 计划任务被执行,线程id:17 

17.2011-11-01 19:09:06.013 计划任务被执行,线程id:39 

18.2011-11-01 19:09:09.021 计划任务被执行,线程id:19 

19.2011-11-01 19:09:09.021 计划任务被执行,线程id:40 



从上面的日志中可以看出, 


在2011-11-01 19:09:03.878 定时计划任务类产生了一个实例 


在2011-11-01 19:09:05.249 定时 计划任务类又产生了一个实例 


起初我对它并不关心,但是下面的问题却是不可接受的,计划任务确实是差不多每隔3秒钟被调度的,但是每次调度执行了任务方法两次。设想一下,这仅仅是个开销很小的例子,但是如果这个方法执行的是一个非常耗时耗资源的任务,好不容易执行完一次后又要执行一次,这是对计算资源的极大浪费。于是查找了一天的原因,最后在国外的一个论坛上找到了解决的办法(http://forum.springsource.org/showthread.php?33311-IoC-Container-initializes-my-app-twice)。 


楼主roncox和我遇到了同样的问题,他和我的配置差不多,同样也贴出了配置文件。虽然其他人没有解决问题,但是他自己解决了,并提供了最后的解决方法: 


解决办法就是将web.xml配置文件中的如下节点删掉: 


[html] view plaincopy在CODE上查看代码片派生到我的代码片 

01.<listener> 

02. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 

03.</listener> 

04. 

05.<context-param> 

06. <param-name>contextConfigLocation</param-name> 

07. <param-value>/WEB-INF/classes/applicationContext*.xml</param-value> 

08.</context-param> 



修改之后程序运行一切正常。个人推测,由于org.springframework.web.context.ContextLoaderListener和org.springframework.web.servlet.DispatcherServlet都能够加载applicationContext*.xml(“*”是通配符,表示所有以“applicationContext”开头的xml文件)。而两个类殊途同归,最终都将这些配置文件交给了Spring框架的Ioc容器进行实例化。因此每个类都会被实例化两次。 



发现之前说的不完全正确,不应该删除web.xml中的如下节点 


[html] view plaincopy在CODE上查看代码片派生到我的代码片 

01.<listener> 

02. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 

03.</listener> 

04. 

05.<context-param> 

06. <param-name>contextConfigLocation</param-name> 

07. <param-value>/WEB-INF/classes/applicationContext*.xml</param-value> 

08.</context-param> 



因为该节点指派的applicationContext*.xml是用于实例化除servlet之外的所有对象的,可以说项目中绝大多数的service和dao层操作都由ContextLoaderListener传递给Spring来进行实例化。 


在web应用中,web.xml还经常出现如下的配置: 


[html] view plaincopy在CODE上查看代码片派生到我的代码片 

01.<!--全局Servlet调度配置 --> 

02. <servlet> 

03. <!--若设置 servlet-name为[name] --> 

04. <!--则DispatcherServlet在实例化后会自动加载[name]-servlet.xml --> 

05. <servlet-name>spring</servlet-name> 

06. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 

07. <init-param> 

08. <param-name>contextConfigLocation</param-name> 

09. <param-value>classpath:servletContext.xml</param-value> 

10. </init-param> 

11. <!--随服务器一同启动 --> 

12. <load-on-startup>1</load-on-startup> 

13. </servlet> 

14. <servlet-mapping> 

15. <servlet-name>spring</servlet-name> 

16. <url-pattern>*.do</url-pattern> 

17. </servlet-mapping> 



这个是用来处理所有servlet的,没有它就无法通过请求地址来调用相应的Controller。 

 这里我明确地指示了要加载类路径下的servletContext.xml,如果不指定,则会按照注释中所描述地那样自动加载spring-servlet.xml 

无论是servletContext.xml还是applicationContext*.xml都可以按照<beans>...<bean id="XXX" class="XXX" />...</beans>这样的形式来配置。 

 问题来了,有时候不注重对象初始化的分类,尤其是使用<context:component-scan base-package="controller" />这样的包扫描形式统一初始化, 

 很容易造成满足条件的对象被初始化两次,那么在计划任务的时候被执行两次也就不奇怪了。其实说来说去,还是要提醒大家,不同的配置文件其作用是不一样的, 

 不要将所有的初始化操作都放到一个配置文件中,更不要重复配置。不仅浪费资源,还很容易导致莫名其妙的故障。 



另外,有相关文章还提到过是Tomcat服务器的问题,修改conf目录下的server.xml。修改节点Host,将appBase属性由默认的“webapps”设置为空("")即可,如下所示: 


[html] view plaincopy在CODE上查看代码片派生到我的代码片 

01.<Host name="localhost" appBase="" unpackWARs="true" autoDeploy="true" 

02. xmlValidation="false" xmlNamespaceAware="false"> 

03. 

04. <Context docBase="/usr/local/apache-tomcat-6.0.29/webapps/semwinner" 

05. path="" reloadable="true"></Context> 

06. <Context docBase="/usr/local/apache-tomcat-6.0.29/webapps/emarboxmanager" 

07. path="/admin" reloadable="true"></Context> 

08.</Host> 



是Tomcat服务器的问题,修改conf目录下的server.xml。修改节点Host,将appBase属性由默认“webapps”设置为空("")即可,如下所示: 


<Host name="localhost" appBase="" unpackWARs="true" autoDeploy="true" 

 xmlValidation="false" xmlNamespaceAware="false"> 

 <Context docBase="/usr/local/apache-tomcat-6.0.29/webapps/crm" 

 path="" reloadable="true"></Context> 

 <Context docBase="/usr/local/apache-tomcat-6.0.29/webapps/crm" 

 path="/admin" reloadable="true"></Context> 

</Host> 


 把appBase设置为空即可! 


去除了appBase="webapps"中的webapps变成了appBase="",因为web应用程序都是放在webapps这个目录下的,如果 不把“webapps“去掉,这里会调用一次quartz的任务调度,在接下来的“<Context path”中又会调用一次quartz的任务调度,所以就重复了2次 


下面提供我的解决办法: 


 修改前的web.xml文件中的配置 

 <context-param> 

 <param-name>contextConfigLocation</param-name> 

 <param-value>classpath:applicationContext*.xml</param-value> 

 </context-param> 

 <listener> 

 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 

 </listener> 


 上面的配置文件,让我的定时计划任务执行了2次,下面是修改后的web.xml中的配置 

 <context-param> 

 <param-name>contextConfigLocation</param-name> 

 <param-value>classpath:applicationContext.xml</param-value> 

 </context-param> 

 <listener> 

 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 

 </listener>



注意: 我只去掉了applicationContext后面的 * 号,明确指定了加载的配置文件。