在struts2的struts-default.xml中定义了一个name为autowiring拦截器,实现类是com.opensymphony.xwork2.spring.interceptor.ActionAutowiringInterceptor,它的作用是在struts2和spring整合时为action注入spring上下文ApplicationContext(Action需要实现org.springframework.context.ApplicationContextAware接口),并使用com.opensymphony.xwork2.inject.Container对象为action注入其他属性。
首先说明一下struts2与spring的整合。
要实现struts2与spring的整合,只需将struts2-spring-plugin-2.x.x.jar加入到项目中即可。在该jar包中存在文件struts-plugin.xml,在struts2启动时会被加载,文件内容如下:
<struts> <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" /> <constant name="struts.class.reloading.watchList" value="" /> <constant name="struts.class.reloading.acceptClasses" value="" /> <constant name="struts.class.reloading.reloadConfig" value="false" /> <package name="spring-default"> <interceptors> <interceptor name="autowiring" class ="com.opensymphony.xwork2.spring.interceptor.ActionAutowiringInterceptor"/> </interceptors> </package> </struts>
这里它将struts2框架常量struts.objectFactory设置为"spring",这里其实是使用了缩写形式,全称是"org.apache.struts2.spring.StrutsSpringObjectFactory"。这个缩写的"spring"是和bean配置中的name属性相对应的。默认情况下struts2框架创建的对象都是由ObjectFactory实例化的,ObjectFactory提供了与其他IoC容器(如Spring等)集成的方法。覆盖这个ObjectFactory的类必须继承ObjectFactory类或者它的任何子类,并且带有一个无参构造方法或者构造方法中的参数全部使用struts2的@Inject注解。上述代码使用org.apache.struts2.spring.StrutsSpringObjectFactory代替了默认的ObjectFactory。该ObjectFactory使得struts2中的bean可以由spring来装配,默认情况下框架使用的自动装配策略是name,也就是说框架会根据spring中bean的name属性自动装配,可选的装配策略还有type、auto、constructor,我们可以根据常量struts.objectFactory.spring.autoWire来进行设置。
在struts2与spring进行整合的时候还需要在web.xml中添加如下信息来启动spring:
<listener> <listener-class >org.springframework.web.context.ContextLoaderListener</listener-class > </listener>
但如果没有配置这个会出现什么情况呢?如果未配置上述代码,启动web服务器会出现如下错误FATAL 2011-12-28 11:53:34:272 - ********** FATAL ERROR STARTING UP
STRUTS-SPRING INTEGRATION ********** Looks like the Spring listener was not configured for your web app! Nothing will work until WebApplicationContextUtils returns a valid ApplicationContext. You might need to add the following to web.xml: <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> ERROR 2011-12-28 11:53:34:288 - Dispatcher initialization failed java.lang.NullPointerException at com.opensymphony.xwork2.spring.SpringObjectFactory.getClassInstance(SpringObjectFactory.java:220) …………
错误中显示是出现了NullPointerException(SpringObjectFactory.java:220), 出现该异常的代码是:
if(appContext.containsBean(className))
此处的appContext是org.springframework.context.ApplicationContext,说明在SpringObjectFactory类(StrutsSpringObjectFactory的直接父类)中需要存在org.springframework.context.ApplicationContext对象的引用,此处的 ApplicationContext需要什么时候进行装配呢?在StrutsSpringObjectFactory类中只存在一个构造方法
public class StrutsSpringObjectFactory extends SpringObjectFactory { private static final Logger LOG = LoggerFactory.getLogger(StrutsSpringObjectFactory.class); //@Inject //public StrutsSpringObjectFactory( // @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,required=false) String autoWire, // @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_USE_CLASS_CACHE,required=false) String useClassCacheStr, // @Inject ServletContext servletContext) { // this(autoWire, "false", useClassCacheStr, servletContext); //} /** * Constructs the spring object factory * @param autoWire The type of autowiring to use * @param alwaysAutoWire Whether to always respect the autowiring or not * @param useClassCacheStr Whether to use the class cache or not * @param servletContext The servlet context * @since 2.1.3 */ @Inject public StrutsSpringObjectFactory( @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,required=false) String autoWire, @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT,required=false) String alwaysAutoWire, @Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_USE_CLASS_CACHE,required=false) String useClassCacheStr, @Inject ServletContext servletContext, @Inject(StrutsConstants.STRUTS_DEVMODE) String devMode, @Inject Container container) { super(); boolean useClassCache = "true".equals(useClassCacheStr); LOG.info("Initializing Struts-Spring integration..."); Object rootWebApplicationContext = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); if(rootWebApplicationContext instanceof RuntimeException){ RuntimeException runtimeException = (RuntimeException)rootWebApplicationContext; LOG.fatal(runtimeException.getMessage()); return; } ApplicationContext appContext = (ApplicationContext) rootWebApplicationContext; if (appContext == null) { // uh oh! looks like the lifecycle listener wasn't installed. Let's inform the user String message = "********** FATAL ERROR STARTING UP STRUTS-SPRING INTEGRATION **********\n" + "Looks like the Spring listener was not configured for your web app! \n" + "Nothing will work until WebApplicationContextUtils returns a valid ApplicationContext.\n" + "You might need to add the following to web.xml: \n" + " <listener>\n" + " <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>\n" + " </listener>"; LOG.fatal(message); return; } String watchList = container.getInstance(String.class, "struts.class.reloading.watchList"); String acceptClasses = container.getInstance(String.class, "struts.class.reloading.acceptClasses"); String reloadConfig = container.getInstance(String.class, "struts.class.reloading.reloadConfig"); if ("true".equals(devMode) && StringUtils.isNotBlank(watchList) && appContext instanceof ClassReloadingXMLWebApplicationContext) { //prevent class caching useClassCache = false; ClassReloadingXMLWebApplicationContext reloadingContext = (ClassReloadingXMLWebApplicationContext) appContext; reloadingContext.setupReloading(watchList.split(","), acceptClasses, servletContext, "true".equals(reloadConfig)); LOG.info("Class reloading is enabled. Make sure this is not used on a production environment!", watchList); setClassLoader(reloadingContext.getReloadingClassLoader()); //we need to reload the context, so our isntance of the factory is picked up reloadingContext.refresh(); } this.setApplicationContext(appContext); int type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME; // default if ("name".equals(autoWire)) { type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME; } else if ("type".equals(autoWire)) { type = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE; } else if ("auto".equals(autoWire)) { type = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT; } else if ("constructor".equals(autoWire)) { type = AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR; } else if ("no".equals(autoWire)) { type = AutowireCapableBeanFactory.AUTOWIRE_NO; } this.setAutowireStrategy(type); this.setUseClassCache(useClassCache); this.setAlwaysRespectAutowireStrategy("true".equalsIgnoreCase(alwaysAutoWire)); LOG.info("... initialized Struts-Spring integration successfully"); } }
在上述代码中从servletContext中取到一个名称为"org.springframework.web.context.WebApplicationContext.ROOT"的属性值,将其强制转换为ApplicationContext类型,之后调用SpringObjectFactory类setApplicationContext方法将spring上下文传递给了SpringObjectFactory。spring上下文是什么时候放置到servletContext中的呢?我们接下来看一下org.springframework.web.context.ContextLoaderListener类,这个监听器到底做了什么? 我们知道ContextLoaderListener是在服务器初始化的时候执行contextInitialized这个方法,该方法的代码如下:
public void contextInitialized(ServletContextEvent event) { this.contextLoader = createContextLoader(); this.contextLoader.initWebApplicationContext(event.getServletContext()); }
可以看出主要起作用的应该是initWebApplicationContext方法,初始化WebApplicationContext,我们进去看看:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) throws IllegalStateException, BeansException { if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { throw new IllegalStateException( "Cannot initialize context because there is already a root application context present - " + "check whether you have multiple ContextLoader* definitions in your web.xml!"); } servletContext.log("Initializing Spring root WebApplicationContext"); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started"); } long startTime = System.currentTimeMillis(); try { // Determine parent for root web application context, if any. ApplicationContext parent = loadParentContext(servletContext); // Store context in local instance variable, to guarantee that // it is available on ServletContext shutdown. this.context = createWebApplicationContext(servletContext, parent); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); currentContextPerThread.put(Thread.currentThread().getContextClassLoader(), this.context); if (logger.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; } }
上面代码,将spring上下文放置到了servletContext中,因此只要通过org.springframework.web.context.ContextLoaderListener类启动了spring,那么在struts2中通过servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)或者ActionContext.getContext().getApplication().get(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)就能取到spring上下文对象
spring把ApplicationContext放在application里面,然后struts2在application中把这个对象取出来,放在SpringObjectFactory里面,然后struts2与spring就实现了集成,当我们没有在web.xml里面配置监听器,也就是没有把ApplicationContext的对象放在application里面,所以struts2去application里面去取的时候会是null,这就是为什么没有配置监听器那里会抛NullPointerException异常。
当struts2和spring集成之后,struts2将允许Spring创建Action、Interceptror和Result,并且由Struts创建的对象能够被Spring装配。这样就可以在配置action时不使用类名,而使用spring中bean的name值了。如果在spring中找不到与Action的class值相匹配的bean,struts2框架会根据class值初始化一个action对象,并对action对象进行装配。
其实SpringObjectFactory已经对Action对象注入了ApplicationContext和其他struts2对象,那么autowiring拦截器岂不是没用了?有一种情况,当项目中同时使用了Struts2和spring,但未将struts.objectFactory的值设置为"spring",即Struts2的objectFactory还是默认的ObjectFactory类,这样action中就不会被注入spring的上下文对象,就需要使用autowiring拦截器了。autowiring拦截器会从application中获取到spring上下文对象,初始化一个SpringObjectFactory对象,使用该对象对Action对象进行装配。