1. 程式人生 > >[轉]結合原始碼淺析Struts2與Spring整合的原理

[轉]結合原始碼淺析Struts2與Spring整合的原理

文章的結構如下:

一、回顧Struts2與Spring整合的配置方法

二、(重點)對關鍵配置的分析

--------------------------------------------------------

一、回顧Struts2與Spring整合的配置方法

1. 建立一個普通的Web應用(含/WEB-INF/web.xml)

2. 配置Struts2

  首先,我們要匯入Struts2所需的Jar包(先不考慮整合用的包),導包的就不多說了。接下來,我們要建立struts.xml配置檔案,我把配置檔案放在CLASSPATH /src 中。這裡我先建立一個測試用的package,配置檔案的主要內容如下:

複製程式碼

1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" "http://struts.apache.org/dtds/struts-2.3.dtd" >
3 <struts>
4     <package name="test" extends="struts-default" namespace="/">
5     </package>
6 </struts>

複製程式碼

  接下來,我們要在Web應用的配置檔案web.xml中配置Struts2的過濾器StrutsPrepareAndExecuteFilter,用來過濾所有的請求,配置檔案如下:

複製程式碼

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
 3   <display-name>ssh7</display-name>
 4   <filter>
 5       <filter-name>struts2</filter-name>
 6       <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
 7   </filter>
 8   <filter-mapping>
 9       <filter-name>struts2</filter-name>
10       <url-pattern>/*</url-pattern>
11   </filter-mapping>
12 </web-app>

複製程式碼

  Struts2配置到這裡就可以了,啟動一下,看看成不成功。if 成功 then 繼續下一步,else 檢查一下導的包對不對,或者配置檔案的路徑和內容有沒有錯。

3. 配置Spring

  首先,匯入Spring所需的Jar包(先不考慮整合所需的包)。

  接下來,建立Spring配置檔案,我建立的配置檔案為applicationContext.xml,放在/WEB-INF目錄下。配置檔案如下:

<?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-4.1.xsd ">
</beans>

4. 整合Struts2與Spring

  1)在web.xml配置檔案中配置一個上下文初始化引數(context-param),配置片段如下:

<context-param>
      <param-name>contextConfigLocation</param-name><!--這個引數用於指定Spring配置檔案的位置-->
      <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

       2)web.xml配置檔案中配置監聽器org.springframework.web.context.ContextLoaderListener,用於監聽ServletContext的載入,配置檔案片段如下:

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

  3)匯入Struts2的Spring外掛包struts2-spring-plugin-2.3.16.3.jar,這是一個Struts2的外掛包,用於在Struts2中引入Spring。  

  4)啟動伺服器,看看成功沒有。(注意:檢查一下有沒有把需要的包都複製到/WEB-INF/lib中)

5. 建立一個測試用的Action,並在struts.xml和applicationContext.xml中配置,

  Action類:

複製程式碼

package way.blog.struts2spring.action;

import com.opensymphony.xwork2.ActionSupport;

public class TestAction extends ActionSupport{

    private static final long serialVersionUID = 1L;

    public TestAction() {
    }
    
    public String execute(){
        return "success";
    }
    
}

複製程式碼

  applicationContext.xml配置片段:

<bean id="testAction" class="way.blog.struts2spring.action.TestAction" scope="prototype">
</bean>

   struts.xml配置片段:

<package name="test" extends="struts-default" namespace="/">
   <!-- 這裡action的class屬性我們不填實現類的類名,而是填這個action在Spring配置中的bean的名稱 -->
   <action name="testAction" class="testAction" method="execute">
      <result name="success">/index.jsp</result>
   </action>
</package>

  執行伺服器並訪問該Action,頁面成功跳轉到/index.jsp,說明Struts2和Spring整合成功。

 二、(重點)對關鍵配置的分析

  1. 整合過程中,web.xml裡新增的初始化引數(context-param) contextConfigLocation,是用來指定Spring配置檔案的位置的,在這個例子中,引數的值為/WEB-INF/applicationContext.xml,那這個引數是被誰使用的呢?我們可以猜到可能與web.xml中新增的監聽器有關,接著往下看。

  2. web.xml中新增的監聽器org.springframework.web.context.ContextLoaderListener,繼承了ContextLoader類,實現了ServletContextListener介面,所以它是在監聽Web應用的狀態(啟動和關閉),即監聽ServletContext物件的狀態(初始化和銷燬),這裡我檢視Spring的原始碼:

  ContextLoaderListener類片段:

複製程式碼

/**
 * Initialize the root web application context.
 */
 @Override
 public void contextInitialized(ServletContextEvent event) {
     initWebApplicationContext(event.getServletContext());
 }

