Spring MVC容器和IOC容器引起的国际化问题

在讲述Spring国际化中遇到的问题之前,首先看下Spring MVC容器和Spring IOC容器,双容器的相关背景知识。

 

双容器

 

通常所说的spring 容器,只的是IOC容器。容器的主要作用是在程序启动时把所需的bean提前加载到内存(本质上是存储在一个ConcurrentHashMap里,DefaultListableBeanFactory

类的beanDefinitionMap字段),恰好如果web层使用的是Spring MVC,这时会产生另一个新的容—Spring MVC容器。这两个容器的启动入口都是在web.xml配置:

 

<!--  step1 Spring 容器启动监听器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
<!-- spring IOC 容器配置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-config.xml</param-value>
    </context-param>
 
    <!-- step2 Spring mvc 容器配置 -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup> <!-- 程序启动时装在该servlet -->
    </servlet>
 
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
 

 

这份web.xml配置指出:Spring IOC容器是通过ContextLoaderListener启动,并根据spring-config.xml的配置读取对应的bean列表加载到容器;Spring MVC容器是通过DispatcherServlet启动,并根据spring-mvc.xml的配置读取对应的bean列表加载到容器。

 

两个容器的初始化过程

 

两个容器的初始化顺序为: Spring IOC容器àSpring mvc容器。首先看Spring IOC容器,通过阅读ContextLoaderListener的源码,可以得知其通过initWebApplicationContext方法创建容器,类型为XmlWebApplicationContext。创建完成后,调用容器自身的refresh()方法加载到容器,实际调用的是XmlWebApplicationContext的父类AbstractApplicationContextrefresh()方法,容器的加载过程是该方法再通过调用refreshBeanFactory()创建容器自己的beanFactory,并读取配置文件把bean加载到容器:

protected final void refreshBeanFactory() throws BeansException {
        if(this.hasBeanFactory()) {//如果容器已有beanFactory,先销毁
            this.destroyBeans();
            this.closeBeanFactory();
        }
 
        try {
            //创建一个DefaultListableBeanFactory类型的BeanFactory
            DefaultListableBeanFactory ex = this.createBeanFactory();
            ex.setSerializationId(this.getId());
            this.customizeBeanFactory(ex);
           
            //通过该方法读取配置文件spring-config.xml中所有的bean加载到容器
            this.loadBeanDefinitions(ex);
            Object var2 = this.beanFactoryMonitor;
            synchronized(this.beanFactoryMonitor) {
                //把刚创建的DefaultListableBeanFactory赋值给容器
                this.beanFactory = ex;
            }
        } catch (IOException var5) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);
        }
    }

 

AbstractApplicationContextrefresh()方法执行完成后,标志着Spring IOC容器 加载完成。

 

Spring IOC容器 加载完成后,才开始Spring mvc容器的初始化。上面已经提到Spring mvc容器是通过DispatcherServlet加载的,DispatcherServlet本质上是一个Servlet,容器的初始化过程是在FrameworkServletinitWebApplicationContext()方法中完成:

protected WebApplicationContext initWebApplicationContext() {
        //获取父容器---spring ioc容器,通过ContextLoaderListener已经创建完毕
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
        WebApplicationContext wac = null;
 
        //省略部分代码
 
        if(wac == null) {
            //创建spring mvc容器类型为XmlWebApplicationContext,父容器为spring ioc容器
            wac = this.createWebApplicationContext(rootContext);
        }
 
        if(!this.refreshEventReceived) {
            this.onRefresh(wac);
        }
        //省略部分代码
        return wac;
    }
 

 

该方法通过调用createWebApplicationContext方法是创建容器,通过onRefresh方法把bean加载到容器。首先来看createWebApplicationContext方法:

protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
        //获取容器类型,这里还是XmlWebApplicationContext
        Class contextClass = this.getContextClass();
        if(this.logger.isDebugEnabled()) {
            this.logger.debug("Servlet with name \'" + this.getServletName() + "\' will try to create custom WebApplicationContext context of class \'" + contextClass.getName() + "\'" + ", using parent context [" + parent + "]");
        }
 
        if(!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Fatal initialization error in servlet with name \'" + this.getServletName() + "\': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
        } else {
            //根据contextClass 创建容器
            ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
            wac.setEnvironment(this.getEnvironment());
           
            //设置父容器,这里是Spring IOC容器
            wac.setParent(parent);
            wac.setConfigLocation(this.getContextConfigLocation());
           
            //调用AbstractApplicationContext的refresh()方法,加载bean到容器
            this.configureAndRefreshWebApplicationContext(wac);
            return wac;
        }
    }

 

