1,概述
在配置<mvc:annotation-driven.../>元素之后,它会为SpringMVC配置HandlerMapping、HandlerAdapter、HandlerExceptionResovler这三个特殊的Bean,它们解决了URL—Controller的处理方法的映射。
当Controller的处理方法处理完成后,该处理方法可返回:String(逻辑视图名)、View(视图对象)、ModelAndView(同时包括Model与逻辑视图或View),而View对象才代表具体的视图,因此,SpringMVC必须必须使用ViewResolver将逻辑视图名(String)解析成实际视图(View对象)。
ViewResolver的作用示意图:
ViewResolver本身是一个接口,它提供了如下常用类:
- AbstractCachingViewResolver:抽象视图解析器,负责缓存视图。很多视图都需要在使用前做好准备,它的子类可以缓存视图。
- XmlViewResolver:能接收XML配置文件的视图解析器,该XML配置文件的DTD与Spring的配置文件的dtd相同。默认的配置文件是/WEB-INF/views.xml。
- BeanNameViewResolver:它会直接从已有的容器中获取id为viewName的Bean作为View。
- ResourceBunldeViewResolver:使用ResourceBundle中的Bean定义实现ViewResolver,这个ResourceBundle由bundle的basename指定。这个bundle通常被定义在一个位于CLASSPATH中的属性文件中。
- UrlBasedViewResolver:该视图解析器允许将视图名解析成URL,它不需要显示配置,只要视图名。
- InternalResourceViewResolver:UrlBasedViewResolver的子类,能方便地支持Servlet和JSP视图以及JstlView和TilesView等子类,它是在实际开发中常用的视图解析器,也是SpringMVC默认的视图解析器。
- FreeMarkerViewResolver:UrlBasedViewResolver的子类,能方便地支持FreeMarker视图与之类似的还有GroovyMarkupViewResolver、TilesViewReoslver。
- ContentNegotiatingViewResolver:它不是一个具体的视图解析器,它会根据请求的MIME类型来“动态”选择合适的视图解析器,然后将视图解析工作委托给所选择的视图解析器负责。
2,UrlBasedViewResolver
2.1,UrlBasedViewResolver的功能与用法
UrlBasedViewResolver继承了AbstractCachingViewResolver基类,是ViewResolver接口的一个简单实现类。UrlBasedViewResolver使用一种拼接URL的方式来解析视图,它可通过prefix属性指定一个前缀,也可通过suffix属性指定一个后缀,然后将逻辑视图名加上指定的前缀和后缀,这样就得到了实际视图的URL。
例如,指定prefix="/WEB-INF/content/",suffix=".jsp",当控制器的处理方法返回的视图名为“error”时,UrlBasedViewResolver解析得到的视图URL为/WEB-INF/content/error.jsp。默认的prefix和suffix属性值都是空字符串。
在使用URLBasedViewResolver作为视图解析器时,它支持在逻辑视图名中使用forword:前缀或redirect:前缀,其中:
- forward:前缀代表转发到指定的视图资源,依然是同一个请求,因此转发到目标资源后请求参数、请求属性都不会丢失。
- redirect:前缀代表重定向到指定的视图资源,重新发送请求,重定向会生成一个新的请求,因此重定向后请求参数、请求属性都会丢失。
在使用UrlBasedViewResolver时必须指定viewClass属性,表示解析成那种视图,一般用的较多(非100%)的是InternalResourceView,用于呈现普通的JSP视图;如果希望使用JSTL,则应该将该属性值指定为JstlView。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven /> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller" /> <!-- 定义视图解析器 --> <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp" p:viewClass="org.springframework.web.servlet.view.InternalResourceView"/> </beans>
@Controller public class UserController { @Resource(name = "userService") private UserService userService; @PostMapping("/login") public String login(String username, String pass, Model model) { if (userService.userLogin(username,pass)>0){ model.addAttribute("tip","欢迎您,登录成功!"); return "success"; } model.addAttribute("tip","对不起,您输入的用户名、密码不正确!"); return "error"; } }
如果打开UrlBasedViewResolver类的源代码,则可以看到它重写了createView()方法,其代码片段如下:
protected View createView(String viewName, Locale locale) throws Exception { //判断是否不支持处理该视图名,返回null则表明传给下一个视图解析器链的下一个节点 if (!this.canHandle(viewName, locale)) { return null; } else { String forwardUrl; //判断是否以redirect:开头 if (viewName.startsWith("redirect:")) { //去掉redirect开头 forwardUrl = viewName.substring("redirect:".length()); //创建RedirectView,执行重定向 RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible()); String[] hosts = this.getRedirectHosts(); if (hosts != null) { view.setHosts(hosts); } return this.applyLifecycleMethods("redirect:", view); } else if (viewName.startsWith("forward:")) { forwardUrl = viewName.substring("forward:".length()); //创建InternalResource,执行转发 InternalResourceView view = new InternalResourceView(forwardUrl); return this.applyLifecycleMethods("forward:", view); } else { //其他情况,则直接调用父类的createView()方法进行处理 return super.createView(viewName, locale); } } }
从源码可以看出,虽然在配置UrlBasedViewResolver视图解析器时指定了viewClass属性,但如果返回的逻辑视图名包含“forward:”前缀,则意味着UrlBasedViewResovler总是使用InternalResourceView视图;但如果返回的逻辑视图名包含“redirect:”前缀,则意味着UrlBasedViewResovler总是使用RedirectView视图进行重定向。
在AbstractCachingViewResolver(UrlBasedViewResolver的父类)的createView()方法内部会调用loadView()抽象方法,UrlBasedViewResolver则实现了该抽象方法。代码如下:
protected View loadView(String viewName, Locale locale) throws Exception { AbstractUrlBasedView view = this.buildView(viewName); View result = this.applyLifecycleMethods(viewName, view); return view.checkResource(locale) ? result : null; }
在loadView()内部实际上是调用了buildView()方法来创建View的,buildView()方法的源代码如下:
protected AbstractUrlBasedView buildView(String viewName) throws Exception { Class<?> viewClass = this.getViewClass(); Assert.state(viewClass != null, "No view class"); AbstractUrlBasedView view = (AbstractUrlBasedView)BeanUtils.instantiateClass(viewClass); //根据prefix.suffix属性和视图名来构建视图URL view.setUrl(this.getPrefix() + viewName + this.getSuffix()); view.setAttributesMap(this.getAttributesMap()); String contentType = this.getContentType(); //如果设置了contentType属性,则为该视图设置contentType if (contentType != null) { view.setContentType(contentType); } //如果设置了requestContextAttribute属性,则直接传给视图对象 String requestContextAttribute = this.getRequestContextAttribute(); if (requestContextAttribute != null) { view.setRequestContextAttribute(requestContextAttribute); } //根据配置为视图设置exposePathVariables属性 Boolean exposePathVariables = this.getExposePathVariables(); if (exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } //根据配置为视图设置exposeContextBeansAsAttributes属性 Boolean exposeContextBeansAsAttributes = this.getExposeContextBeansAsAttributes(); if (exposeContextBeansAsAttributes != null) { view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); } //根据配置为视图设置exposedContextBeanName属性 String[] exposedContextBeanNames = this.getExposedContextBeanNames(); if (exposedContextBeanNames != null) { view.setExposedContextBeanNames(exposedContextBeanNames); } return view; }
上面源代码除根据prefix、suffix属性和视图名构建视图URL之外,还为视图设置了contentType、requestContextAttribute、attributesMap属性,这些属性都是在配置UrlBasedViewResolver时可额外设置的属性,其中contentType、reqeustContextAttribute都是字符串属性,attributesMap则允许设置一个Map属性,这些属性都会被直接传给UrlBasedViewResolver创建的View对象。
UrlBasedViewResolver还允许设置如下三个属性:
- exposePathVariables:设置是否将路径变量(PathVariable)添加到与视图对应的Model中。
- exposeContextBeansAsAttributes:如果将该属性设为true,则意味着将Spring容器中的Bean作为请求属性暴露给视图页面。
- exposedContextBeanNames:设置只将Spring容器中的那些Bean(多个Bean之间以英文逗号隔开)作为请求属性暴露给视图页面;如果不指定该属性,则暴露Spring容器中的所有Bean。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven /> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller" /> <!-- 定义视图解析器 --> <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp" p:viewClass="org.springframework.web.servlet.view.InternalResourceView" p:exposeContextBeansAsAttributes="true" p:exposedContextBeanNames="now,win"/> </beans>
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <bean id="userService" class="com.ysy.springmvc.Service.UserService"/> <bean id="now" class="java.util.Date"/> <bean id="win" class="javax.swing.JFrame" c:_0="我的窗口"/> </beans>
2.2,InternalResourceViewResolver的功能与用法
InternalResourceViewResolver是UrlBasedViewResolver的子类,它与UrlBasedViewResolver最大的区别在于:在配置InternalResourceViewResolver作为视图解析器时,无须指定viewClass属性。
public InternalResourceViewResolver() { Class<?> viewClass = this.requiredViewClass(); if (InternalResourceView.class == viewClass && jstlPresent) { viewClass = JstlView.class; } this.setViewClass(viewClass); }
protected Class<?> requiredViewClass() { return InternalResourceView.class; }
InternalResourceView的优势在于:它可以"智能"地选择视图类。
- 如果类加载路径中有JSTL类库,它默认使用JstlView作为viewClass。
- 如果类加载路径中没有JSTL类库,他默认使用InternalResourceView作为viewClass。
虽然InternalResourceViewResolver为viewClass(视图类)“智能”地提供了默认值,但是在配置视图解析器时依然可指定viewClass属性,显式配置的viewClass属性将覆盖它“智能”选择的默认值。
此外,还可以在InternalResourceViewResolver的源代码中看到如下buildView()方法的代码:
protected AbstractUrlBasedView buildView(String viewName) throws Exception { InternalResourceView view = (InternalResourceView)super.buildView(viewName); if (this.alwaysInclude != null) { view.setAlwaysInclude(this.alwaysInclude); } view.setPreventDispatchLoop(true); return view; }
在使用InternalResourceView时还可额外配置一个alwaysInclude属性,如果将该属性设为true,则表明程序将inlude视图页面,而不是forward(转发)到视图页面。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven /> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller" /> <!-- 定义视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp"/> </beans>
3,重定向
3.1,重定向视图
不管是UrlBasedView解析器,还是InternalResourceViewResolver解析器,它们都支持为视图名指定“redirect:”前缀,这样即可让视图解析器创建RedirectView来重定向指定视图。
实际上,也可以让控制器的处理方法直接返回RedirectView对象,这样强制DispatcherServlet不再使用正常的视图解析,执行重定向。
视图解析器的作用就是根据逻辑视图名(String)解析出View对象(视图),如果控制器的处理方法直接返回View对象(或者用ModelAndView封装View对象),那么表明该处理方法返回的已经是View对象了,自然就不再需要视图解析器进行解析了。当然,处理方法直接返回View对象并不是好的策略,因为这意味着处理方法与视图形成了硬编码耦合,不利于项目维护。
不管使用“redirect:”前缀执行重定向,还是显式使用RedirectView执行重定向,model中的所有数据都会被附加在URL后作为请求参数传递,由于在URL后追加的请求参数只能是基本类型或String,因此model中复杂类型的数据无法被正常传递。
此外,SpringMVC的重定向与HttpServletResponse的sendRedirect()方法类似,它们都是控制浏览器重新生成一次请求,即相当于在浏览器地址栏中重新输入URL地址发送请求,因此重定向不能访问/WEB-INF/下的JSP页面。
@Controller public class UserController { @Resource(name = "userService") private UserService userService; @PostMapping("/login") public View login(String username, String pass, Model model) { if (userService.userLogin(username,pass)>0){ model.addAttribute("tip","欢迎您,登录成功!"); return new RedirectView("success.jsp"); } model.addAttribute("tip","对不起,您输入的用户名、密码不正确!"); return new RedirectView("error.jsp"); } }
方法使用RedirectView作为返回值,这意味着该控制器会执行重定向,此时model中的数据不会通过请求属性的方式传递,而是以HTTP请求参数的方式(追加到重定向的URL后面)传递:
http://localhost:8080/success.jsp?tip=%3F%3F%3F%3F%3F%3F%3F%3F%21
从结果可以看出SpringMVC重定向的特征:重定向后浏览器地址栏的地址变成重定向的地址,这表明是一次新的请求;model中的数据变成地址栏中地址的请求参数。因此,重定向时model中的数据不再以请求属性的方式传递到重定向目标,在页面上使用${tip}访问不到任何数据,输出一片空白。
3.2,将数据传递给重定向目标
重定向很好的解决“重复提交”的问题,但它带来一个新问题——它丢失请求属性和请求参数。虽然Spring MVC的重定向改进了一步:它会自动将model中的数据拼接在转发的URL后面。但这种改进依然存在两个限制:
- 追加在URL后面的model数据只能是String或基本类型及其包装类;而且追加在URL后面的model数据长度是有限的。
- 追加在URL后面的model数据直接显示在浏览器的地址栏,这可能引发安全问题。
为了解决重定向后model数据丢失的问题,SpringMVC提出了一个“Flash属性”的解决方案,这个方案很简单:SpringMVC会在重定向之前将model中的数据临时保存起来,但重定向后再将临时保存再将临时保存的数据放入新的model中,然后立即清空临时存储的数据。
Spring MVC使用FlashMap来保存model中的临时数据,FlashMap继承了HashMap<String,Object>,其实它就是一个Map对象;而FlashMapManager对象则负责保存、放入model数据,目前SpringMVC为FlashMapManager接口只提供了一个SessionFlashMapManager实体类,这意味着FlashMapManager会使用session临时存储model数据。
对于每个请求,FlashMapManager都会维护“input”和“output”两个FlashMap属性,其中input属性存储了一个请求传入的数据,而output属性则存储了将要传给下一个请求的数据。
打开SessionFlashMapManager,可以看到如下两个方法的源代码:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.web.servlet.support; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.lang.Nullable; import org.springframework.web.servlet.FlashMap; import org.springframework.web.util.WebUtils; public class SessionFlashMapManager extends AbstractFlashMapManager { private static final String FLASH_MAPS_SESSION_ATTRIBUTE = SessionFlashMapManager.class.getName() + ".FLASH_MAPS"; public SessionFlashMapManager() { } @Nullable protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) { HttpSession session = request.getSession(false); return session != null ? (List)session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null; } protected void updateFlashMaps(List<FlashMap> flashMaps, HttpServletRequest request, HttpServletResponse response) { WebUtils.setSessionAttribute(request, FLASH_MAPS_SESSION_ATTRIBUTE, !flashMaps.isEmpty() ? flashMaps : null); } protected Object getFlashMapsMutex(HttpServletRequest request) { return WebUtils.getSessionMutex(request.getSession()); } }
在实际开发中利用“Flash属性”传递数据有两种方式:
- 对于传统的、没有注解修饰的控制器方法,程序可通过RequestContextUtils类的getInputFlashMap()或getOutpuFlashMap()方法来获取FlashMap对象。
FlashMap flashMap=RequestContextUtils.getOutputFlashMap(request); FlashMap flashMap=RequestContextUtils.getInputFlashMap(request);
- 对于使用注解修饰的控制器方法,SpringMVC又提供了一个RedirectAttributes接口来操作“Flash属性”。
RedirectAttributes继承了Model接口,这说明它是一个特殊的、功能更强大的Model接口。实际上,RedirectAttribute主要提供了如下两类方法:
- addAttribute(Object attributeValue):等同于Model的addAttribute()方法,使用自动生成的属性名。
- addAttribute(String attributeName, Object attributeValue):等同于Model的addAttribute()方法。
- addFlashAttribute(Object attributeValue):将属性添加到“Flash属性”中,使用自动生成的属性名。
- addFlashAttribute(String attributeName, Object attributeValue):将属性添加到“Flash属性”中,使用attributeName参数指定的属性名。
从方法上看,RedirectAttributes完全可取代Model,当调用addAttribute()方法添加属性时,其作用等同于Model接口中的方法,依然只是将属性添加到model中;当调用addFlashAttribute()方法添加属性时,这些数据由FlashMapManager负责存储、取出,这样可保证通过addFlashAttribute()方法添加的属性在重定向时不会丢失。
对于开发者而言,其实只要记住RedirectAttribute是一个增强的Model接口,就完全可以用它取代Model接口;如果只希望model数据在转发后有效,那么调用普通的addAttribute()方法即可;如果希望model数据在重定向后有效,则需要调用addFlashAttribute()方法。
Model接口的主要作用就是作为模型传递数据,与ModelMap、RedirectAttributes及其实现类的关系:
Model接口是整个继承体系跟接口,不管是RedirectAttributes接口,还是RedirectAttributesModeMap、ExtendedModelMap实现类,它们要么继承Model接口,要么实现Model接口。
ModelMap是Spring2.0引入、设计不足的残次品。它本身是一个类,并为实现Model接口,但又几乎实现了Model接口中的所有方法,从面向接口编程来看,推荐使用Model接口或RediretAttributes接口作为模型来传递数据。
实际上使用Model接口,还是使用ModelMap类作为模型的,SpringMVC底层都会使用ExtendModelMap作为具体实现,因此控制器的处理方法还是推荐面向Model接口编程,这样更具有更大的灵活性。
@Controller public class UserController { @Resource(name = "userService") private UserService userService; @PostMapping("/login") public View login(String username, String pass, RedirectAttributes attrs) { if (userService.userLogin(username,pass)>0){ attrs.addFlashAttribute("tip","欢迎您,登录成功!"); return new RedirectView("success"); } attrs.addFlashAttribute("tip","对不起,您输入的用户名、密码不正确!"); return new RedirectView("error"); } }
4,其他视图解析器及视图解析器的链式处理
4.1,视图解析器的链式处理
虽然InternalResovler用起来非常简单,但它是一个很“霸道”的视图解析器——它会尝试解析所有的逻辑视图名,比如控制器的处理方法返回了“ysy”字符串(逻辑视图名),他总会解析得到/WEB-INF/content/ysy.jsp作为该视图名的视图资源——实际上,应用中可能根本没有/WEB-INF/content/roma.jsp,应用希望使用其他视图页面来显示“ysy”逻辑视图。
如果希望应用中存在多个视图解析逻辑,则可以在SpringMVC容器中配置多个视图解析器,多个视图解析器会形成链式处理:
视图解析器的作用就是将传入的String对象(逻辑视图名)解析成View对象(实际视图),从上图看出,只有当视图解析器A解析结果为null时,才会将视图名传给视图解析器B(下一个)继续解析——只要视图解析器链上的任意视图解析器将String对象解析成View对象,解析就结束,这个视图名不会被传给下一个视图解析器进行解析。
所有视图解析器都实现了Ordered接口,并实现了该接口中的getOrder()方法,该方法的返回值决定了该视图解析器的顺序——顺序值越大,越排在解析器链后面。因此,视图解析器都允许配置一个order属性,该属性就代表了该视图解析器的顺序值。
由于InternalResourceViewResolver太“霸道”,因此需要将该视图解析器的order属性设置为最大,保证该视图解析器排在最后面;否则,排在InternalResourceViewResolver后面的视图解析器根本没有执行的机会。
4.2,XmlViewResolver
下面在页面中增加一个链接,该链接用于查看作者的部分图书,但请求希望生成一个Excel文档作为视图,那么InternalResourceViewResolver肯定就搞不定了,因为它负责的不是单个视图名解析,而是要对应用中的绝大部分视图名进行解析。因此,它总按“统一”的规则执行解析,总是为逻辑视图名添加“/WEB-INF/content/”前缀、“.jsp”后缀。
此时就考虑使用XmlViewResolver与InternalResourceViewResolver形成视图解析器链,让XmlViewResolver负责解析那些特殊的逻辑视图名,而对于XmlViewResolver解析不了的逻辑视图名,它会放行给后面的InternalResourceViewResolver处理。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven/> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller"/> <!-- 定义视图解析器 --> <mvc:annotation-driven ignore-default-model-on-redirect="true"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp" p:order="10"/> <bean class="org.springframework.web.servlet.view.XmlViewResolver" p:location="/WEB-INF/views.xml" p:order="1"/> </beans>
那么XmlViewResolver到底如何解析视图名呢?打开该类源码,可以看到loadView()方法:
protected View loadView(String viewName, Locale locale) throws BeansException { //根据location属性指定的配置文件创建Spring容器 BeanFactory factory = this.initFactory(); try { //直接查找容器id为viewName的Bean作为View return (View)factory.getBean(viewName, View.class); } catch (NoSuchBeanDefinitionException var5) { //如果找不到对应的Bean,则返回null,放行给下一个视图 return null; } }
XmlViewResolver需要根据location参数创建Spring容器——因此,上面在配置XmlViewResolver解析器时设置了location参数。XmlViewResolver创建的Spring容器是一个全新的容器,它既不是Root容器,也不是SpringMVC的Servlet容器,在这个全新的容器与Root容器、SpringMVC的Servlet容器也不发生关系。
在创建这个全新的Spring容器之后,XmlViewResolver直接返回该容器中id为viewName的Bean作为解析得到的View——这意味着该Spring容器内的所有Bean都应该是View实例。假如控制器的处理方法返回了“books”逻辑视图名,XmlViewResolver将直接从这个容器中查找id为books的Bean作为解析得到的View。
@GetMapping("/viewBooks") public String viewBooks(Model model){ List bookList = new LinkedList(); bookList.add("燕双嘤");bookList.add("杜马");bookList.add("步鹰"); model.addAttribute("books",bookList); return "books"; }
<?xml version="1.0" encoding="utf-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="books" class="com.ysy.springmvc.View.BookExcelDoc" p:sheetName="XmlViewResolver"/> </beans>
public class BookExcelDoc extends AbstractXlsView { private String sheetName; public void setSheetName(String sheetName) { this.sheetName = sheetName; } @SuppressWarnings("unchecked") public void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) { // 创建第一页,并设置页标签 Sheet sheet = workbook.createSheet(this.sheetName); //设置默认列宽 sheet.setDefaultColumnWidth(20); // 定位第一个单元格,即A1处 Cell cell = sheet.createRow(0).createCell(0); cell.setCellValue("Spring-Excel测试"); CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); // 设置使用红色字体 font.setColor(Font.COLOR_RED); // 设置字体下面使用双下划线 font.setUnderline(Font.U_DOUBLE); style.setFont(font); // 为单元格设置样式 cell.setCellStyle(style); // 获取Model中的数据 List<String> books = (List<String>) model.get("books"); // 使用Model中的数据填充Excel表格 for (int i = 0; i < books.size(); i++) { Cell c = sheet.createRow(i + 1).createCell(0); c.setCellValue(books.get(i)); } } }
4.3,ResourceBundleViewResolver的功能与用法
ResourceBundleViewResolver与XmlViewResolver的本质是一样的,它们都会创建一个全新的Spring容器,然后获取该容器中id为viewName的Bean作为解析得到的View——如果查看ResourceBundleViewResolver的源代码,就会发现它的loadView()方法与XmlViewResolver的loadView()方法的源代码几乎完全一样,这说明它们解析View的方法是完全相同的。
ResourceBundleViewResolver与XmlViewResolver的区别主要体现在创建Spring容器的方式上——XmlViewResover需要提供一个配置文件,因此它直接使用配置文件创建Spring容器即可;而ResourceBundleViewResolver要求提供一个属性文件(*.properties文件),因此它要根据该属性文件来创建Spring容器,较为复杂。
ResourceBundleViewResource与XmlViewResource这两个解析器的功能几乎是重复的,差别是配置文件的格式不同。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven/> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller"/> <!-- 定义视图解析器 --> <mvc:annotation-driven ignore-default-model-on-redirect="true"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp" p:order="10"/> <bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver" p:basename="view" p:order="1"/> </beans>
# 配置books Bean的实现类 books.(class)=com.ysy.springmvc.View.BookExcelDoc # 为books Bean的sheetName属性指定值 books.sheetName=ResourceBundleViewResolver
4.4,BeanNameViewResolver的功能与用法
BeanNameViewResolver是XmlViewResolver的简陋版:XmlViewResolver会自行创建一个全新的Spring容器来管理所有的View对象,但BeanNameViewResolver更偷懒,它不创建Spring容器,而是直接从已有的容器中获取id为viewName的Bean作为解析得到的View。
BeanNameViewResovler类中resolveViewName()方法的代码如下:
public View resolveViewName(String viewName, Locale locale) throws BeansException { //直接获取已有的Spring容器 ApplicationContext context = this.obtainApplicationContext(); //如果容器中不包含id为viewName的Bean if (!context.containsBean(viewName)) { //返回null,意味着这该视图交给下一个视图解析器处理 return null; //要求viewName对应的Bean必须是View实体类的实例 } else if (!context.isTypeMatch(viewName, View.class)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Found bean named '" + viewName + "' but it does not implement View"); } return null; } else { //返回容器中id为viewName、类型为View的Bean作为解析得到的View return (View)context.getBean(viewName, View.class); } }
理解BeanNameViewResolver的原理之后,不难理解为何称它为XmlViewResolver的简陋版了。但是从系统设计角度来看,BeanNameViewResolver比XmlViewResolver更差,XmlViewResolver使用专门的配置文件、Spring容器来管理视图Bean,但BeanNameViewResolver直接使用整个应用的Spring配置文件、Spring容器来管理Bean,这显然有点功能混乱。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven/> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller"/> <!-- 定义视图解析器 --> <mvc:annotation-driven ignore-default-model-on-redirect="true"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp" p:order="10"/> <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="1"/> <bean id="books" class="com.ysy.springmvc.View.BookExcelDoc" p:sheetName="XmlViewResolver"/> </beans>
4.5,ContentNegotiatingViewResolver的功能与用法
严格来说,ContentNegotiatingViewResolver并不是真正的视图解析器,因为它并不负责实际的视图解析,它只是多个视图解析器的代理。当一个逻辑视图名到来之后,ContentNegotiatingViewResolver并未直接解析得到实际的View(视图),而是智能地“分发”给系统中有的视图解析器A、视图解析器B、视图解析器C...等。
那么ContentNegotiatingViewResolver怎么知道如何“分发”逻辑视图名呢?它是根据请求的内容类型(contentType)进行分发的,比如用户请求的contentType是application/json,它就会把请求分发给返回JSON视图的解析器处理;用户请求的contentType是text/html,它就会把请求分发给返回HTML视图的解析器处理。
这样又产生了两个问题:
- ContentNegotiatingViewResolver如何判断请求的contentType呢?
- ContentNegotiatingViewResolver如何知道系统包含哪些视图解析器呢?
对于第一个问题,ContentNegotiatingViewResolver判断请求的contentType一共有三种方式:
- 根据请求的后缀,比如请求的后缀是.json,它就判断请求的contentType是application/json;用户请求的后缀是.xls,它就判断请求的contentType是application/vnd.ms-excel......依次类推。
- 根据请求参数(通常是format参数),比如请求的参数为/aa?format=json,它就判断请求的contentType是application/json;请求的参数为/aa?format=xls,它就判断请求的contentType是application/vnd.ms-excel.....依次类推。这种方式默认是关闭的,需要将favorParameteer参数设为true来打开这种判断方式。
- 根据请求的Accept请求头,比如请求的Accept请求头包含text/html,它就判断请求的contentType是text/html。这种判断方式可能出现问题,尤其是当用户使用浏览器发送请求时,Accept请求头完全是由浏览器控制的,用户不能改变这个请求头。
对于第二个问题,ContentNegotiatingViewResolver确定系统包含那些视图解析器有两种方法:
- 显示通过viewResolvers属性进行配置,该属性可接受一个List属性值,这样即可显式列出供ContentNegotiatingViewResolver转发的视图解析器。
- ContentNegotiatingViewResolver还会自动扫描Spring容器中的所有Bean,它会自动将ViewResolver实现类都当成可供ContentNeigotiatingViewResolver。
示例:用户可以向viewBooks.json、viewBooks.xls、viewBooks.pdf和viewBooks(无后缀)发送请求,这4个请求的地址是相同的(只是后缀不同),因此程序会使用同一个处理方法来处理该请求,并返回相同的逻辑视图名。
因为用户向viewBooks.json、viewBooks.xls、viewBooks.pdf和viewBooks(无后缀)发送请求,肯定希望分别看到JSON、Excel文档、PDF文档和JSP响应,此时就轮到ContentNegotiatingViewResolver了,它会根据用户请求的contentType将逻辑视图名分别发给不同的视图解析器。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <html> <body> <h2>Hello World!</h2> <a href="${pageContext.request.contextPath}/viewBooks.pdf">pdf</a> <a href="${pageContext.request.contextPath}/viewBooks.xls">excel</a> <a href="${pageContext.request.contextPath}/viewBooks.json">json</a> <a href="${pageContext.request.contextPath}/viewBooks">jsp</a> </body> </html>
@Controller public class BookController { @GetMapping("/viewBooks") public String viewBooks(Model model){ ArrayList bookList = new ArrayList(); bookList.add("燕双嘤");bookList.add("杜马");bookList.add("步鹰"); model.addAttribute("books",bookList); return "books"; } }
该处理方法总是返回“books”逻辑视图名。为了让该逻辑视图名能对应到不同的视图,接下来就需要在SpringMVC的得配置文件中配置ContentNegotiatingViewResolver解析器。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用注解驱动 --> <mvc:annotation-driven/> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller"/> <!-- 定义视图解析器 --> <mvc:annotation-driven ignore-default-model-on-redirect="true"/> <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="viewResolvers"> <list> <ref bean="jspResovler"/> <bean class="com.ysy.springmvc.View.PdfViewResolver" p:viewPackage="com.ysy.springmvc.View"/> <bean class="com.ysy.springmvc.View.ExcelViewResolver" p:viewPackage="com.ysy.springmvc.View"/> <bean class="com.ysy.springmvc.View.JsonViewResolver" p:viewPackage="com.ysy.springmvc.View"/> </list> </property> </bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp"/> </beans>