複製程式碼

  監聽器監聽ServletContext的初始化,在初始化完成後呼叫contextInitialized方法,contextInitialized方法中呼叫了父類ContextLoader中的initWebApplicationContext方法,並把初始化完成的ServletContext物件作為引數傳入,我們看看ContextLoader的原始碼:

  ContextLoader原始碼片段:

複製程式碼

  1     /**
  2      * Initialize Spring's web application context for the given servlet context,
  3      * using the application context provided at construction time, or creating a new one
  4      * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
  5      * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
  6      * @param servletContext current servlet context
  7      * @return the new WebApplicationContext
  8      * @see #ContextLoader(WebApplicationContext)
  9      * @see #CONTEXT_CLASS_PARAM
 10      * @see #CONFIG_LOCATION_PARAM
 11      */
 12     public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
 13         if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
 14             throw new IllegalStateException(
 15                     "Cannot initialize context because there is already a root application context present - " +
 16                     "check whether you have multiple ContextLoader* definitions in your web.xml!");
 17         }
 18 
 19         Log logger = LogFactory.getLog(ContextLoader.class);
 20         servletContext.log("Initializing Spring root WebApplicationContext");
 21         if (logger.isInfoEnabled()) {
 22             logger.info("Root WebApplicationContext: initialization started");
 23         }
 24         long startTime = System.currentTimeMillis();
 25 
 26         try {
 27             // Store context in local instance variable, to guarantee that
 28             // it is available on ServletContext shutdown.
 29             if (this.context == null) {
 30                 this.context = createWebApplicationContext(servletContext);
 31             }
 32             if (this.context instanceof ConfigurableWebApplicationContext) {
 33                 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
 34                 if (!cwac.isActive()) {
 35                     // The context has not yet been refreshed -> provide services such as
 36                     // setting the parent context, setting the application context id, etc
 37                     if (cwac.getParent() == null) {
 38                         // The context instance was injected without an explicit parent ->
 39                         // determine parent for root web application context, if any.
 40                         ApplicationContext parent = loadParentContext(servletContext);
 41                         cwac.setParent(parent);
 42                     }
 43                     configureAndRefreshWebApplicationContext(cwac, servletContext);
 44                 }
 45             }
 46             servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
 47 
 48             ClassLoader ccl = Thread.currentThread().getContextClassLoader();
 49             if (ccl == ContextLoader.class.getClassLoader()) {
 50                 currentContext = this.context;
 51             }
 52             else if (ccl != null) {
 53                 currentContextPerThread.put(ccl, this.context);
 54             }
 55 
 56             if (logger.isDebugEnabled()) {
 57                 logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
 58                         WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
 59             }
 60             if (logger.isInfoEnabled()) {
 61                 long elapsedTime = System.currentTimeMillis() - startTime;
 62                 logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
 63             }
 64 
 65             return this.context;
 66         }
 67         catch (RuntimeException ex) {
 68             logger.error("Context initialization failed", ex);
 69             servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
 70             throw ex;
 71         }
 72         catch (Error err) {
 73             logger.error("Context initialization failed", err);
 74             servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
 75             throw err;
 76         }
 77     }    
 78 
 79   protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
 80         if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
 81             // The application context id is still set to its original default value
 82             // -> assign a more useful id based on available information
 83             String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
 84             if (idParam != null) {
 85                 wac.setId(idParam);
 86             }
 87             else {
 88                 // Generate default id...
 89                 wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
 90                         ObjectUtils.getDisplayString(sc.getContextPath()));
 91             }
 92         }
 93 
 94         wac.setServletContext(sc);
 95         String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
 96         if (configLocationParam != null) {
 97             wac.setConfigLocation(configLocationParam);
 98         }
 99 
100         // The wac environment's #initPropertySources will be called in any case when the context
101         // is refreshed; do it eagerly here to ensure servlet property sources are in place for
102         // use in any post-processing or initialization that occurs below prior to #refresh
103         ConfigurableEnvironment env = wac.getEnvironment();
104         if (env instanceof ConfigurableWebEnvironment) {
105             ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
106         }
107 
108         customizeContext(sc, wac);
109         wac.refresh();
110     }