这里contextClass是容器的类型,为XmlWebApplicationContext。然后根据contextClass创建Spring MVC容器,并设置之前已经创建好的Spring IOC容器为其父容器,也就是说Spring MVC容器和Spring IOC容器虽然是独立的,但也存在父子关系。最后的configureAndRefreshWebApplicationContext方法会调用容器自身的refresh()方法,加载bean到容器完成初始化,refresh()方法的主要执行过程在Spring Ioc容器加载过程中已有描述,这里不再累述。

 

两个容器的父子关系

 

再说下Spring MVC容器和Spring IOC容器的父子关系:子容器对父容器是可见的,但父容器对子容器不可见,这有点类似java类的父子关系。所谓可以见,就是在调用容器的getBean方法时,子容器会在自己的容器里先查找有没有对应的bean,如果没有就会到父容器中查找,具体代码实现参考AbstractBeanFactory类的doGetBean方法:

protected <T> T doGetBean(String name, Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {
        final String beanName = this.transformedBeanName(name);
        //首先在自己的容器中查找
        Object sharedInstance = this.getSingleton(beanName);
        Object bean;
        if (sharedInstance != null && args == null) {
            if (this.logger.isDebugEnabled()) {
                if (this.isSingletonCurrentlyInCreation(beanName)) {
                    this.logger.debug("Returning eagerly cached instance of singleton bean \'" + beanName + "\' that is not fully initialized yet - a consequence of a circular reference");
                } else {
                    this.logger.debug("Returning cached instance of singleton bean \'" + beanName + "\'");
                }
            }
 
            bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition) null);
        } else {
            if (this.isPrototypeCurrentlyInCreation(beanName)) {
                throw new BeanCurrentlyInCreationException(beanName);
            }
 
            //如果自己的容器中没找到,在到父容器中查找
            BeanFactory ex = this.getParentBeanFactory();
            if (ex != null && !this.containsBeanDefinition(beanName)) {
                String var24 = this.originalBeanName(name);
                if (args != null) {
                    return ex.getBean(var24, args);
                }
 
                return ex.getBean(var24, requiredType);
            }
            //省略其他代码
        }
        //省略其他代码
}
 

 

换句话说,Spring MVC容器可以使用Spring IOC容器中的bean,反之则不行。通常spring mvc项目中分为 Controller层、Service层、Dao层,一般Controller层使用Spring MVC容器,其他的放到Spring IOC容器,Controller层对ServviceDao层可见,但反之则不行,这也就是事务处理在Controller层失效的原因。

 

Spring的双容器有时,会引发一些问题,比如 下面讲到的国际化资源 可见性问题。

 

Spring国际化中遇到的问题

 

最近做的项目需要满足国际化需求,直接使用Spring MVC视图渲染技术做页面国际化处理。但对于有些ajax的异步数据接口里的文字信息,由于没有视图页面,只能在接口数据返回时自行处理。处理方式为:

1、首先在xml中配置国际化资源文件,为了方便在Service层和Dao层使用,下列配置会配置到Spring IOC容器对应的spring-config.xml配置文件。

<!-- 国际化资源文件 -->
    <bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames" >
            <list>
                <value>test</value>
            </list>
        </property>
        <property name="defaultEncoding" value="UTF-8"/>
</bean>

 

该配置会自动读取test开头的国际化资源文件列表,这里有三个:



 

为了测试,配置文件内容均为:my.name=lilei

 

2、在需要使用国际化的地方,直接从容器中获取messageSource,调用其getMessage方法。这里就发现一个奇怪的问题,在Service层、Dao层使用没有问题,但在Controller层使用却报异常:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xxxxController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'messageSource' must be of type
[org.springframework.context.support.ResourceBundleMessageSource], but was actually of type [org.springframework.context.support.DelegatingMessageSource]

Controller中的代码为:

 

@Resource
private ResourceBundleMessageSource messageSource;
 
@ResponseBody
    @RequestMapping("/i18n")
    public String Testi18n(Locale locale){  // Locale 入参
        String val = messageSource.getMessage("my.name", null, locale);
        System.out.println(val);
        return val;
    }
 

 

通过分析异常日志可以发现,其大概意思是在通过容器的getBean方法获取messageSource bean时,期望messageSource的类型为ResourceBundleMessageSource类型,但得到的是DelegatingMessageSource类型。

 

