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

2017-10-12 19:32:02来源:作者:人点击

分享

在讲述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的父类AbstractApplicationContext的refresh()方法,容器的加载过程是该方法再通过调用refreshBeanFactory()创建容器自己的beanFactory,并读取配置文件把bean加载到容器:

protected final void refreshBeanFactory() throws BeansException {if(this.hasBeanFactory()) {//如果容器已有beanFactory,先销毁this.destroyBeans();this.closeBeanFactory();} try {//创建一个DefaultListableBeanFactory类型的BeanFactoryDefaultListableBeanFactory 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);} }

 

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

 

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

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) {//获取容器类型,这里还是XmlWebApplicationContextClass 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层对Servvice、Dao层可见,但反之则不行,这也就是事务处理在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中的代码为:

 

@Resourceprivate 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容器中的类型为ResourceBundleMessageSource,DelegatingMessageSource肯定不是我们期望。再深入分析,messageSource是在Controller层使用,Controller属于Spring MVC容器,其依赖的bean优先从Spring MVC容器中查找,如果找不到再到Spring IOC容器中查找。

 

根据异常信息发现找到的是messageSource是DelegatingMessageSource类型,可以推测下,应该是在Spring MVC容器中有一个messageSource类型为DelegatingMessageSource,首先被找到后就返回了,没有继续到父容器中查找。带着这样个想法,又读了一遍容器初始化的源码,在AbstractApplicationContext的refresh()方法中,发现其调用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的名称看起来不雅观。

 

方法三:把资源文件拆分成两份:testMVC和testIOC,Controller层使用的资源文件为testMVC,其他层使用的资源文件为testIOC, 分别在spring-mvc.xml和spring-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

SpringMVC容器IOC容器国际化

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台