1. 程式人生 > >Quartz定時任務執行兩遍的解決辦法

Quartz定時任務執行兩遍的解決辦法

今天在做一個專案的時候用到了spring的定時計劃任務。這是Spring的特色功能,可以根據設定在特定的時間或間隔時間做特定的事。
下面給出一個例子:

package net.csdn.blog.chaijunkukn;  

import java.text.SimpleDateFormat;  
import java.util.Calendar;  
import java.util.Locale;  

public class TimerTask {  
    public void printTimeStamp(){  
        Calendar ca= Calendar.getInstance
(); ca.setTimeInMillis(System.currentTimeMillis()); SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ", Locale.CHINA); //顯示當前時間 精確到毫秒 System.out.print(sdf.format(ca.getTime())); } public TimerTask(){ this.printTimeStamp
(); System.out.println("計劃任務被初始化了"); } public void doTask(){ this.printTimeStamp(); System.out.print("計劃任務被執行,執行緒id:"); System.out.println(Thread.currentThread().getId()); } }

根據Spring關於定時任務的規範,任務執行方法應為無引數無返回的方法,因此按照規範上面的例子中聲明瞭doTask方法。上面的例子很簡單,Spring作為IoC容器,構造TimerTask例項時會呼叫無參建構函式,此類會在例項化時在控制檯輸出當前時間和構造資訊。當定時任務被觸發時,也會在控制檯顯示當前時間和任務被執行的提示資訊。
下面是配置(需要宣告的是,本例項基於J2EE工程,使用了log4j,配置檔案只是工程中的一部分):

<?xml version="1.0" encoding="utf-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">  
    <!-- 註冊定時器 -->  
    <bean id="timer"  
        class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
        <property name="triggers">  
            <list>  
                <ref bean="timerTaskTrigger" />  
            </list>  
        </property>  
    </bean>  
    <!-- 指定何時觸發定時任務 -->  
    <bean id="timerTaskTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">  
        <property name="jobDetail">  
            <ref bean="timerTaskJobDetail" />  
        </property>  
        <property name="cronExpression">  
            <!-- 每3秒鐘觸發一次 -->  
            <value>0/3 * * * * ?</value>  
        </property>  
    </bean>  
    <!-- 指定定時任務細節 呼叫哪個類 哪個方法 -->  
    <bean id="timerTaskJobDetail"  
        class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">  
        <property name="targetObject">  
            <ref bean="timerTaskInstance" />  
        </property>  
        <property name="targetMethod">  
            <value>doTask</value>  
        </property>  
        <property name="concurrent" value="false" />  
    </bean>  
    <!-- 例項化定時任務類 -->  
    <bean id="timerTaskInstance" class="net.csdn.blog.chaijunkukn.TimerTask" />  
</beans> 

web.xml的配置:

<?xml version="1.0" encoding="UTF-8"?>  
<web-app id="WebApp_ID" version="2.4"  
    xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">  
    <display-name>TaskTest</display-name>  
    <servlet>  
        <servlet-name>springapp</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <init-param>  
            <param-name>contextConfigLocation</param-name>  
            <param-value>/WEB-INF/classes/applicationContext*.xml</param-value>  
        </init-param>  
        <load-on-startup>1</load-on-startup>  
    </servlet>  

    <servlet-mapping>  
        <servlet-name>springapp</servlet-name>  
        <url-pattern>*.htm</url-pattern>  
    </servlet-mapping>  

    <filter>  
        <filter-name>EncodingFilter</filter-name>  
        <filter-class>com.ku6.tech.wap.filter.EncodingFilter</filter-class>  
        <init-param>  
            <param-name>encoding</param-name>  
            <param-value>utf-8</param-value>  
        </init-param>  
        <init-param>  
            <param-name>forceEncoding</param-name>  
            <param-value>true</param-value>  
        </init-param>  
    </filter>  

    <filter-mapping>  
        <filter-name>EncodingFilter</filter-name>  
        <url-pattern>*.htm</url-pattern>  
    </filter-mapping>  

    <error-page>  
        <error-code>404</error-code>  
        <location>/error.jsp</location>  
    </error-page>  

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

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

    <context-param>  
        <param-name>contextConfigLocation</param-name>  
        <param-value>/WEB-INF/classes/applicationContext*.xml</param-value>  
    </context-param>  
</web-app>

配置的部分就是這樣,然後我使用MyEclipse 9.1關聯上Tomcat伺服器。一切都是預設的設定,然後將本引用部署並啟動Tomcat伺服器。
這時候問題來了,我的任務類居然被建立了兩次,下面是擷取的部分日誌資料:

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  
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  
2011-11-01 19:09:03.878 計劃任務被初始化了  
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  
2011-11-01 19:09:03,987 INFO [main] - org.quartz.core.QuartzScheduler.<init>(214) | Quartz Scheduler v.1.6.1-RC1 created.  
...  
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.  
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  
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  
2011-11-01 19:09:05.249 計劃任務被初始化了  
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  
2011-11-01 19:09:05,249 INFO [main] - org.quartz.core.QuartzScheduler.<init>(214) | Quartz Scheduler v.1.6.1-RC1 created.  
...  
2011-11-1 19:09:05 org.apache.catalina.startup.Catalina start  
資訊: Server startup in 9451 ms  
2011-11-01 19:09:06.013 計劃任務被執行,執行緒id:17  
2011-11-01 19:09:06.013 計劃任務被執行,執行緒id:39  
2011-11-01 19:09:09.021 計劃任務被執行,執行緒id: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配置檔案中的如下節點刪掉:

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