複製程式碼

   原始碼看起來很複雜,但是隻是出於理解的目的,所以我們只看重點的地方。 

  以上第30行處建立了一個WebApplicationContext物件(其實,WebApplicationContext只是這個物件實現的介面之一,通過第33行的型別強轉我們可以看出這個物件還實現了ConfigurableWebApplicationContext介面),這個物件就是Spring的上下文物件,通過這個物件我們可以獲取Spring中定義的Bean(還記得單獨使用Spring時用的ClassPathXmlApplicationContext或FileSystemXmlApplicationContext嗎?)。

  第43行處的configureAndRefreshWebApplicationContext方法使用ServletContext物件對WebApplicationContext物件進行配置。

  第95行處獲取了ServletContext中的初始化引數CONFIG_LOCATION_PARAM,並在97行將該引數的值設定為WebApplicationContext的配置檔案位置。其實常量CONFIG_LOCATION_PARAM就是字串"contextConfigLocation",也就是說web.xml中配置的初始化引數在這裡被用到了,這也印證了我們上面的猜想。

  建立並配置了Spring的上下文物件之後,在46行處,ServletContext物件將該WebApplicationContext物件設定為自己的一個屬性,屬性名為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。

  剩下的就是異常處理的程式碼了。

  我們發現,在ContextLoaderListener監聽到ServletContext初始化完成後,只不過是建立了一個Spring的上下文物件,並將其設定為ServletContext物件的一個屬性而已。我們可以知道的是,這個Spring的上下文物件肯定會在建立Action物件的時候被用到,但是Struts2是在什麼時候獲取這個物件,並在哪裡使用這個物件來獲取Action物件的bean呢?

  3. 為了確定Action物件的建立時機,我使用了一個小技巧,我在TestAction的構造方法中手動丟擲了一個異常,這樣我就可以根據異常資訊跟蹤呼叫建立Action物件的方法路徑了。

    修改後的TestAction構造方法: 

1     public TestAction() throws Exception{
2         throw new Exception("建立Action物件時手動丟擲的異常");
3     }

    訪問TestAction時的異常資訊:

複製程式碼

 1 嚴重: Exception occurred during processing request: Unable to instantiate Action, testAction,  defined for 'testAction' in namespace '/'Error creating bean with name 'testAction' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [way.blog.struts2spring.action.TestAction]: Constructor threw exception; nested exception is java.lang.Exception: 建立Action物件時手動丟擲的異常
 2 Unable to instantiate Action, testAction,  defined for 'testAction' in namespace '/'Error creating bean with name 'testAction' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [way.blog.struts2spring.action.TestAction]: Constructor threw exception; nested exception is java.lang.Exception: 建立Action物件時手動丟擲的異常
 3     at com.opensymphony.xwork2.DefaultActionInvocation.createAction(DefaultActionInvocation.java:316)
 4     at com.opensymphony.xwork2.DefaultActionInvocation.init(DefaultActionInvocation.java:397)
 5     at com.opensymphony.xwork2.DefaultActionProxy.prepare(DefaultActionProxy.java:194)
 6     at org.apache.struts2.impl.StrutsActionProxy.prepare(StrutsActionProxy.java:63)
 7     at org.apache.struts2.impl.StrutsActionProxyFactory.createActionProxy(StrutsActionProxyFactory.java:37)
 8     at com.opensymphony.xwork2.DefaultActionProxyFactory.createActionProxy(DefaultActionProxyFactory.java:58)
 9     at org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:552)
