SpringMVC之源码分析--ViewResolver(四)

猫耳山在天边 2019-06-27

概述

本章继续学习ViewResolver另一个实现类ContentNegotiatingViewResolver解析器,该类的主要作用是根据同一请求的某些策略,选择对应的View进行渲染。可以把ContentNegotiatingViewResolver理解为适配器,对不同类型View进行适配。值得注意的是处理的handler为同一个。

ContentNegotiatingViewResolver本身不解析视图,它将委托其他视图解析器进行视图解析。

请求的策略包括:请求后缀、请求头的Accept、使用参数等等。

本系列文章是基于Spring5.0.5RELEASE。

流程概述

使用此视图解析器时,调用resolverViewName(viewName,locale)方法,首先调用本类的getMediaType(reuqest)获取请求的媒体类型mediaType(根据策略),然后调用getCandidateViews()方法解析归并到View集合,最后调用getBestView()方法,根据contentType选择出合适的视图并返回。

源码分析

  • ContentNegotiatingViewResolver

该类主要完成

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
    implements ViewResolver, Ordered, InitializingBean {

// 判断请求mediaType,内部包含使用的策略集合
@Nullable
private ContentNegotiationManager contentNegotiationManager;
// 用于创建ContentNegotiationManager实例
private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
// 控制为查找到时的处理
private boolean useNotAcceptableStatusCode = false;
// 存储View实例,可在此集合查询符合条件的View实例进行视图渲染
@Nullable
private List<View> defaultViews;
// 视图解析器集合,用于解析视图
@Nullable
private List<ViewResolver> viewResolvers;
// 排序属性
private int order = Ordered.HIGHEST_PRECEDENCE;

... ...
/**
 *启动时从上下文中加载ViewResolver
 */
@Override
protected void initServletContext(ServletContext servletContext) {
    // 从上下文中获取所有视图解析器
    Collection<ViewResolver> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    if (this.viewResolvers == null) {
        // 将上下文配置的视图解析器添加到属性viewResolvers中,以供后续使用
        this.viewResolvers = new ArrayList<>(matchingBeans.size());
        for (ViewResolver viewResolver : matchingBeans) {
            if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
            }
        }
    }
    else {
        // 初始化viewResolvers属性中配置的视图解析器
        for (int i = 0; i < this.viewResolvers.size(); i++) {
            ViewResolver vr = this.viewResolvers.get(i);
            if (matchingBeans.contains(vr)) {
                continue;
            }
            String name = vr.getClass().getName() + i;
            obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
        }

    }
    if (this.viewResolvers.isEmpty()) {
        logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
                "'viewResolvers' property on the ContentNegotiatingViewResolver");
    }
    // 排序视图解析器
    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    this.cnmFactoryBean.setServletContext(servletContext);
}

/*
 *启动时调用,如果没有配置ContentNegotiationManager,启动时进行创建初始化该属性
 */
@Override
public void afterPropertiesSet() {
    if (this.contentNegotiationManager == null) {
        this.contentNegotiationManager = this.cnmFactoryBean.build();
    }
}

/*
 *请求到来时匹配核实的View并返回
 */
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    // 获取请求的mediaType
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        // 解析出所有视图View和配置的默认View合并,供后面从中匹配选择
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        // 根据contentType匹配选择出合适的View
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        // 返回
        if (bestView != null) {
            return bestView;
        }
    }
    // 未匹配到合适的View,并且把参数useNotAcceptableStatusCode设置为true时,返回406
    if (this.useNotAcceptableStatusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
        }
        return NOT_ACCEPTABLE_VIEW;
    }
    else {
        logger.debug("No acceptable view found; returning null");
        return null;
    }

    ... ...
}

以上是ContentNegotiatingViewResolver类的主要代码,具体调用的方法再此不再展开,有兴趣的童鞋可以自行查看,下面我们来使用这个解析器做个例子,通过例子再加深下理解。

实战

  • 需求目标

实现后缀名或参数控制,显示不同的视图。如:

http://localhost:8088/user jsp视图显示

http://localhost:8088/user.json(http://localhost:8088/user?format=json) json视图显示

http://localhost:8088/user.xml(http://localhost:8088/user?format=xml) xml视图显示

  • 项目结构

新建maven web项目,最终目录结构如下:

SpringMVC之源码分析--ViewResolver(四)

  • pom文件

通过maven引入jar依赖,代码如下:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.dalianghe</groupId>
    <artifactId>spring-mvc-viewresolver-03</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>spring-mvc-viewresolver-03 Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- 依赖spring mvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
        <!-- servlet依赖 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- jsp依赖 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <!-- json依赖 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.5</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.5</version>
        </dependency>
        <!--xml依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.9.5</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>spring-mvc-viewresolver-03</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>2.2</version>
                    <configuration>
                        <path>/</path>
                        <port>8088</port>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
  • spring配置文件

