1、ViewResolver(视图解析器)
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="1"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/"
p:suffix=".jsp" p:order="2"/>
视图解析器(实现接口 org.springframework.web.servlet.ViewResolver) 用来把 ModelAndView 对象的逻辑视图名解析成一个用于将结果渲染给用户的视图 Bean。Spring 有 4 种 ViewResolver 实现,相当于不同 Struts2 的 result 类型:
·InternalResourceViewResolver -- 将逻辑视图名解析为一个用模板文件(如 JSP 和 Velocity 等模板) 渲染的视图对象。
·BeanNameViewResolver --解析为一个 DispatcherServlet 应用上下文中的视图 Bean
·ResourceBundleViewResolver --解析为 ResourceBundler 中的视图对象
·XmlViewResolver --从一个 XML 文件中解析视图 Bean,这个文件是从 DispatcherServlet 应用上下文中分离出来的。
(1)InternalResourceViewResolver 的使用,假如 Controller 中是 return new ModelAndView("userDetail"),要转向到 /WEB-INF/jsp/userDetail.jsp 时应配置为:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix"><value>/WEB-INF/jsp/</value></property>
<property name="suffix"><value>.jsp</value></property>
</bean>
"/WEB-INF/jsp/userDetail.jsp" 中分别包含了前面指定的前缀、视图逻辑名和后缀名。
如果这个 JSP 中使用了 JSTL 标签,就应该为 InternalResourceViewResolver 指定 viewClass 属性为 JstlView,这时候的配置如下:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
<property name="prefix"><value>/WEB-INF/jsp/</value></property>
<property name="suffix"><value>.jsp</value></property>
</bean>
这个 JstlView 就能解析 JSTL 特定的请求属性,也就让你可以在 JSP 中利用 JSTL 的化支持了(P276)
(2)BeanNameViewResolver 的使用,用如下的配置:
<bean id="beanNameViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean id="courseList" class="com.unmi.controller.CourseListPdfview"/>
对于 return new ModelAndView("courseList"); 就会被 beanNameViewResolver 解析到 courseList 来显示结果(P278)
(3)XmlViewResolver 与 BeanNameViewResolver 的工作方式类似,只不过它不是从主应用上下文中寻找视图 Bean,而是在一个独立的 XML 文件中查找,用法如下:
<bean id="xmlViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="location"><value>/WEB-INF/training-views.xml</value></property>
</bean>
这时候,对于 return new ModelAndView("courseList"); 就会到 /WEB-INF/training-views.xml 中寻找 courseList 的 Bean。XmlViewResolver 的缺少 location 属性值为 /WEB-INF/views.xml(P278)
(4)ResourceBundleViewResolver,一看这名字中的 ResourceBundle 就是跟属性文件、国际化有关。来看看这样一个配置:
<bean name="bundleViewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename"><value>views</value></property>
</bean>
并在 classpath 下有四个属性文件,文件名及内容分别是:
views.properties 文件和 views_zh_CN.properties 文件的内容是:
courseList.class=com.unmi.training.mvc.CourseListPdfView
views_en_US.properties 文件内容是:
courseList.class=com.unmi.training.mvc.CourseListExcelView
views_de_DE.properties 文件的内容是:
courseList.class=org.springframework.web.servlet.view.JstlView
#这下面就是设置 JstlView 的 url 属性
courseList.url=/WEB-INF/jsp/courseList.jsp
courseList.class=org.springframework.web.servlet.view.JstlView
很好也很自然的理解,通过上面的配置就能根据不同区域的用户用不同类型的视图呈现给用户,例如对于 new ModelAndView("courseList"),中国人访问解析到了 CourseListPdfView(PDF 展示),美国人访问显示了一个 Excel 表格(CourseListExcelView),给德国的是一个使用 JSTL 标签 JSP 页面。也了解到视图 Bean 是根据 properties 文件中的配置来初始化的(P279)
项目中使用模板页面居多,所以常用的是 InternalResourceViewResolver。不过我常见得这一视图解析器比起 Struts 和 WebWork 有不足之处resolver是,它的逻辑视图名还携速了文件名信息,要导向到别的 JSP 文件还得修改代码。其他三个视图解析器 BeanNameViewResolver、XmlViewResolver 和 ResoureBundlerViewResolver 的区别只在它们从哪儿获得视图实现,应根据实际情况选用。
如果一个系统中要多个视图解析器,该怎么办呢?那就在配置文件中配置多个了,用 order 属性指定应用的顺序,从小至大,即 ViewResolver 的:
<property name="order"><value>1</value></property>
书中 P281 中给 InternalResourceViewResolver 配置了 order 属性,而我查过 Spring 1.2.4/1.2.8/1.2.9 的 InternalResourceViewResolver 代码,它并没有直接或简接的实现 Ordered 接口,也就是不能对它配置 order 属性。在 Spring 2.0 的 InternalResourceViewResolver 才简接的实现了 Ordered 接口,看得出这本书确有些超前。那还有一个问题,一个逻辑视图名,给哪个视图解析器都是能执行的,那优先级低的视图解析器在什么时机能应用上(P281)
2、ContentNegotiatingViewResolver
(1)REST内容协商介绍
RESTful服务中很重要的一个特性即是同一资源,多种表述.也即如下面描述的三种方式:
1.使用http request header: Accept
- GET /user/123 HTTP/1.1
- Accept: application/xml //将返回xml格式数据
- GET /user/123 HTTP/1.1
- Accept: application/json //将返回json格式数据
2.使用扩展名
- /user/123.xml 将返回xml格式数据
- /user/123.json 将返回json格式数据
- /user/123.html 将返回html格式数据
3.使用参数
- /user/123?format=xml //将返回xml数据
- /user/123?format=json //将返回json数据
而以上三种各有优缺点:
1.使用Accept header:
这一种为教科书中通常描述的一种,理想中这种方式也是最好的,但如果你的资源要给用户直接通过浏览器访问(即html展现),那么由于浏览器的差异,发送上来的Accept Header头将是不一样的. 将导致服务器不知要返回什么格式的数据给你. 下面是浏览器的Accept Header
- chrome:
- Accept:application/xml,application/xhtml+xml,textml;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
- firefox:
- Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
- IE8:
- Accept:image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/x-silverlight, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, */*
2.使用扩展名
丧失了同一url多种展现的方式,但现在这种在实际环境中是使用最多的.因为更加符合程序员的审美观.
3.使用参数
现在很多open API是使用这种方式,但可能由于要编写的字符较多,所以较少使用.
带着上面的选择: 使用扩展名,我们来看一下spring中如何配置这部分.
(2)Spring配置
现spring完成内容协商(content negotiation)的工作是由ContentNegotiatingViewResolver来完成的.它的工作模式支持我上面讲的三种,
ContentNegotiatingViewResolver 是根据客户提交的MimeType(如 text/html,application/xml)来跟服务端的一组viewResover的MimeType相比较,如果符合,即返回 viewResover的数据.
而 /user/123.xml, ContentNegotiatingViewResolver会首先将 .xml 根据mediaTypes属性将其转换成 application/xml,然后完成前面所说的比较.
下面是ContentNegotiatingViewResolver的完全配置.
这个类它实现了ViewResolver。但它并不直接解析视图,而是委托给别人。默认情况,它是从spring 上下文,查找视图解析器,并调用这些解析器。也可以在初始化这个bean的时候,设置它的解析器属性(viewResolvers),这是个list类型的属性。
请注意,要让这个视图解析器正常工作,需要设置比别人更高的优先级(默认为Ordered.HIGHEST_PRECEDENCE)。
配置的例子
<!--根据客户端的不同的请求决定不同的view进行响应, 如 /rest/1.json /rest/1.xml /rest?format=json /rest?format=xml-->
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<!-- 设置为true以忽略对Accept Header的支持-->
<property name="ignoreAcceptHeader" value="true" />
<!-- true,开启扩展名支持,false关闭支持 -->
<property name="favorPathExtension" value="false" />
<!-- 用于开启 /userinfo/123?format=json的支持 -->
<property name="favorParameter" value="true" />
<!--在没有扩展名和参数时即: "/user/1" 时的默认展现形式-->
<property name="defaultContentType" value="text/html" /><!--参数值至mimeType的映射,即 /rest?format=json json是key,application/json就是value 暂时只支持json和xml-->
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
</map>
</property><property name="viewResolvers">
<!-- 关闭所有的解析器,防止它在查找候选视图时多个解析器都运行 -->
<list></list>
</property><property name="defaultViews">
<list>
<!-- for application/json -->
<bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
<!-- 重新设置 objectMapper-->
<property name="objectMapper">
<bean class="org.codehaus.jackson.map.ObjectMapper">
<!--设置objectMapper的serializationConfig的serializationInclusion属性,以忽略null对象-->
<property name="serializationConfig.serializationInclusion">
<value type="org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion">NON_NULL</value>
</property>
</bean>
</property>
</bean>
<!-- for application/xml -->
<bean class="org.springframework.web.servlet.view.xml.MarshallingView">
<property name="marshaller">
<bean class="org.springframework.oxm.castor.CastorMarshaller">
<property name="validating" value="false"></property>
</bean>
</property>
</bean>
</list>
</property>
</bean>这个视图解析器根据请求类型来返回视图。就是上面说的三种方式。按照以下的先后顺序:
1.如果设置了setFavorPathExtension(boolean)为true,会根据后缀来使用不同的数据格式。
2.如果设置了setFavorParameter(boolean)为true,会根据请求参数来设置不同的数据格式。参数名是通过属性parameterName设置的,默认private String parameterName = "format";
3.如果ignoreAcceptHeader未设置为false(未关闭),那么会通过accpet 来获得相应的信息。在其内部中,是通过
protected List<MediaType> getMediaTypes(HttpServletRequest request) {
方法来根据请求过来的信息,返回对应的需要的回应的context-type的。首先判断的是扩展名是否开启,
if (this.favorPathExtension) {
如果开启,先获取扩展名,如果扩展名不为null,
查看是否有可以处理的扩展,这个是将扩展名作为key,来获取的
MediaType mediaType = this.mediaTypes.get(extension);
这里的mediaTypes就是刚才的配置
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
<entry key="xml" value="application/xml" />
</map>
</property>
设置的值。
如果我们访问的是/127.0.0.1/daowole/rest.xml
扩展名就是xml,这个key对应的value就是application/xml。有则返回媒体类型。扩展名得方式还会判断useJaf是否开启。开启的话会使用jaf(Java Activation Framework)来获取,也就是通过
String mediaType = fileTypeMap.getContentType(fileName);
来获取媒体类型。这里的fileTypeMap是一个静态常量。它里面建立了一张还算完整的根据文件名字的后缀,映射的媒体表格。
如(其中的一部分)
c++=MIMETypeEntry: text/plain, c++, bcpio=MIMETypeEntry: application/x-bcpio, bcpio, xwd=MIMETypeEntry: image/x-xwindowdump, xwd
如果是从jaf取出来的,会自动往mediaTypes添加内容。
if (mediaType != null) {
this.mediaTypes.putIfAbsent(extension, mediaType);
}否则进入下一个处理方式,
下一个处理方式是参数,判断favorParameter是否开启。处理类似
通过参数。只是将参数值,作为key从mediaTypes获取。然后然后是accept方式,判断ignoreAcceptHeader是否开启。处理类似,但不需要从mediaTypes获取信息。
如果没有,查看defaultContentType是否为空,返回的是defaultContentType
否则返回return Collections.emptyList();一个空的内容。
其次根据返回的List<MediaType> requestedMediaTypes,controller返回的viewname,以及locale,来生成多个候选的View(List<View>)
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);/**发现候选的方式是遍历它的viewResolvers属性(这个类在构造的时候,会从spring上下文把所有的解析器取出来,当然我们也可以通过bean初始化的property元素设置它的viewResolvers属性),将每个viewResolver拿出来,调用它的resolveViewName,来获取view,如果view有值,表示这个视图解析器可以处理(可以处理不表示只有这一个是候选的,候选可能是多个的)。这个时候还没结束*/
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
//不为null,添加到候选视图
candidateViews.add(view);
}//这里还需要遍历需要返回给客户端的媒体类型
for (MediaType requestedMediaType : requestedMediaTypes) {
//根据媒体类型,返回扩展名,
List<String> extensions = getExtensionsForMediaType(requestedMediaType);
这部做的是和通过key获取value方式反了一下,而是通过value获取key了,也就是通过媒体,获取这个媒体在mediaType中的key,这里返回的key可能是多个,也就是说一种媒体可以对应各种文件格式,如json可以对应application/json text/json。List<String> extensions = getExtensionsForMediaType(requestedMediaType);
//遍历这个媒体,通过String viewNameWithExtension = viewName + "." + extension;操作后,生成新的逻辑视图,并查看这个逻辑是否也可以在当前的视图解析器处理出一个物理视图。如我们的action是/daowole/test.json 本来我们controller返回的逻辑视图是 order/addsuccess,它对应的物理视图是 order/addsuccess.ftl,而这个时候会增加逻辑视图, order/addsuccess.json 那么对应的物理视图就是 order/addsuccess.json.ftl
for (String extension : extensions) {
String viewNameWithExtension = viewName + "." + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
//可以处理,将它也加入到候选视图。
if (view != null) {
candidateViews.add(view);
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
//这里会把我们在上面设置的默认视图(视图不是视图解析器)也加入进去。我们这里设置的默认视图只有一个json
candidateViews.addAll(this.defaultViews);
}
当然这里的视图比较特殊,它不需要逻辑视图名,就可以直接返回responsebody了。随后需要从候选视图查找一个唯一可以处理的视图,也就是
View bestView = getBestView(candidateViews, requestedMediaTypes);代码如下
MediaType bestRequestedMediaType = null;
View bestView = null;
//首先是遍历需要回应给客户端的媒体信息
for (MediaType requestedMediaType : requestedMediaTypes) {
//这里是遍历每个候选视图
for (View candidateView : candidateViews) {
if (StringUtils.hasText(candidateView.getContentType())) {
//获取当前候选时候的媒体信息
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
//判断这个候选视图媒体是否在位于需要回应的媒体中,至于怎么判断是否位于这个媒体中我们不做详细介绍,但候选视图的媒体刚好包含于回应视图中的,那么
if (requestedMediaType.includes(candidateContentType)) {
bestRequestedMediaType = requestedMediaType;
//baseView就确定了。
bestView = candidateView;
break;
}
}
}
if (bestView != null) {
if (logger.isDebugEnabled()) {
logger.debug("Returning [" + bestView + "] based on requested media type '" + bestRequestedMediaType +"'");
}
break;
}
}
return bestView;这里返回后,也就是
View bestView = getBestView(candidateViews, requestedMediaTypes);执行后if (bestView != null) {
return bestView;
}else {
//这里生成是一个404
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
}
return NOT_ACCEPTABLE_VIEW;
}else {
if (logger.isDebugEnabled()) {
logger.debug("No acceptable view found; returning null");
}
//任何都不返回,这个useNotAcceptableStatusCode设置的时候要注意了,如果为true,而且没有找到任何可以处理的view,那么就会404,否则,它就return null。当spring mvc 的servlet接受到的view为null,那么就会交给下一个视图解析器处理,那么原来的解析器就依然可以起到效果了。这种情况是我们大多数应用所希望的。所以spring mvc在设计这个类得时候把它的默认值设置为false。
return null;
}
}
3、Spring国际化
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
p:basename="messages"/>如果你使用过Eclipse的国际化,或者用过Eclipse的“外部化字符串”向导(Eclipse主菜单:源代码->外部化字符串),那么对Spring提供的国际化功能应该是非常容易理解,两者基本一样,或者说各种Java程序的国际化方式都基本一样。
先谈谈Eclipse国际化的两个组成部分:*.properties的资源文件、获取资源文件内容的Message类。
而Spring则和Eclipse的处理类似:资源文件两者是一样的,不同语言的翻译放在不同的资源文件里,连起名规则都一样;Eclipse的Message类要自己写(代码通用,复制以前项目的即可,或用Eclipse的向导生成一个也行),Spring则已经有写好的Message类,我们在IoC的xml文件里注册一下即可使用(也可以实现Spring的MessageSource接口,自己来写一个Message类,代码并不复杂,不过这没什么必要,用Spring提供的就行了)。
无论是Eclipse的Message类,还是Spring的自带的Message类,或是我们自己写一个Message类,都是使用JDK的java.util.ResourceBundle类来实现*.properties文件的读取。
我们要让这个程序能够根据使用者的语言情况输出不同的字符,比如:对英文使用者输出“ChenGang”,对中文使用者输出“陈刚”,对台湾使用输出“陳剛”等等。这个需求的实现方法如下:
1、创建一系列的资源文件
在cn.com.chengang.spring包下创建以下文件:
(1)messages.properties(默认:英文),内容仅一句,如下
chengang=Giles
“chengang”是键值,Giles是要输出的英文字符串
(2)messages_zh_CN.properties(简体中文)
chengang=\u9648\u521A
“\u9648\u521A”是UNICODE码,对应的中文是“陈刚”
(3)messages_ zh_TW.properties(繁体中文)
chengang=\u9673\u525B
“\u9673\u525B”对应的中文是“陳剛”
2、修改bean.xml
将Spring自带的org.springframework.context.support.ResourceBundleMessageSource类注册到bean.xml中,这个类的作用是获取资源文件的内容,注册到IoC的bean.xml文件中是为了自动获得此类的对象(Spring做了一些简化编程的处理)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="Chinese" class="cn.com.chengang.spring.Chinese"/>
<bean id="American" class="cn.com.chengang.spring.American"/>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>cn.com.chengang.spring.messages</value>
</list>
</property>
</bean>
</beans>
代码说明:
l id="messageSource" 的设置是不变的、必须的。
l ResourceBundleMessageSource是Spring的一个Message类。这里还有一个选择,用ReloadableResourceBundleMessageSource 类,此类可以提供不用重启即可重新加载资源文件的特性(前者对资源文件只加载一次)。对于那种有热修改资源文件的需求,后者比较合适,只是后者在效率上有可能有损耗,因为至少要多一些检查资源文件是否改变的代码(这只是我的猜测,我没有仔佃去读这段的源码)。
l “basenames”是不变的、必须的。它是ResourceBundleMessageSource的一个属性,在源代码中的定义是“private String[] basenames;”,可见它是一个字符串数组。
l “cn.com.chengang.spring.messages”是把资源文件的位置传入到basenames属性中。注意:三个资源文件只需要将共同的主名(红色字体)传入:messages.properties、messages_zh_CN.properties、messages_zh_TW.properties。
3、使用。修改MessageTest类,如下
package cn.com.chengang.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class MessageTest {
public static void main(String[] args) {
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
String str = ctx.getMessage("chengang", null, null);
System.out.println(str);
}
}
代码说明:
(1)main方法里
l 第一句取得bean.xml文件的配置信息。
l 第二句从资源文件里得到键值chengang对应的字符串。
l 第三句将字符串打印出来,结果是打印的是“陈刚”,说明读取的是messages_zh_CN.properties资源文件。
(2)ctx.getMessage("chengang", null, null);有三个参数:
l 第一个是资源文件的键值;
l 第二个是资源文件字符串的参数,由于本字符串没有参数,所以用一个null(后面给出了一个用到字符串参数的实例);
l 第三个是一个java.util. Locale类型的参数。参数为null,则表示根据使用者的语言环境来选择Locale,因为我用的是中文版的windows,所以在取字符串时它自动选择了messages_zh_CN.properties资源文件。
这其中还有一个控制点在JVM,JVM会根据当前操作系统的语言环境进行相应处理,我们可以通过在JVM启动参数中追加“-Duser.language=zh_TW”来设定当前JVM语言类型,通过JVM级的设定,也可以实现自动切换所使用的资源文件类型。
所以这里面的控制语言的方式有三种:从最低层的操作系统的Locale设定,到更上一层的JVM的Locale设定,再到程序一级的Locale设定。我认为最佳的方法是在程序一级进行控制:定义一个统一的Locale静态变量,然后整个系统中只使用这一个变量,以后就可以通过界面操作设置此Locale变量的值,让用户来选择他所需的软件语言。而且我们也可以将此静态变量设成null值,来自动选择资源文件。
这个实例演示了如何使用多个资源文件,以及如何使用字符串参数
(1)在cn.com.chengang.spring包下再创建一个资源文件messagesOther_zh_CN.properties
chengang.info=\u9648\u521A\uFF0C\u7F51\u540D\uFF1A{0}\uFF0C\u82F1\u6587\u540D\uFF1A{1}\uFF0CBlog\uFF1A{2}
其中UNICODE字符串对应的中文是:“陈刚,网名:{0},英文名:{1},Blog:{2}”,这个字符串一共有三个参数。
(2)修改 bean.xml文件
因为basenames属性是一个数组,当然也就可以接收多个资源文件设定。具体修改如下面的红字部份
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="Chinese" class="cn.com.chengang.spring.Chinese"/>
<bean id="American" class="cn.com.chengang.spring.American"/>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>cn.com.chengang.spring.messages</value>
<value>cn.com.chengang.spring.messagesOther</value>
</list>
</property>
</bean>
</beans>
(3)修改MessageTest类,加入几行使用的代码
String[] strArgs = new String[3];
strArgs[0]="混北民工";
strArgs[1]="Giles";
strArgs[2]="http://blog.csdn.net/glchengang";
str = ctx.getMessage("chengang.info", strArgs, null);
System.out.println(str);
打印出来的结果就是:“陈刚,网名:混北民工,英文名:Giles,Blog:http://blog.csdn.net/glchengang”