10     at org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77)
11     at org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:99)
12     at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
13     at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
14     at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
15     at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
16     at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
17     at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
18     at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
19     at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950)
20     at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
21     at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
22     at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
23     at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
24     at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:313)
25     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
26     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
27     at java.lang.Thread.run(Thread.java:745)
28 Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'testAction' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [way.blog.struts2spring.action.TestAction]: Constructor threw exception; nested exception is java.lang.Exception: 建立Action物件時手動丟擲的異常
29     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1101)
30     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1046)
31     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:504)
32     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
33     at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
34     at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
35     at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:956)
36     at com.opensymphony.xwork2.spring.SpringObjectFactory.buildBean(SpringObjectFactory.java:151)
37     at com.opensymphony.xwork2.ObjectFactory.buildBean(ObjectFactory.java:171)
38     at com.opensymphony.xwork2.factory.DefaultActionFactory.buildAction(DefaultActionFactory.java:22)
39     at com.opensymphony.xwork2.ObjectFactory.buildAction(ObjectFactory.java:141)
40     at com.opensymphony.xwork2.DefaultActionInvocation.createAction(DefaultActionInvocation.java:297)
41     ... 24 more
42 Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [way.blog.struts2spring.action.TestAction]: Constructor threw exception; nested exception is java.lang.Exception: 建立Action物件時手動丟擲的異常
43     at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:163)
44     at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:89)
45     at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1094)
46     ... 35 more
47 Caused by: java.lang.Exception: 建立Action物件時手動丟擲的異常
48     at way.blog.struts2spring.action.TestAction.<init>(TestAction.java:13)
49     at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
50     at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
51     at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
52     at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
53     at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:147)
54     ... 37 more

複製程式碼

   異常資訊很長,但是我們可以過濾掉一些跟我們本次分析無關的,只保留與Struts2和Spring有關的資訊,即紅色和藍色字型部分。

  通過第11行到第3行,我們可以看出從StrutsPrepareAndExecuteFilter的doFilter方法開始如何通過一層層呼叫來建立Action物件,意料之中的是,我們發現了Struts2和Spring的銜接點,即上面的35-37行(藍色字型),Struts2呼叫了Spring上下文物件(AbstractApplicationContext物件,猜測應該就是上面ContextLoaderListener中建立的Spring上下文物件轉換來的,下面將驗證這一點)的getBean方法!!!

  Struts2中的一個物件引起了我們的注意——SpringObjectFactory,正是這個物件的buildBean方法完成了對Spring上下文物件的呼叫。我們繼續檢視原始碼:

  com.opensymphony.xwork2.SpringObjectFactory原始碼片段:

複製程式碼

 1 /**
 2  * Simple implementation of the ObjectFactory that makes use of Spring's application context if one has been configured,
 3  * before falling back on the default mechanism of instantiating a new class using the class name. <p/> In order to use
 4  * this class in your application, you will need to instantiate a copy of this class and set it as XWork's ObjectFactory
 5  * before the xwork.xml file is parsed. In a servlet environment, this could be done using a ServletContextListener.
 6  *
 7  * @author Simon Stewart ([email protected])
 8  */
 9 public class SpringObjectFactory extends ObjectFactory implements ApplicationContextAware {
10     private static final Logger LOG = LoggerFactory.getLogger(SpringObjectFactory.class);
11 
12     protected ApplicationContext appContext;
13 /*
14  *      省略............  
15  */
16     /**
17      * Set the Spring ApplicationContext that should be used to look beans up with.
18      *
19      * @param appContext The Spring ApplicationContext that should be used to look beans up with.
20      */
21     public void setApplicationContext(ApplicationContext appContext)
22             throws BeansException {
23         this.appContext = appContext;
24         autoWiringFactory = findAutoWiringBeanFactory(this.appContext);
25     }
26 /*
27  *      省略............  
28  */
29     /**
30      * Looks up beans using Spring's application context before falling back to the method defined in the {@link
31      * ObjectFactory}.
32      *
33      * @param beanName     The name of the bean to look up in the application context
34      * @param extraContext
35      * @return A bean from Spring or the result of calling the overridden
36      *         method.
37      * @throws Exception
38      */
39     @Override
40     public Object buildBean(String beanName, Map<String, Object> extraContext, boolean injectInternal) throws Exception {
41         Object o;
42         
43         if (appContext.containsBean(beanName)) {
44             o = appContext.getBean(beanName);
45         } else {
46             Class beanClazz = getClassInstance(beanName);
47             o = buildBean(beanClazz, extraContext);
48         }
49         if (injectInternal) {
50             injectInternalBeans(o);
51         }
52         return o;
53     }
54 /*
55  *      省略............  
56  */
57 }