<context-param>  
    <param-name>contextConfigLocation</param-name>  
    <param-value>/WEB-INF/classes/applicationContext*.xml</param-value>  
</context-param>  

修改之後程式執行一切正常。個人推測,由於org.springframework.web.context.ContextLoaderListener和org.springframework.web.servlet.DispatcherServlet都能夠載入applicationContext*.xml(“*”是萬用字元,表示所有以“applicationContext”開頭的xml檔案)。而兩個類殊途同歸,最終都將這些配置檔案交給了Spring框架的Ioc容器進行例項化。因此每個類都會被例項化兩次。

2012年1月10日補充:今天做專案自習研究了一下spring的配置檔案,發現之前說的不完全正確,不應該刪除web.xml中的如下節點

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

<context-param>  
    <param-name>contextConfigLocation</param-name>  
    <param-value>/WEB-INF/classes/applicationContext*.xml</param-value>  
</context-param>  

因為該節點指派的applicationContext*.xml是用於例項化除servlet之外的所有物件的,可以說專案中絕大多數的service和dao層操作都由ContextLoaderListener傳遞給Spring來進行例項化。
在web應用中,web.xml還經常出現如下的配置:

<!--全域性Servlet排程配置 -->  
    <servlet>  
        <!--若設定 servlet-name為[name] -->  
        <!--則DispatcherServlet在例項化後會自動載入[name]-servlet.xml -->  
        <servlet-name>spring</servlet-name>  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <init-param>  
            <param-name>contextConfigLocation</param-name>  
            <param-value>classpath:servletContext.xml</param-value>  
        </init-param>  
        <!--隨伺服器一同啟動 -->  
        <load-on-startup>1</load-on-startup>  
    </servlet>  
    <servlet-mapping>  
        <servlet-name>spring</servlet-name>  
        <url-pattern>*.do</url-pattern>  
    </servlet-mapping>  

這個是用來處理所有servlet的,沒有它就無法通過請求地址來呼叫相應的Controller。
這裡我明確地指示了要載入類路徑下的servletContext.xml,如果不指定,則會按照註釋中所描述地那樣自動載入spring-servlet.xml
無論是servletContext.xml還是applicationContext*.xml都可以按照……這樣的形式來配置。
問題來了,有時候不注重物件初始化的分類,尤其是使用這樣的包掃描形式統一初始化,
很容易造成滿足條件的物件被初始化兩次,那麼在計劃任務的時候被執行兩次也就不奇怪了。其實說來說去,還是要提醒大家,不同的配置檔案其作用是不一樣的,
不要將所有的初始化操作都放到一個配置檔案中,更不要重複配置。不僅浪費資源,還很容易導致莫名其妙的故障。

另外,有相關文章還提到過是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/semwinner"  
        path="" reloadable="true"></Context>  
    <Context docBase="/usr/local/apache-tomcat-6.0.29/webapps/emarboxmanager"  
        path="/admin" reloadable="true"></Context>  
</Host>    

但是本人嘗試之後並沒有起作用。可能不適用於我遇到的這個問題。寫出上面解決方法的作者認為web應用程式預設都是放在webapps這個目錄下的,如果不把“webapps“去掉,這裡會呼叫一次quartz的任務排程,在接下來的“

<listener>  
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
</listener>  
<context-param>  
<param-name>contextConfigLocation</param-name>  
<param-value>classpath:applicationContext*.xml</param-value>  
</context-param>  
<servlet>  
<servlet-name>spring</servlet-name>  
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
<init-param>  
<param-name>contextConfigLocation</param-name>  
<param-value>classpath:spring-servlet.xml</param-value>  
</init-param>  
<load-on-startup>1</load-on-startup>  
</servlet>  

listener–context-param這段配置是負責依賴注入的,配置檔名稱支援正則匹配。這裡是關鍵,你要看看所有匹配規則的配置檔案中是否存在重複注入bean的現象;
servlet這段是負責請求URL請求處理轉發到哪個Bean上的。
希望對你有幫助。

關鍵:spring-servlet.xml 檔案負責的是url 的轉發,而 applicationContext.xml 負責的是bean 的注入,兩者負責的工作不一樣,所以在 web.xml 檔案中配置的時候,就需要注意,DispatcherServlet 的初始化引數是spring-servlet.xml,而ContextLoaderListener的初始化引數才是applicationContext.xml 檔案呢。

經過本人測試發現第二種:修改 server.xml 檔案的方式是可以的,反而第一種方式不起作用。