由于上述配置在Spring IOC容器中的类型为ResourceBundleMessageSourceDelegatingMessageSource肯定不是我们期望。再深入分析,messageSource是在Controller层使用,Controller属于Spring MVC容器,其依赖的bean优先从Spring MVC容器中查找,如果找不到再到Spring IOC容器中查找。

 

根据异常信息发现找到的是messageSourceDelegatingMessageSource类型,可以推测下,应该是在Spring MVC容器中有一个messageSource类型为DelegatingMessageSource,首先被找到后就返回了,没有继续到父容器中查找。带着这样个想法,又读了一遍容器初始化的源码,在AbstractApplicationContextrefresh()方法中,发现其调用this.initMessageSource()方法进行资源文件加载,方法内容为:

protected void initMessageSource() {
        ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
        //判断容器中是否已经包含名称为messageSource 的资源对象
        if(beanFactory.containsLocalBean("messageSource")) {
            this.messageSource = (MessageSource)beanFactory.getBean("messageSource", MessageSource.class);
            if(this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
                HierarchicalMessageSource dms = (HierarchicalMessageSource)this.messageSource;
                if(dms.getParentMessageSource() == null) {
                    dms.setParentMessageSource(this.getInternalParentMessageSource());
                }
            }
 
            if(this.logger.isDebugEnabled()) {
                this.logger.debug("Using MessageSource [" + this.messageSource + "]");
            }
        } else {
            //如果容器中不包含名为messageSource的资源对象,就新建一个DelegatingMessageSource类型的资源对象放入容器
            DelegatingMessageSource dms1 = new DelegatingMessageSource();
            dms1.setParentMessageSource(this.getInternalParentMessageSource());
            this.messageSource = dms1;
            beanFactory.registerSingleton("messageSource", this.messageSource);
            if(this.logger.isDebugEnabled()) {
                this.logger.debug("Unable to locate MessageSource with name \'messageSource\': using default [" + this.messageSource + "]");
            }
        }
 
}
 

 

果然在容器中如果没有名称为messageSource的资源对象,会自动创建一个名称为messageSource 类型为DelegatingMessageSource的资源对象,并放入容器。

 

这就是上述异常发生的根本原因。

 

解决办法

 

方法一:把下列资源对象配置从Spring IOC容器对应的spring-config.xml 迁移到Spring MVC容器对应的

spring-mvc.xml中,即:
<bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames" >
            <list>
                <value>test</value>
            </list>
        </property>
        <property name="defaultEncoding" value="UTF-8"/>
</bean>
 

 

通过refresh()方法的源码发现,如果容器中已经存在名称为messageSource的资源对象,就不会再去创建DelegatingMessageSource类型的资源对象。

 

缺点:通过配置迁移,可以在Controller层中使用messageSource资源对象。但由于Sping IOC容器对该messageSource资源对象不可见,在Service层或Dao层就无法使用该对象获取国际化消息了。

 

方法二:资源对象配置还是Spring IOC容器对应的spring-config.xml中,只是把bean名称改下,不使用messageSource即可。

<bean id="messageSourceIoc"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames" >
            <list>
                <value>test</value>
            </list>
        </property>
        <property name="defaultEncoding" value="UTF-8"/>
</bean>

这种方式Controller层、Service层、Dao层都可以使用,只是bean的名称看起来不雅观。

 

方法三:把资源文件拆分成两份:testMVCtestIOCController层使用的资源文件为testMVC,其他层使用的资源文件为testIOC 分别在spring-mvc.xmlspring-config.xml中配置资源对象:

spring-mvc.xml中配置为:

<bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames" >
            <list>
                <value>testMVC</value>
            </list>
        </property>
        <property name="defaultEncoding" value="UTF-8"/>
</bean>
 

 spring-config.xml中配置为:

<bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames" >
            <list>
                <value>testIOC</value>
            </list>
        </property>
        <property name="defaultEncoding" value="UTF-8"/>
</bean>
 

 

Bean的名称都是messageSource,但不会报错,也不会覆盖。因为他们分别属于两个独立的容器,在使用时也不会互相干扰。这种方式的缺点是:两个文件中有可能有重复配置,比如在Controller层、Service层都会使用同样的配置值。

 

这三种方式,可以根据实际项目情况进行选择。

转载请注明出处:

http://moon-walker.iteye.com/blog/2395925

猜你喜欢

转载自moon-walker.iteye.com/blog/2395925