複製程式碼

  以上只截取了SpringObjectFactory中與我們分析有關的部分。我們來分析一下,首先,這個類繼承了Struts2的ObjectFactory類,而通過檢視文件中的說明或者原始碼(這裡就不粘出來了),我們可以發現,這個ObjectFactory類是Struts2很重要的一個類,它用於建立所有Struts2的核心物件,包括Action, Interceptor, Result等。而SpringObjectFactory繼承了ObjectFactory,說明通過將Struts2預設的ObjectFactory類替換為SpringObjectFactory就可以實現由Spring來建立物件了。

  看一看SpringObjectFactory的原始碼和註釋,buildBean(String,Map<String,Object>,boolean)方法覆蓋了ObjectFactory中的對應方法,它接收的第一個引數,即是我們在struts.xml配置檔案中為action指定的class屬性。我們看到,這個方法首先是嘗試從appContext中獲取對應名稱的bean,如果失敗,才把該名稱當做類名去建立物件。還記得我們前面的一個問題嗎?Struts2怎麼知道什麼時候把action配置中的class屬性當做bean的名稱,什麼時候又把它當做類名?這裡就是答案了。我們從方法註釋上也可以看到,該方法先嚐試從Spring的上下文中獲取對應名稱的物件,如果失敗,才使用父類的方法根據類名去建立新的物件。

  謎團已經逐步解開,但是還有一個問題。注意,SpringObjectFactory中的ApplicationContext物件appContext是通過setApplicationContext方法傳入的,那是由誰傳入的?傳入的是不是前面在ContextLoaderListener中建立的那個WebApplicationContext物件呢?

  4. 我為了解決上面的問題想了很久,最後才發現,我一直忽略了之前匯入的struts2-spring-plugin-xxx.jar這個包,也許這就是問題的答案了。通過檢視該包,發現一個StrutsSpringObjectFactory類,這個類繼承了上面提到的SpringObjectFactory,

  org.apache.struts2.spring.StrutsSpringObjectFactory原始碼片段:

複製程式碼

  1 /**
  2  * Struts object factory that integrates with Spring.
  3  * <p/>
  4  * Spring should be loaded using a web context listener
  5  * <code>org.springframework.web.context.ContextLoaderListener</code> defined in <code>web.xml</code>.
  6  *
  7  */
  8 public class StrutsSpringObjectFactory extends SpringObjectFactory {
  9     private static final Logger LOG = LoggerFactory.getLogger(StrutsSpringObjectFactory.class);
 10     /*
 11      *省略...
 12      */
 13     /**
 14      * Constructs the spring object factory
 15      * @param autoWire The type of autowiring to use
 16      * @param alwaysAutoWire Whether to always respect the autowiring or not
 17      * @param useClassCacheStr Whether to use the class cache or not
 18      * @param servletContext The servlet context
 19      * @since 2.1.3
 20      */
 21     @Inject
 22     public StrutsSpringObjectFactory(
 23             @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,required=false) String autoWire,
 24             @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT,required=false) String alwaysAutoWire,
 25             @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_USE_CLASS_CACHE,required=false) String useClassCacheStr,
 26             @Inject ServletContext servletContext,
 27             @Inject(StrutsConstants.STRUTS_DEVMODE) String devMode,
 28             @Inject Container container) {
 29           
 30         super();
 31         boolean useClassCache = "true".equals(useClassCacheStr);
 32         if (LOG.isInfoEnabled()) {
 33             LOG.info("Initializing Struts-Spring integration...");
 34         }
 35 
 36         Object rootWebApplicationContext =  servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
 37 
 38         if(rootWebApplicationContext instanceof RuntimeException){
 39             RuntimeException runtimeException = (RuntimeException)rootWebApplicationContext;
 40             LOG.fatal(runtimeException.getMessage());
 41             return;
 42         }
 43 
 44         ApplicationContext appContext = (ApplicationContext) rootWebApplicationContext;
 45         if (appContext == null) {
 46             // uh oh! looks like the lifecycle listener wasn't installed. Let's inform the user
 47             String message = "********** FATAL ERROR STARTING UP STRUTS-SPRING INTEGRATION **********\n" +
 48                     "Looks like the Spring listener was not configured for your web app! \n" +
 49                     "Nothing will work until WebApplicationContextUtils returns a valid ApplicationContext.\n" +
 50                     "You might need to add the following to web.xml: \n" +
 51                     "    <listener>\n" +
 52                     "        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>\n" +
 53                     "    </listener>";
 54             LOG.fatal(message);
 55             return;
 56         }
 57         
 58         String watchList = container.getInstance(String.class, "struts.class.reloading.watchList");
 59         String acceptClasses = container.getInstance(String.class, "struts.class.reloading.acceptClasses");
 60         String reloadConfig = container.getInstance(String.class, "struts.class.reloading.reloadConfig");
 61 
 62         if ("true".equals(devMode)
 63                 && StringUtils.isNotBlank(watchList)
 64                 && appContext instanceof ClassReloadingXMLWebApplicationContext) {
 65             //prevent class caching
 66             useClassCache = false;
 67 
 68             ClassReloadingXMLWebApplicationContext reloadingContext = (ClassReloadingXMLWebApplicationContext) appContext;
 69             reloadingContext.setupReloading(watchList.split(","), acceptClasses, servletContext, "true".equals(reloadConfig));
 70             if (LOG.isInfoEnabled()) {
 71             LOG.info("Class reloading is enabled. Make sure this is not used on a production environment!", watchList);
 72             }
 73 
 74             setClassLoader(reloadingContext.getReloadingClassLoader());
 75 
 76             //we need to reload the context, so our isntance of the factory is picked up
 77             reloadingContext.refresh();
 78         }
 79 
 80         this.setApplicationContext(appContext);
 81 
 82         int type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;   // default
 83         if ("name".equals(autoWire)) {
 84             type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
 85         } else if ("type".equals(autoWire)) {
 86             type = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE;
 87         } else if ("auto".equals(autoWire)) {
 88             type = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT;
 89         } else if ("constructor".equals(autoWire)) {
 90             type = AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR;
 91         } else if ("no".equals(autoWire)) {
 92             type = AutowireCapableBeanFactory.AUTOWIRE_NO;
 93         }
 94         this.setAutowireStrategy(type);
 95 
 96         this.setUseClassCache(useClassCache);
 97 
 98         this.setAlwaysRespectAutowireStrategy("true".equalsIgnoreCase(alwaysAutoWire));
 99 
100         if (LOG.isInfoEnabled()) {
101             LOG.info("... initialized Struts-Spring integration successfully");
102         }
103     }
104 }