配置视图解析器等,代码如下:

<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"
       default-autowire="byName">

    <!-- 扫描指定路径 -->
    <context:component-scan base-package="com.github.dalianghe.controller"/>

    <!-- 配置ContentNegotiationManagerFactoryBean构造ContentNegotiationManager实例 -->
    <bean id="cnManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <!-- 忽略accept header,即禁用HeaderContentNegotiationStrategy策略 -->
        <property name="ignoreAcceptHeader" value="true"/>
        <!-- 是否启用扩展名支持,即支持PathExtensionContentNegotiationStrategy策略 -->
        <property name="favorPathExtension" value="true"></property>
        <!-- 是否启用参数支持,即支持ParameterContentNegotiationStrategy策略 -->
        <property name="favorParameter" value="true"></property>
        <!--<property name="defaultContentType" value="text/html"/>-->
        <property name="mediaTypes">
            <map>
                <entry key="xml" value="application/xml"/>
                <!--<entry key="json" value="text/plain"/>-->
                <entry key="json" value="application/json"/>
                <!--<entry key="xls" value="application/vnd.ms-excel"/>-->
            </map>
        </property>
    </bean>

    <!-- 配置视图解析器 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="contentNegotiationManager" ref="cnManager"/>

        <property name="viewResolvers">
            <list>
                <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
                    <property name="prefix" value="/WEB-INF/jsp/"/>
                    <property name="suffix" value=".jsp"/>
                </bean>
            </list>
        </property>
        <!-- 默认支持的View -->
        <property name="defaultViews">
            <list>
                <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
                    <!--<property name="prettyPrint" value="true"/>-->
                    <property name="contentType" value="application/json"/>
                </bean>
                <bean class="org.springframework.web.servlet.view.xml.MappingJackson2XmlView">
                    <property name="contentType" value="application/xml"/>
                </bean>
            </list>
        </property>
        <!--<property name="useNotAcceptableStatusCode" value="true"/>-->
    </bean>

</beans>
  • 部署描述文件

配置DispatcherServlet,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
  <display-name>Archetype Created Web Application</display-name>

  <servlet>
    <!-- Servlet名称,可任意定义,但必须与servlet-mapping中对应 -->
    <servlet-name>dispatcher</servlet-name>
    <!-- 指定Spring MVC核心控制类,即J2EE规范中的前端控制器(Front Controller) -->
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 指定Spring MVC配置文件,默认在WEB-INF目录下,切名字为[servlet-name]-servlet.xml,此文件中配置web相关内容,比如:指定扫描Controller路径、配置逻辑视图前缀后缀、上传文件等等 -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <!-- 此配置的值为正整数时,表示容器启动时初始化,即调用Servlet的init方法 -->
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>
  <!-- 定义servlet映射 -->
  <servlet-mapping>
    <!-- 与servlet中servlet-name对应 -->
    <servlet-name>dispatcher</servlet-name>
    <!-- 映射所有的url -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>
  • User实体

简单的实体类,代码如下:

public class User{

    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
  • handler处理器

编写Controller,代码如下:

import com.github.dalianghe.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DemoController {
    @GetMapping("/user")
    public String demo(ModelMap model){

        User user = new User("hedaliang", "123456");
        model.addAttribute(user);
        return "user";
    }
}
  • jsp页面

jsp视图(user.jsp),代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>My Frist JSP</title>
</head>
<body>
<h1>username:${user.username}</h1><br>
<h1>password:${user.password}</h1>
</body>
</html>

以上代码编写结束,下面来进行测试。

测试

启动应用,访问地址:http://localhost:8080/user,此时应使用jsp进行渲染,结果如下:

SpringMVC之源码分析--ViewResolver(四)

访问http://locahost:8088/user.json或http://localhost:8088/user?format=json,结果如下:

SpringMVC之源码分析--ViewResolver(四)

访问http://localhost:8088/user.xml或http://localhost:8088/user?format=xml,结果如下:

SpringMVC之源码分析--ViewResolver(四)

OK!跟预期一致,测试通过,至此我们就实现了需求功能。

总结

本章介绍了ContentNegotiatingViewResolver类,并通过开发小demo验证了此类的功能,里面细节很多,有兴趣的朋友可以再深入了解,希望本文能给大家一写启发。

最后创建了qq群方便大家交流,可扫描加入,同时也可加我qq:276420284,共同学习、共同进步,谢谢!

SpringMVC之源码分析--ViewResolver(四)

相关推荐