SpringMVC:视图解析器(ViewResolver)

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的功能与用法

InternalResourceViewResolverUrlBasedViewResolver的子类,它与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>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕双嘤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值