複製程式碼

  這個類中只有一個方法,就是構造方法,在36行處我們驚奇的發現,我們之前存入ServletContext物件中的屬性ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE終於又出現了!!!該屬性的值就是在ContextLoaderListener中建立的那個Spring上下文物件,這裡將其獲取出來,並在第80行處呼叫了父類,即SpringObjectFactory的setApplicationContext方法將其賦值給繼承自父類的ApplicationContext型別成員變數appContext。到這裡就解決了上面問題的,我們證明了SpringObjectFactory中用到的ApplicationContext物件就是之前ContextLoaderListener中建立的,而且該物件由StrutsSpringObjectFactory的構造方法中呼叫父類的setApplicationContext方法傳入。

  5. 通過上面的分析我們確定了最後會用StrutsSpringObjectFactory類代替Struts中原來的ObjectFactory。那麼是在哪裡發生替換的呢?我先看了看struts核心包中的struts-default.xml檔案,發現了我們要找的預設的ObjectFactory的定義:

  struts-default.xml片段:

  <bean class="com.opensymphony.xwork2.ObjectFactory" name="struts"/>

  我們再檢視struts核心包中的default.properties檔案中定義的常量,我們找到了這一段:

1 ### if specified, the default object factory can be overridden here
2 ### Note: short-hand notation is supported in some cases, such as "spring"
3 ###       Alternatively, you can provide a com.opensymphony.xwork2.ObjectFactory subclass name here
4 # struts.objectFactory = spring

  也就是說只要我們將struts.objectFactory常量的值覆蓋,換成我們自己定義的ObjectFactory物件,就可以覆蓋原來的預設ObjectFactory了。我們再看看struts2-spring-plugin-xxx.jar外掛包中的struts-plugin.xml檔案,真相大白了!!!!

  struts-plugin.xml檔案片段: 

<bean type="com.opensymphony.xwork2.ObjectFactory" name="spring" class="org.apache.struts2.spring.StrutsSpringObjectFactory" />
    
<!--  Make the Spring object factory the automatic default -->
<constant name="struts.objectFactory" value="spring" />

  我們看到這裡定義了一個名為"spring"的ObjectFactory物件,其實現類正是StrtutsSpringObjectFactory,並且接下來設定了struts.objectFactory常量,將其設定成了我們定義的"spring"物件。

  我們知道,Struts2在載入配置檔案的時候會在Classpath中的尋找struts-plugin.xml檔案,並自動將其載入,這樣就完成了將Struts2與Spring的整合了。

----------------------------------------------------------------------------