springMVC

转载于:http://blog.csdn.net/johnstrive/article/details/50323077

[转] 一篇文章学会springMVC

目录(?)[+]

  1. 说在前面
  2. 简单描述
  3. 工作流程
    1. 以springMVC源码为导向
    2. 以springMVC工作流为导向
  4. springMVC在项目中的位置以maven为例
  5. HELLO WORLD
    1. 第一步创建web工程
    2. 第二步导入依赖库
    3. 第三步配置webxml加载spring到容器中
    4. 第四步配置springmvcxml配置文件
    5. 第五步编写第一个controllerUserController
    6. 第六步编写请求的jsp
  6. 进阶
    1. 练习1RequestMapping映射请求参数请求方法或请求头
    2. 练习2PathVariable映射URL绑定占位符
    3. 练习3REST请求
    4. 练习4请求处理方法签名RequestParam
    5. 练习5使用RequestHeader绑定请求报头的属性值
    6. 练习6使用CookieValue绑定请求中的Cookie值
    7. 练习7使用POJO对象自动绑定参数
    8. 练习8使用servletAPI作为入参
    9. 练习9处理模型数据ModelAndView
    10. 练习10处理模型数据Map
    11. 练习11处理模型数据SessionAtrribute
    12. 练习12ModelAttribute修饰
      1. 源码分析
      2. 整个的运行流程
      3. 这里需要注意的是
    13. 练习13 视图和视图解析器
      1. 视图
      2. 视图解析器
      3. InternalResourceViewResolver
      4. 源码分析
      5. 常用的视图实现类
      6. 常用的视图解析器实现类
    14. 练习14 springMVCJSTL 实现国际化
    15. 练习15 mvcview-controller
    16. 练习16 自定义视图之BeanNameViewResolver
    17. 练习17 重定向
      1. 源码分析
    18. 练习18 springMVC表单标签
      1. STEP1 准备数据
      2. STEP2 增加controller对应的方法
      3. STEP3 增加展现页面springFormjsp
      4. STEP4 请求地址
    19. 练习19 处理静态资源
    20. 练习20 自定义类型转换器
      1. 数据绑定流程
      2. 数据转换
      3. 自定义类型转换器
      4. spring支持的类型转换器
      5. 写一个自定义类型转换器UserConverterService
      6. 源码分析
      7. 测试
    21. 练习21 mvcannotation-driven
      1. 对比配置annotation-driven前后的差异
    22. 练习22 initBinder
    23. 练习23 数据格式化
      1. 日期格式化
      2. 数值格式化
      3. 源码分析
        1. 第一种配置了annotation-driven且自定义类型转换器是通过ConversionServiceFactoryBean加入的
        2. 第二种只配置annotation-driven
        3. 第三种配置了annotation-driven且自定义类型转换器是通过FormattingConversionServiceFactoryBean加入的
      4. 结论
    24. 练习24 数据校验
      1. 原理
      2. 在页面上显示错误
      3. 国际化
      4. 测试
    25. 练习25 返回Json分析HttpMessageConverter
      1. 返回Json
      2. HttpMessageConverter
      3. 小练习
    26. 练习26 国际化
      1. 概述
      2. 小练习
      3. 源码分析
    27. 练习27 文件上传
    28. 练习28 自定义拦截器
    29. 练习29 异常处理
      1. ExceptionHandlerExceptionResolver
      2. ResponseStatusExceptionResolver
      3. 小练习
      4. 源码分析
      5. DefaultHandlerExceptionResolver
      6. 小练习
      7. 源码分析
      8. SimpleMappingExceptionResolver

说在前面

本文只是入门
为什么用springMVC?springMVC有什么有缺点?springMVC和Struts有什么区别?等等这些问题可以参考网路上资源,本文的重点是快速带入,让大家了解熟悉springMVC。springMVC毕竟是工具,工具的特点就是熟能生巧,通过快速掌握,多加练习、解决问题及归纳总结肯定可以掌握并且成为自己的东西。

简单描述

springMVC主要是通过前端控制器controller中的注解来完成请求处理的。前端无论是以何种方式请求,都会通过controller进行轻度处理、转发以及调度后端的处理器进行处理,最后返回正确的视图及响应。以此来看,springMVC说白了既可以返回合适的页面,也可以响应RESTful请求。

工作流程

以springMVC源码为导向

这里写图片描述

以springMVC工作流为导向

这里写图片描述

springMVC在项目中的位置(以maven为例)

src
|-main
|---|---java
|---|-----|----package.controller (springMVC中controller类)
|---|-----|----package.interceptor (springMVC中的拦截器)
|---|-----|----package.service (dao,po,jo,vo,commons,utils)
|---|---resource
|---|-----|----applicationContext-*.xml
|---|-----|----springmvc.xml (springMVC配置文件)
|---|-----|----messages.properties (springMVC消息属性文件)
|---|---webapp
|---|----|----pages(springMVC视图页面)
|---|----|----WEB-INF
|---|----|-----|---web.xml(springMVC的DispatcherServlet及相关配置)
|--external libraries (springMVC依赖库)

HELLO WORLD

第一步,创建web工程

国际惯例,先搭建一个spring项目,通过实战的方式,渐进的学习,杜绝眼高手低的学习方式。
如果是maven需要archetype为maven-archetype-webapp。

第二步,导入依赖库

附上pom.xml,非maven项目可用导入相应的jar包

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>xhsTest</groupId>
  <artifactId>spring-mvc-test</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>spring-mvc-test Maven Webapp</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <webroot.path>src/main/webapp</webroot.path>
    <spring-version>4.1.0.RELEASE</spring-version>
    <log4j-version>2.2</log4j-version>
  </properties>

  <build>
    <finalName>springmvctest</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.3</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>${project.build.sourceEncoding}</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <webXml>src/main/webapp/WEB-INF/web.xml</webXml>
        </configuration>
      </plugin>

      <!-- 添加jetty -->
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>8.1.8.v20121106</version>
        <configuration>
          <reload>manual</reload>
          <webAppConfig>
            <contextPath>/</contextPath>
          </webAppConfig>
          <connectors>
            <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
              <port>8080</port>
            </connector>
          </connectors>
        </configuration>
      </plugin>
    </plugins>

    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>false</filtering>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>true</filtering>
      </resource>
    </resources>
  </build>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>

    <!-- web servlet -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>

    <!-- spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-expression</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring-version}</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.7.4</version>
    </dependency>

    <!-- miscellaneous spring验证依赖库-->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>5.1.3.Final</version>
    </dependency>

  </dependencies>
</project>

第三步,配置web.xml加载spring到容器中

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>springMVC-test</display-name>

  <!--springMVC:创建DispatcherServlet-->
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--DispatcherServlet初始化参数,配置springmvc配置文件的位置和名字-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <!--在创建web应用且web应用未加载的适合创建,而不是第一次请求的时候创建-->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <!--拦截所有请求-->
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

</web-app>

第四步,配置springmvc.xml配置文件

<?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"
       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-4.1.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">

    <!--配置自动扫描的包,将bean加入spring容器-->
    <context:component-scan base-package="com.iboray.smt"></context:component-scan>

    <!--配置视图解析器-->
    <bean id="resolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--返回前缀-->
        <property name="prefix" value="/"></property>
        <!--后缀-->
        <property name="suffix" value=".jsp"></property>
    </bean>
</beans>

第五步,编写第一个controller(UserController)

package com.iboray.smt.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Controller 标识该类为Controller
 * RequestMapping value为请求根路径
 */
@Controller
@RequestMapping(value = "/user")
public class UserController {

    /**
     * RequestMapping中value定义该方法的请求地址为userInfo,method定义请求方式为GET方式
     * 最终访问地址为: 根路径 / 请求地址.也就是/user/userInfo
     *
     * @return 返回地址为视图解析器的前缀+返回值+后缀.也就是/userInfo.jsp
     */
    @RequestMapping(value = "userInfo",method = RequestMethod.GET)
    public String getUserInfo(){

        return "userInfo";
    }
}

第六步,编写请求的jsp

附上两个jsp,一个是测试发送请求,一个是controller响应的视图jsp
index.jp

<html>
<body>
<h2>Hello World!</h2>
<hr>
<a href="/user/userInfo" >get UserInfo</a>
</body>
</html>

userInfo.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>user info</title>
</head>
<body>
<h2>This is user info page</h2>
</body>
</html>

进阶

通过以上6步,最简单的springMVC就搭建好了。接下来,我们就可以通过练习springmvc中各技术揭开她的神秘面纱。
之后所有练习都通过index.jsp和UserController.java完成。

练习1:RequestMapping映射请求参数、请求方法或请求头

RequestMapping为控制器指定可以处理哪些请求的URL,在控制器的类定义和方法定义处都可以修饰。
- 类定义处:提供初步的请求映射信息,相当于web应用的根目录
- 方法定义出:提供进一步的细分映射信息,相对于类定义处的URL,如果类定义处未标注@RequestMapping,那么方法定义处就相对于web应用的根目录。
DispatchServlet截获请求后,就通过控制器上@RequestMapping提供的映射信息确定对应的处理方法
RequestMapping除了可以使用请求URL外,还可以通过请求参数,请求方法,请求头映射请求
requestMapping的value,method,params和heads分别表示,请求URL,请求方法,请求参数和请求头的映射条件,他们之间是与的关系,联合使用多个条件,可以使请求映射更加精准。
params和heads支持简单的表达式
1. param1:表示请求必须包含名为param1的请求参数
2. !param1:表示请求不能包含名为param1的请求参数
3. param1 != value1:表示请求必须包含名为param1的请求参数,且值不等于value1
4. {“param1 == value1”,”param2”}:请求必须包含名为param1和param2的参数,且param1的值必须等于value1
练习
index.jsp

<a href="/user/userInfo?name=xxx&age=20" >RequestMapping params</a>

UserController.java

@RequestMapping(value = "userInfo",method = RequestMethod.GET,params = {"name=xxx","age"})
    public String getUserInfo(){

        return "userInfo";
    }

练习2:PathVariable映射URL绑定占位符

带URL占位符是Spring3.0新增功能,该功能在springmvc向REST目标挺进发展过程中具有里程碑的意义。
通过@PathVariable可以将URL中占位符参数绑定到控制器处理的方法的入参中,也就是说URL中的{paramName}占位符可以通过@PathVariable(paramName)绑定到操作方法的入参中。
index.jsp

<a href="/user/delUser/3" >PathVariable_delUser</a>

UserController.java

@RequestMapping(value = "/delUser/{delId}",method = RequestMethod.GET)
    public String delUser(@PathVariable(value = "delId") Integer id){
        System.out.println("delId : "+id);
        return SUCCESS;
    }

练习3:REST请求

关于更多REST请参考如下两篇文章
理解本真REST架构
深入浅出REST
SpringMVC通过配置HiddenHttpMethodFilter来支持PUT/DELETE请求
1 web.xml增加如下配置

  <!--可让springMVC 将POST请求转为PUT/DELETE等..-->
  <filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

2 发送POST请求,并携带name=_method的隐藏域转为DELETE或PUT等请求
index.jsp

<form id="putUserForm" method="post" action="/user/putUser/5">
            <input type="hidden" name="_method" value="PUT">
            <input type="submit" value="submit">
</form>

UserController.java

@RequestMapping(value = "/putUser/{putId}",method = RequestMethod.PUT)
    public String putUser(@PathVariable(value = "putId") Integer id){
        System.out.println("putId : "+id);
        return SUCCESS;
    }

练习4:请求处理方法签名@RequestParam

SpringMVC通过分析处理方法的签名,将HTTP请求的信息绑定到处理方法的入参中,并根据方法的返回类型做出相应的后续处理。SpirngMVC对控制器处理方法的限制很宽松,必要时可以对方法及方法入参标注注解(@PathVariable@RequestParam@RequestHeader)
使用RequestParam来映射参数
1. value 请求参数名
2. required 参数是否必须,默认为True
3. defaultValue 参数默认值
index.jsp

<a href="/user/queryUser?userId=9&name=Tim" >RequestParam_queryUser</a>

UserController.java

@RequestMapping(value = "/queryUser",method = RequestMethod.GET)
    public String queryUser(@RequestParam(value = "userId") Integer id,@RequestParam(value = "name",required = false,defaultValue = "") String name){
        System.out.println("queryUserId : "+id + " name : "+ name);
        return SUCCESS;
    }

练习5:使用@RequestHeader绑定请求报头的属性值

请求报头包含若干属性值,服务器可据此获取客户端信息,通过@RequestHeader即可将请求报头的属性值绑定到处理方法的入参中。
index.jsp

<a href="/user/userPwd" >RequestHeader_userPwd</a>

UserController.java

@RequestMapping(value = "/userPwd",method = RequestMethod.GET)
    public String userPwd(@RequestHeader("Accept-Encoding") String encoding){
        System.out.println("encoding : "+encoding);
        return SUCCESS;
    }

练习6:使用@CookieValue绑定请求中的Cookie值

index.jsp

<a href="/user/testCookie" >CookieValue_testCookie</a>

UserController.java

@RequestMapping(value = "/testCookie")
    public String testCookie(@CookieValue(value = "JSESSIONID") String jsession){
        System.out.println("jsession : "+jsession);
        return SUCCESS;
    }

练习7:使用POJO对象自动绑定参数

SpringMVC会按照请求参数名和对象属性名进行自动绑定,自动为该对象填充属性值,支持级联属性。如dept.deptName,dept.address.tel等
请求地址

http://127.0.0.1:8080/user/addUser?id=1&name=Tim&dept.deptName=dev

User.java

public class User {

    private int id;

    private String name;

    private Detp dept;

    getter setter toString...

}

Dept.java

public class Detp {

    private int  id;

    private String deptName;

    getter setter toString...
}

后台输出:

user : User{id=1, name='Tim', dept=Detp{id=0, deptName='dev'}}

练习8:使用servletAPI作为入参

UserController.java

@RequestMapping(value = "/testServletAPI")
    public String testServletAPI(HttpServletRequest request){
        System.out.println("request : "+request.getRequestURI());
        return SUCCESS;
    }

练习9:处理模型数据ModelAndView

SpringMVC提供以下几种途径输出模型数据
ModelAndView:处理方法返回值类型为ModelAndView时,方法体即可通过该对象添加模型数据。
Map 或 Model:入参为Model、ModelMap或Map时,处理方法返回时,Map中的数据会自动添加到模型中。
@SessionAttributes:将模型中的某个数据暂时存到HttpSession中,以便多个请求之间可以共享这个属性。
@ModelAttribute:方法入参标书该注解后,入参的对象就会放到数据模型中。
ModelAndView
控制器返回值如果为ModelAndView时,则其既包含视图信息,也包含数据模型信息。
添加模型数据:
ModelAndView addObject(String attributeName,Object attributeValue);
ModelAndView addAllObject(Map《String,?》, modelMap)
设置视图
void setView(View view);
void setViewName(String viewName);

Controller.java

@RequestMapping(value = "/testModelAndView")
    public ModelAndView testModelAndView(){
        ModelAndView mv = new ModelAndView(SUCCESS);
        mv.addObject("msg","testModelAndView");
        return mv;
    }

那究竟是怎样一个运行流程呢?
跟踪源码主线为:
由关键对象DispacherServlet最外层doDispatch进入
第一层
org.springframework.web.servlet.DispatcherServlet#doDispatch

this.processDispatchResult(processedRequest, response, mappedHandler, err, dispatchException);

第二层
org.springframework.web.servlet.DispatcherServlet#processDispatchResult

this.render(mv, request, response);

第三层
org.springframework.web.servlet.DispatcherServlet#render

view.render(mv.getModelInternal(), request, response);

第四层
org.springframework.web.servlet.view.AbstractView#render

this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);

第五层
这一步比较关键,需要找到我们配置的视图解析器类型InternalResourceView(默认也是这个)
org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel

this.exposeModelAsRequestAttributes(model, request);

第六层
org.springframework.web.servlet.view.AbstractView#exposeModelAsRequestAttributes

while(var3.hasNext()) {
            Entry entry = (Entry)var3.next();
            String modelName = (String)entry.getKey();
            Object modelValue = entry.getValue();
            if(modelValue != null) {
                request.setAttribute(modelName, modelValue);//关键看这里!!!看下面说明1
                if(this.logger.isDebugEnabled()) {
                    this.logger.debug("Added model object \'" + modelName + "\' of type [" + modelValue.getClass().getName() + "] to request in view with name \'" + this.getBeanName() + "\'");
                }
            }
             ...

说明1:通过modelAndView的addObject设置的K/V值,最终是通过setAttribute一个个set到request请求域中。这下就明白了吧!再看modelAndView对象,包括两个重要属性是private Object view;private ModelMap model;,那其中的addObject方法

public ModelAndView addObject(String attributeName, Object attributeValue) {
        this.getModelMap().addAttribute(attributeName, attributeValue);
        return this;
    }

就是获取modelMap,执行addAttribute方法,再进入这个方法

 public ModelMap addAttribute(String attributeName, Object attributeValue) {
        Assert.notNull(attributeName, "Model attribute name must not be null");
        this.put(attributeName, attributeValue);//关键看这里
        return this;
    }

因为modelMap继承了public class ModelMap extends LinkedHashMap,所以put方法就是我们常用的Map对象的put方法。说白了,ModelAndView中的model就是一个Map对象,搞明白这个很重要。

练习10:处理模型数据Map

SpringMVC内部使用了一个org.springframework.ui.model接口存储模型数据
SpringMVC在调用方法前会创建一个隐含的模型对象存储模型数据。
如果方法的入参为Map or Model类型,SpringMVC会将隐含的模型的引用传递给这些入参,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据。
Controller.java

@RequestMapping(value = "/testMap")
    public String testMap(Map<String,Object> map){
        map.put("msg", Arrays.asList("a","b","c"));
        return SUCCESS;
    }

数据结果页面接参msg : ${msg} 结果为:msg : [a, b, c]

练习11:处理模型数据@SessionAtrribute

若想再多个请求之间共享某个模型属性数据,则可以在控制器上标注@SessionAttribute注解,SpringMVC将模型中对应的属性暂存到HttpSession中。
@SessionAttribute除了可以通过属性名指定放到会话中的属性外(value属性),还可以根据对象类型指定哪些类型可以放到会话中(types属性)。
Controller.java

@SessionAttributes(value = {"user"},types = {String.class} )
@Controller
@RequestMapping(value = "/user")
public class UserController {
@RequestMapping(value = "/testSessionAttribute")
    public String testSessionAttribute(Map<String,Object> map){
        map.put("user", new User(1,"Tim"));
        map.put("sessStr","John");
        return SUCCESS;
    }
}

练习12:@ModelAttribute修饰

我个人使用ModelAttribute的地方很少。一般的使用是修饰方法和修饰入参。修饰方法会在调用目标方法前首先调用ModelAttribute修饰的方法,修饰入参意思是获取ModelAttribute中指定的对象名称。
修饰方法controller.java

@ModelAttribute
    public void userModelAttributeModel(@RequestParam(value = "id",required = false) Integer id
                                        ,Map<String,Object> map){
        System.out.println("userModelAttributeModel coming...");
        if(id != null){
            User u = new User(1,"Jackson","98765");
            System.out.println("userModelAttributeModel : " + u);
            map.put("user",u);
        }
    }

修饰入参

public ModelAndView sendMail(HttpServletRequest request, @ModelAttribute("user") User user)

究竟什么地方调用的ModelAttribute修饰的方法,运行流程是什么?

源码分析

第一步,探究@ModelAttribute修饰的方法入参Map
在修饰的方法map.put…也就是设置值的时候打断点,进入方法后,先查看Map入参的类型为BindingAwareModelMap,那BindingAwareModelMap 继承 ExtendedModelMap,而 ExtendedModelMap 继承 ModelMap 实现了 Model接口。而ModelMap 又继承 LinkedHashMap。这个入参Map便是我们常用的ModelAndView中的Model。
第一层(顶层执行[ 类 # 方法 ])我觉得有必要把整个方法附上
org.springframework.web.bind.annotation.support.HandlerMethodInvoker#invokeHandlerMethod

public final Object invokeHandlerMethod(Method handlerMethod, Object handler,
            NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {

        Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod);
        try {
            boolean debug = logger.isDebugEnabled();
            //获取所有@SessionAttribute,并从HttpSession中获取key对应的对象,放入implicitModel中
            for (String attrName : this.methodResolver.getActualSessionAttributeNames()) {
                Object attrValue = this.sessionAttributeStore.retrieveAttribute(webRequest, attrName);
                if (attrValue != null) {
                    implicitModel.addAttribute(attrName, attrValue);
                }
            }
            //获取所有@ModelAttribute,并从将@ModelAttribute修饰的方法中初始化的对象,放入implicitModel中
            for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) {
                Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod);
                Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel);
                if (debug) {
                    logger.debug("Invoking model attribute method: " + attributeMethodToInvoke);
                }
                String attrName = AnnotationUtils.findAnnotation(attributeMethod, ModelAttribute.class).value();
                if (!"".equals(attrName) && implicitModel.containsAttribute(attrName)) {
                    continue;
                }
                ReflectionUtils.makeAccessible(attributeMethodToInvoke);
                //这里就是反射执行@ModelAttribute修饰的方法,执行完后implicitModel就增加了方法中添加的KV值。
                Object attrValue = attributeMethodToInvoke.invoke(handler, args);
                if ("".equals(attrName)) {
                    Class<?> resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass());
                    attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue);
                }
                if (!implicitModel.containsAttribute(attrName)) {
                    implicitModel.addAttribute(attrName, attrValue);
                }
            }
            //主要作用是将@SessionAttribute和@ModelAttribute赋值的implicitModel和请求参数并集操作,得到目标方法的最终入参(implicitModel)
            Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel);
            if (debug) {
                logger.debug("Invoking request handler method: " + handlerMethodToInvoke);
            }
            ReflectionUtils.makeAccessible(handlerMethodToInvoke);
            //执行目标方法,传入参数(implicitModel)
            return handlerMethodToInvoke.invoke(handler, args);
        }
        catch (IllegalStateException ex) {
            // Internal assertion failed (e.g. invalid signature):
            // throw exception with full handler method context...
            throw new HandlerMethodInvocationException(handlerMethodToInvoke, ex);
        }
        catch (InvocationTargetException ex) {
            // User-defined @ModelAttribute/@InitBinder/@RequestMapping method threw an exception...
            ReflectionUtils.rethrowException(ex.getTargetException());
            return null;
        }
    }

由此可见不管请求这个controller的什么方法,都会先获取@SessionAttribute中的KV值,并进入@ModelAttribute修饰的方法,执行invoke后进入ModelAttribute方法,执行后给implicitModel赋值,

最终目标方法的入参的具体赋值的关键代码为:
org.springframework.web.bind.annotation.support.HandlerMethodInvoker#resolveHandlerArguments

private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
            NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {

        Class<?>[] paramTypes = handlerMethod.getParameterTypes();
        Object[] args = new Object[paramTypes.length];

        for (int i = 0; i < args.length; i++) {
            MethodParameter methodParam = new MethodParameter(handlerMethod, i);
            methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
            GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
            String paramName = null;
            String headerName = null;
            boolean requestBodyFound = false;
            String cookieName = null;
            String pathVarName = null;
            String attrName = null;
            boolean required = false;
            String defaultValue = null;
            boolean validate = false;
            Object[] validationHints = null;
            int annotationsFound = 0;
            Annotation[] paramAnns = methodParam.getParameterAnnotations();

            for (Annotation paramAnn : paramAnns) {
                if (RequestParam.class.isInstance(paramAnn)) {
                    RequestParam requestParam = (RequestParam) paramAnn;
                    paramName = requestParam.value();
                    required = requestParam.required();
                    defaultValue = parseDefaultValueAttribute(requestParam.defaultValue());
                    annotationsFound++;
                }
                else if (RequestHeader.class.isInstance(paramAnn)) {
                    RequestHeader requestHeader = (RequestHeader) paramAnn;
                    headerName = requestHeader.value();
                    required = requestHeader.required();
                    defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue());
                    annotationsFound++;
                }
                else if (RequestBody.class.isInstance(paramAnn)) {
                    requestBodyFound = true;
                    annotationsFound++;
                }
                else if (CookieValue.class.isInstance(paramAnn)) {
                    CookieValue cookieValue = (CookieValue) paramAnn;
                    cookieName = cookieValue.value();
                    required = cookieValue.required();
                    defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue());
                    annotationsFound++;
                }
                else if (PathVariable.class.isInstance(paramAnn)) {
                    PathVariable pathVar = (PathVariable) paramAnn;
                    pathVarName = pathVar.value();
                    annotationsFound++;
                }
                else if (ModelAttribute.class.isInstance(paramAnn)) {
                    ModelAttribute attr = (ModelAttribute) paramAnn;
                    attrName = attr.value();
                    annotationsFound++;
                }
                else if (Value.class.isInstance(paramAnn)) {
                    defaultValue = ((Value) paramAnn).value();
                }
                else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
                    validate = true;
                    Object value = AnnotationUtils.getValue(paramAnn);
                    validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
                }
            }

            if (annotationsFound > 1) {
                throw new IllegalStateException("Handler parameter annotations are exclusive choices - " +
                        "do not specify more than one such annotation on the same parameter: " + handlerMethod);
            }

            if (annotationsFound == 0) {
                Object argValue = resolveCommonArgument(methodParam, webRequest);
                if (argValue != WebArgumentResolver.UNRESOLVED) {
                    args[i] = argValue;
                }
                else if (defaultValue != null) {
                    args[i] = resolveDefaultValue(defaultValue);
                }
                else {
                    Class<?> paramType = methodParam.getParameterType();
                    if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) {
                        if (!paramType.isAssignableFrom(implicitModel.getClass())) {
                            throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " +
                                    "Model or Map but is not assignable from the actual model. You may need to switch " +
                                    "newer MVC infrastructure classes to use this argument.");
                        }
                        args[i] = implicitModel;
                    }
                    else if (SessionStatus.class.isAssignableFrom(paramType)) {
                        args[i] = this.sessionStatus;
                    }
                    else if (HttpEntity.class.isAssignableFrom(paramType)) {
                        args[i] = resolveHttpEntityRequest(methodParam, webRequest);
                    }
                    else if (Errors.class.isAssignableFrom(paramType)) {
                        throw new IllegalStateException("Errors/BindingResult argument declared " +
                                "without preceding model attribute. Check your handler method signature!");
                    }
                    else if (BeanUtils.isSimpleProperty(paramType)) {
                        paramName = "";
                    }
                    else {
                        attrName = "";
                    }
                }
            }

            if (paramName != null) {
                args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler);
            }
            else if (headerName != null) {
                args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);
            }
            else if (requestBodyFound) {
                args[i] = resolveRequestBody(methodParam, webRequest, handler);
            }
            else if (cookieName != null) {
                args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);
            }
            else if (pathVarName != null) {
                args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);
            }
            else if (attrName != null) {
                WebDataBinder binder =
                        resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);
                boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));
                if (binder.getTarget() != null) {
                    doBind(binder, webRequest, validate, validationHints, !assignBindingResult);
                }
                args[i] = binder.getTarget();
                if (assignBindingResult) {
                    args[i + 1] = binder.getBindingResult();
                    i++;
                }
                /**
*这里将request域中的请求参数覆盖到@SessionAttribute和@ModelAttribute赋值的implicitModel中。
*这样得到的结果就是@SessionAttribute和ModelAttribute先将所有字段初始化,然后和request中的请求值构成并集,
*这样得出的结果就是request中没有的字段保留,如果key一样的,就覆盖新值。
*也就得到我们想要的结果。最终在目标方法中得到的就是已被改写的参数。
**/ 
                implicitModel.putAll(binder.getBindingResult().getModel());
            }
        }

        return args;
    }

整个的运行流程

  1. 发起request请求—->
  2. 各种filter—>
  3. org.springframework.web.servlet.DispatcherServlet#doDispatch—–>
  4. org.springframework.web.servlet.HandlerAdapter#handle(具体执行适配器为:org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#handle)—–>
  5. org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#invokeHandlerMethod——>
  6. invokeHandlerMethodd的for (String attrName : this.methodResolver.getActualSessionAttributeNames())(先看看SessionAttribute修饰的Handler有没有值,有就放入org.springframework.ui.ExtendedModelMap implicitModel 中)——>
  7. invokeHandlerMethodd的for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) (再看@ModelAttribute修饰的所有方法,如果有就循环每个ModelAttribute修饰的方法,并将每个方法的返回值放入org.springframework.ui.ExtendedModelMap implicitModel )——>
  8. org.springframework.web.bind.annotation.support.HandlerMethodInvoker#resolveHandlerArguments(将@SessionAttribute和@ModelAttribute初始化的对象和自己的请求参数并集(相关代码:Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); )作为自己请求的目标方法的入参,注意,如果没有使用ModelAttribute注解,则目标方法的入参key为POJO类名的首字母小写。)—–>(反射执行自己请求的目标方法。handlerMethodToInvoke.invoke(handler, args);)—->
  9. 最后方法得到已被处理过的入参。执行业务操作。
    SpringMVC确定POJO入参的过程

这里需要注意的是:

如果implicitModel不存在key对应的对象(也就是说ModelAttribute中也没有为key赋值),则会检查当前Handler是否标注了SessionAttribute注解且value属性值包含了这个key,则会从HttpSession中寻找这个key对应的value值,如果有则绑定到目标方法的入参中,如果返回null,将抛出异常:org.springframework.web.HttpSessionRequiredException: Session attribute ‘user’ required - not found in session
相关源代码

private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam,
            ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception {

        // Bind request parameter onto object...
        String name = attrName;
        if ("".equals(name)) {
            name = Conventions.getVariableNameForParameter(methodParam);
        }
        Class<?> paramType = methodParam.getParameterType();
        Object bindObject;
        if (implicitModel.containsKey(name)) {
            bindObject = implicitModel.get(name);
        }
        else if (this.methodResolver.isSessionAttribute(name, paramType)) {
        //如果有SessionAttribute注解,则查找HttpSession中的值
            bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name);
            //如果获取的值为NULL,就抛异常HttpSessionRequiredException异常
            if (bindObject == null) {
                raiseSessionRequiredException("Session attribute '" + name + "' required - not found in session");
            }
        }
        else {
            bindObject = BeanUtils.instantiateClass(paramType);
        }
        WebDataBinder binder = createBinder(webRequest, bindObject, name);
        initBinder(handler, name, binder, webRequest);
        return binder;
    }

练习13 视图和视图解析器

请求处理方法完成后,最终会返回一个ModelAndView对象,对于返回String,View或ModelMap等类型的处理方法,SpringMVC也会在内部将它装配成一个ModelAndView对象,它包含了逻辑名和模型对象的视图。
SpringMVC借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP,也可能是Excel、jfreechart等各种形式的视图。
对于最终采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器的工作重点聚焦在生产模型数据的工作上,从而实现了MVC的充分解耦。

视图

视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。为了实现视图模型和具体实现技术的解耦,spring在org.springframework.web.servlet包中定义了一个高度抽象的接口View
这里写图片描述
视图对象由视图解析器负责实例化,由于视图是无状态的,所以他们不会有线程安全问题。

视图解析器

SpringMVC为逻辑视图名的解析提供了不同的策略,可以在spring WEB 上下文中配置一种或多种解析策略,并指定他们的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。
所有的视图解析器都必须实现org.springframework.web.servlet.ViewResolver接口
这里写图片描述

InternalResourceViewResolver

JSP是最常见是视图技术,可以使用InternalResourceViewResolver作为视图解析器:

<!--配置视图解析器-->
    <bean id="resolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--返回前缀-->
        <property name="prefix" value="/"></property>
        <!--后缀-->
        <property name="suffix" value=".jsp"></property>
    </bean>

若项目中用了JSTL,则SpringMVC会自动把InternalResourceViewResolver转换为JstlView
若使用了JSTL的fmt标签,则需要在SpringMVC的配置文件中配置国际化资源文件

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"></property>
</bean>

若希望直接响应SpringMVC渲染的页面,可以使用
<mvc:view-controller path="user/testMap" view-name="success" />

源码分析

通过InternalResourceViewResolver了解视图解析流程,
InternalResourceViewResolver是我们常用来渲染JSP的解析器,它继承了UrlBasedViewResolver 实现了ViewResolver的resolveViewName方法,获取最终实现视图渲染的解析器,从而最终得到我们想要的视图。
上次我们了解处理器运行流程的时候跟踪的源码是handle方法
这次我们是需要跟踪处理完成后返回视图的运行流程
这里意思是不管执行结果是什么,都返回ModelAndView
顶层入口
org.springframework.web.servlet.DispatcherServlet#doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
这里返回的ModelAndView如下:
这里写图片描述
//这里就是处理结果后续操作,其中包括视图渲染
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
//进入方法后
org.springframework.web.servlet.DispatcherServlet#processDispatchResult

//如果有错误,就进行错误视图渲染
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }

//如果没有错误就进入这一步

        if (mv != null && !mv.wasCleared()) {
        //渲染视图
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }

进入render
org.springframework.web.servlet.DispatcherServlet#render

View view;
        if (mv.isReference()) {
            // We need to resolve the view name.
            //这里说的很清楚了,需要得到视图名字,
            //通过后面的源码我们可以知道,这里返回的是InternalResourceViewResolver的子类JstlView
            view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
            if (view == null) {
                throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                        "' in servlet with name '" + getServletName() + "'");
            }
        }

进入resolveViewName
org.springframework.web.servlet.DispatcherServlet#resolveViewName

//this.viewResolvers这里是所有的视图解析器,这里包括了InternalResourceViewResolver
for (ViewResolver viewResolver : this.viewResolvers) {
//通过controller要返回的结果,找到匹配的视图,这里返回的是JstlView
//因为InternalResourceViewResolver继承了UrlBasedViewResolver,所以可以通过UrlBasedViewResolver的loadView方法获得view(view的实现类AbstractUrlBasedView)的URL及其他属性
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
            //这里返回的是InternalResourceViewResolver的子类JstlView
                return view;
            }
        }

获得view的正确视图后执行渲染方法
org.springframework.web.servlet.DispatcherServlet#render

//真正渲染,其实是view实现类的render方法,这里执行的是JstlView的父类InternalResourceView的父类AbstractUrlBasedView的父类AbstractView的render方法
`view.render(mv.getModelInternal(), request, response);`

进入
org.springframework.web.servlet.view.AbstractView#render,一定要明白,这个方法是通过org.springframework.web.servlet.DispatcherServlet#render调用的,具体实现就是InternalResourceViewResolver的子类JstlView

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (logger.isTraceEnabled()) {
            logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
                " and static attributes " + this.staticAttributes);
        }

        Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
        prepareResponse(request, response);
        //这里执行对应的实现类渲染
        renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
    }

我们用的是InternalResourceView,因为DispatcherServlet中获得的view是是InternalResourceViewResolver的子类JstlView,且执行了render()方法,进而可以执行父类的renderMergedOutputModel方法。

org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel

protected void renderMergedOutputModel(
            Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            ... ...
//关键看这里RequestDispatcher
        RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
         ... ...

        // If already included or response already committed, perform include, else forward.
        if (useInclude(request, response)) {
            response.setContentType(getContentType());
        ... ...
            rd.include(request, response);
        }

        else {
            // Note: The forwarded resource is supposed to determine the content type itself.
            ... ...
            //这里进行请求转发
            rd.forward(request, response);
        }
    }

从以上源码可以大概了解到我们配置的InternalResouceViewResolver的视图渲染过程,也可以举一反三明白其他视图解析器的渲染过程。

常用的视图实现类

大类视图类型说明
URL资源视图InternalResouceView将JSP或其他资源封装成一个视图,是InternalResouceViewResolver默认的视图实现类
URL资源视图JstlView如果JSP中使用了JSTL国际化标签的功能,则需要使用该视图实现类
文档视图AbstractExcelViewExcel文档视图的抽象类,该视图基于POI构造Excel文档
文档视图AbstractPdfViewExcel文档视图的抽象类,该视图基于iText构造PDF文档
报表视图ConfigurableJasperReportsView使用JasperReports报表技术的视图
报表视图JasperReportsCsvView同上
报表视图JasperReportsHtmlView同上
报表视图JasperReportsPdfView同上
报表视图JasperReportsXlsView同上
报表视图JasperReportsMultiFormatView同上
JSON视图MappingJackson2JsonView将模型数据通过Jackson开业框架的ObjectMapper以JSON方式输出

常用的视图解析器实现类

大类视图类型说明
解析为Bean的名字BeanNameViewResolver将逻辑视图名解析为一个Bean,Bean的id等于逻辑视图名
解析为URL文件InternalResourceViewResolver将视图名解析为一个URL文件,一般使用该解析器将视图名映射为一个保存在WEB-INF下的程序文件,如.jsp
解析为URL文件JasperReportsViewResolverJasperReports是一个基于Java的开源报表工具,该解析器将视图名解析为报表文件对应的URL
模板文件视图FreeMarkerViewResolver解析为基于FreeMarker模板技术的模板文件
模板文件视图VelocityViewResolver解析为基于Velocity模板技术的模板文件
模板文件视图VelocityLayoutViewResolver同上

练习14 springMVC+JSTL 实现国际化

步骤1:需要在项目中导入jstl依赖库,并在需要国际化的页面引入jstl的fmt标签
步骤2:配置资源文件(在src/main/resource下)
资源文件基本都是KV结构所以这三个文件内容的key都必须一样,value就换为对应的值就可以了
i18n.properties

i18n.name=name
i18n.pwd=password

i18n_en_US.properties

i18n.name=name
i18n.pwd=password

i18n_zh_CN.properties

i18n.name=用户名
i18n.pwd=密码

步骤3:springMVC配置文件

    <!--配置国际化资源文件,注意basename的value值,需要对应资源文件-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"></property>
        <property name="defaultEncoding" value="UTF-8"></property>
    </bean>

步骤4:需要国际化的页面

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
    <title>user info</title>
</head>
<body>

<p> <fmt:message key="i18n.name"></fmt:message></p>

</body>
</html>

这样,随着本地语言环境的变化,name会自动切换为对应的语言

练习15 mvc:view-controller

有适合我们需要直接访问某个视图,不需要经过controller,那么可以增加springMVC的配置

    <mvc:annotation-driven></mvc:annotation-driven>
    <!--path映射访问地址 view-name为视图名称-->
    <mvc:view-controller path="/abc" view-name="userInfo"></mvc:view-controller>

这样访问http://ip:port/abc就可以跳转到userInfo.jsp了,注意mvc:annotation-driven这个配置,这是为了防止配置view-controller后无法正常访问controller而增加的。至于什么原因,之后补充。

练习16 自定义视图之BeanNameViewResolver

我们有的时候需要通过自定义的视图解决业务的需要,如公司内部的模板等等…
这里我们通过BeanNameViewResolver这个视图解析器来看看自定义视图的实现方式。
首先我们需要一个View接口的实现类MyView1.java

/**
 * 因为BeanNameViewResolver是直接根据视图名称来获取的,
 * 所以需要用@Component注解加入到spring容器中
 */
@Component
public class MyView1 implements View {
    @Override
    public String getContentType() {
        //返回内容类型
        return "text/html";
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        PrintWriter pw = response.getWriter();

        pw.print("this is myView page , Time = "+System.currentTimeMillis());

        pw.close();
    }
}

然后我们需要为springMVC增配

    <!--配置beanNameViewResolver视图解析器,使用视图的名字来解析视图,order为解析顺序,越小优先级越高-->
    <!--所以自定义的视图需要保证比InternalResourceViewResolver优先级要高-->
    <!--而InternalResourceViewResolver的order属性为Integer的最大值,所以这里order可以为任何值-->
    <bean id="beanNameViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="10"></property>
    </bean>

顺便看下源码,看到BeanNameViewResolver是通过context.getBean返回咱们定义的视图,所以这就是为什么要把咱们的View实现类放到容器中的原因。也就是为什么我们在controller中返回视图名称就可以实现自定义视图了。

@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
        ....
        return context.getBean(viewName, View.class);
    }

最后再controller中写测试方法

 @RequestMapping("/testView")
    public String testView(){

        System.out.println("testView");
	//这个名称就是我们的自定义视图类
        return "myView1";
    }

测试结果:
页面会打印出

this is myView page , Time = 1452041832760

其他的视图解析器可以查看练习13下的《常用的视图解析器实现类》

练习17 重定向

一般情况下,控制器方法返回字符串类型的值会被当做逻辑视图名处理。
如果返回的字符串中含有forward:或redirect:前缀时,SpringMVC会对他们进行特殊处理,将forward:和redirect:当成指示符处理,其后的字符串当成URL来处理。
redirect:success.jsp 会完成一个到success.jsp的重定向
forward:success.jsp 会完成一个到success.jsp的转发

源码分析

为了看怎么处理返回结果(即:返回哪个视图VIEW。也就是View接口的哪个实现类),定位到doDispatch这个方法,看过上面源码分析的朋友,相信已经很熟悉了
顶层方法doDispatch
org.springframework.web.servlet.DispatcherServlet#doDispatch
进入processDispatchResult
org.springframework.web.servlet.DispatcherServlet#processDispatchResult
进入render
org.springframework.web.servlet.DispatcherServlet#render
进入resolveViewName
org.springframework.web.servlet.DispatcherServlet#resolveViewName

protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
            HttpServletRequest request) throws Exception {

        for (ViewResolver viewResolver : this.viewResolvers) {
        /**这里就得到我们所配置的View,并且注意List<ViewResolver> viewResolvers这个List是排序的,
        *会按照配置的视图解析器的order属性进行存取
        *练习16中,我们定义了两个视图解析器,一个是InternalResourceViewResolver
        *还有一个是BeanNameViewResolver,
        *这里会首先用BeanNameViewResolver的resolveViewName方法尝试返回View对象,
        *但是我们是通过return "redirect:/index.jsp"返回的,
        *所以BeanNameViewResolver通过他的context.getBean(...)方法无法根据viewName获取到对应的View
        *所以进入下一次循环。得到的是InternalResourceViewResolver,
        *而InternalResourceViewResolver用的是父类UrlBasedViewResolver的父类AbstractCachingViewResolver的
        *resolveViewName方法;
        **/
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                return view;
            }
        }
        return null;
    }

进入
org.springframework.web.servlet.view.AbstractCachingViewResolver#resolveViewName

@Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        if (!isCache()) {
        //UrlBasedViewResolver重写了createView方法,所以这里调用的是UrlBasedViewResolver的createView
            return createView(viewName, locale);
        }
        else {
            Object cacheKey = getCacheKey(viewName, locale);
            View view = this.viewAccessCache.get(cacheKey);
            //如果缓存没有,会把咱们返回的视图解析器进行缓存,下次就直接返回。
            if (view == null) {
                synchronized (this.viewCreationCache) {
                    view = this.viewCreationCache.get(cacheKey);
                    if (view == null) {
                        // Ask the subclass to create the View object.
                        //重点看createView方法
                        //UrlBasedViewResolver重写了createView方法,所以这里调用的是UrlBasedViewResolver的createView
                        view = createView(viewName, locale);
                        if (view == null && this.cacheUnresolved) {
                            view = UNRESOLVED_VIEW;
                        }
                        if (view != null) {
                            this.viewAccessCache.put(cacheKey, view);
                            this.viewCreationCache.put(cacheKey, view);
                            if (logger.isTraceEnabled()) {
                                logger.trace("Cached view [" + cacheKey + "]");
                            }
                        }
                    }
                }
            }
            return (view != UNRESOLVED_VIEW ? view : null);
        }
    }

这里需要明白抽象类之间的相互调用,以免混淆的具体实现方法。
最后看org.springframework.web.servlet.view.UrlBasedViewResolver#createView

@Override
    protected View createView(String viewName, Locale locale) throws Exception {
        // If this resolver is not supposed to handle the given view,
        // return null to pass on to the next resolver in the chain.
        if (!canHandle(viewName, locale)) {
            return null;
        }
        /**
        * 这里就一目了然了。redirect返回的是RedirectView
        * forward返回的是InternalResourceView
        **/
        // Check for special "redirect:" prefix.
        if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
            String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
            RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
            return applyLifecycleMethods(viewName, view);
        }
        // Check for special "forward:" prefix.
        if (viewName.startsWith(FORWARD_URL_PREFIX)) {
            String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
            return new InternalResourceView(forwardUrl);
        }
        // Else fall back to superclass implementation: calling loadView.
        return super.createView(viewName, locale);
    }

练习18 springMVC表单标签

通过使用springMVC的表单标签可以实现将模型数据中的属性与HTML表单元素相绑定,以实现表单数据更便捷编辑与表单值回显
一、Form标签
一般情况下通过GET请求获取表单页面,而通过POST请求提交表单页面,因此获取表单页面和提交表单页面的URL是相同的。只要满足最佳条件的契约,<form:form>标签就无需通过action属性指定表单起脚的URL。
可以通过modelAttribute属性指定绑定模型属性,若没有指定该属性,则默认从request域中读取command的表单bean。如该属性值也不存在,则报如下错误:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute
二、表单标签
springMVC提供了多个表单组件标签,如<form:input />、<form:select />等,用以绑定表单字段的属性值,它们的共有属性如下:
path:表单字段,对应HTML的name属性,支持级联属性
htmlEscape:是否对表单值的特殊字符进行转换,默认为true
cssClass:表单组件的css样式名
cssErrorClass:表单组件的数据存在错误时,采取的css样式
<form:input /><form:password /><form:hidden /><form:textarea />:对应HTML表单的text,password,hidden,textarea标签
<form:radiobutton />:单选框标签,当表单bean的属性值和value相同时,单选框被选中。
<form:radiobuttons />:单选框组标签,用于构造多个单选框
- items:可以是一个list,string[] 或map
- itemValue:是定radio的value值,可以是集合bean中的一个属性值
- itemLabel:指定radio的label名称
- delimiter:多个单选框可以指定分隔符
<form:checkbox />:复选框组件,用于构造单个复选框
<form:checkboxs />:用于构造多个复选框,使用方式同<form:radiobuttons />标签
<form:select />:用于构造下拉组件,使用方式同<form:radiobuttons />标签
<form:option />:下拉框选项组件标签,使用方式同<form:radiobuttons />标签
<form:errors />:显示表单组件或数据校验所对应的错误
- <form:errors path="*"/>:显示表单所有的错误
- <form:errors path="user*"/>:显示以user为前缀的属性对应的错误
- <form:errors path="username"/>:显示特定表单对象属性的错误
三、表单标签练习
通过一个小例子练习上面几个标签

STEP1 :准备数据

初始化数据类,因为没有DAO,所以我放到static变量中。不管通过说明办法,初始化数据就OK。我是通过spring的初始化配置方法进行初始化的。

<bean id="dataInit" class="com.iboray.smt.commons.DataInit"
          scope="singleton" lazy-init="false" init-method="init"></bean>
public class DataInit {

    private static List<User> users = null;
    private static List<Detp> detps = null;
    private void init(){
        System.out.println("数据初始化... ...");
        users = new ArrayList<>();
        users.add(new User(1,"zs","112233",new Detp(1,"dept1")));
        users.add(new User(2,"ls","22233",new Detp(1,"dept1")));
        users.add(new User(3,"wz","41223",new Detp(2,"dept2")));
        users.add(new User(4,"zl","535454",new Detp(3,"dept3")));
        users.add(new User(5,"mq","565575",new Detp(3,"dept3")));
        detps = new ArrayList<>();
        detps.add(new Detp(1,"dept1"));
        detps.add(new Detp(2,"dept2"));
        detps.add(new Detp(3,"dept3"));
        detps.add(new Detp(4,"dept4"));

    }

    public static List<User> getUsers() {
        return users;
    }

    public static List<Detp> getDetps() {
        return detps;
    }
}
STEP2 :增加controller对应的方法
@Controller
@RequestMapping(value = "/user")
public class UserController {
    //保存,并重定向
    @RequestMapping(value = "/saveUser",method = RequestMethod.POST)
    public String saveUser(User user){
        DataInit.getUsers().add(user);
        List l = DataInit.getUsers();
        return "redirect:/user/userInput";
    }

    /**
    *这里是偷懒。需要说明一下,为了少写一个页面
    *所以添加和列表都放在同一个页面中。
    *
    **/
    @RequestMapping(value = "/userInput",method=RequestMethod.GET)
    public String userInput(Map<String,Object> map){
        //对应form:form标签的modelAttribute属性,否则会报上面提到的错误
        map.put("user",new User());
        //对应form:select 中的items属性
        map.put("depts", DataInit.getDetps());
        //查询列表所需
        map.put("users",DataInit.getUsers());
        return "springForm";
    }
}
STEP3 :增加展现页面springForm.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form:form action="/user/saveUser" method="post" modelAttribute="user">
    <form:input path="id" ></form:input>
    <form:input path="name" ></form:input>
    <form:input path="pwd" ></form:input>
    <form:select path="dept.id" items="${depts }" itemLabel="deptName" itemValue="id" ></form:select>
    <input type="submit" value="submit">
</form:form>
<hr>
<table border="1">
<c:forEach items="${users}" var="user">
    <tr>
        <td width="20">${user.id}</td>
        <td width="150">${user.name}</td>
        <td width="150">${user.pwd}</td>
        <td width="20">${user.dept.id}</td>
        <td width="40">${user.dept.deptName}</td>
    </tr>
</c:forEach>
    </table>
</body>
</html>
STEP4 :请求地址

请求:http://ip:port/user/userInput
返回:submit后结果
1到5为咱们初始化数据
这里写图片描述

练习19 处理静态资源

REST风格的资源URL不希望带有.html或.do等后缀,若将dispatcherServlet的请求映射为/,则springMVC将捕获web应用的所有请求,包括静态资源的请求,springMVC会将他当成一个普通的请求来处理,因找不到对应处理器而报错
我们可以在springMVC配置中增加<mvc:default-servlet-handler />的方式解决静态资源的问题。
<mvc:default-servlet-handler />将在springMVC上下文中定义一个defaultservlethttprequesthandler,他会对进入dispatcherServlet的请求进行筛查,如发现没有经过映射的请求,就讲请求交由web应用服务器默认的servlet处理,如果不是静态资源且有映射的才交由dispatcherservlet进行处理
一般web应用服务器默认的servlet的名称都是default,所以不用显示配置<mvc:default-servlet-handler />的default-servlet-name=”“属性,若不是默认的名称,则需要配置。
特别提醒,如果配置了<mvc:default-servlet-handler />,需要配置<mvc:annotation-driven >,原因后面再单说。

练习20 自定义类型转换器

有时候要写自定义类型转换器,那就得先了解一下数据绑定流程。

数据绑定流程

  1. springMVC主框架将servletRequest对象及目标方法的入参实例传递给webDataBinderFactory实例(可再源码分析查看内部结构),以创建DataBinder实例对象。
  2. DataBinder调用装配在springMVC上下文中的conversionService组件进行数据类型转换,数据格式化工作,将servlet请求信息填充到入参对象中。
  3. 调用validator组件对已绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData对象。
  4. springMVC抽取BindingResult中的入参对象和校验错误对象,将它们赋给处理方法的响应入参。
    数据绑定
    springMVC通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中,数据绑定的核心部件是DataBinder,运行机制如下:
    这里写图片描述

数据转换

springMVC上下文中内建了很多转换器,可完成大多数java类型的转换工作。

自定义类型转换器

  1. ConversionService是Spring类型转换体系的核心接口,可以利用ConversionServiceFactoryBean在Spring IOC容器中定义一个ConversionService。Spring将自动识别出IOC容器中的ConversionService,并在Bean属性配置及SpringMVC处理方法入参绑定等场合使用它进行数据转换。
  2. 可通过ConversionServiceFactoryBean的converters属性注册自定义的类型转换器。

spring支持的类型转换器

spring定义了三种类型的转换器接口,实现任意一个接口都可以作为自定义类型转换器注册到ConversionServiceFactoryBean中
1. Converter<S,T>将S类型对象转换为T类型对象
2. ConverterFactory 将相同系列多个“同质”Converter封装在一起,如果希望将一种类型对象转换为另一种类型及子类的对象(例如将String转换为Number及Number的子类[Interger,Double,Long]等)可使用该转换器工厂类
3. GenericConverter会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换

写一个自定义类型转换器UserConverterService

STEP 1:

@Component
public class UserConverterService implements Converter<String,User> {

    @Override
    public User convert(String source) {
    //一会儿在这里打断点,查看ConversionService是否加入了我们的转换器
        if(source != null && !"".equals(source.trim())){
            String[] s = source.split(";");
            if (s.length == 3){
                User u = new User(Integer.parseInt(s[0]),s[1],s[2]);

                System.out.println("source:"+source);
                System.out.println("result:"+u);

                return u;
            }
        }
        return null;
    }
}

STEP 2 :SpringMVC配制文件

    <mvc:annotation-driven conversion-service="conversionService" ></mvc:annotation-driven>

    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <ref bean="userConverterService"></ref>
            </set>
        </property>
    </bean>

STEP 3: Controller方法

/**看入参@RequestParam("userStr") User user,其实是我们请求的字符串,但我们是用User接参的。
*通过http://host/user/saveUserByConverter?userStr=13;qweqwe;xxxooooo请求地址,
*找到我们对应的controller,在此期间,经过了上面说的数据绑定流程,
*判断出我们需要的转换器是String  转  User对象的。
*那正好是我们写的自定义转换器,所以就可以正常转换了。
*/
    @RequestMapping(value = "/saveUserByConverter",method = RequestMethod.GET)
    public String saveUserByConverter(@RequestParam("userStr") User user){
        DataInit.getUsers().add(user);
        List l = DataInit.getUsers();
        return "redirect:/user/userInput";
    }

源码分析

在自定义的转换器中打断点,找到如下方法
org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument

        if (binderFactory != null) {
        //查看WebDataBinder对象
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
            arg = binder.convertIfNecessary(arg, paramType, parameter);
        }

WebDataBinder对象:
这里写图片描述

测试

  1. 请求:http://host/user/saveUserByConverter?userStr=13;qweqwe;xxxooooo
  2. 在自定义的converter中打断点,查看变量
    这里写图片描述
    进一步查看
    这里写图片描述
    ……
    这里写图片描述
    可以看出已经有我们自己的converter了
  3. console输出:
source:13;qweqwe;xxxooooo
result:User{id=13, name='qweqwe', pwd='xxxooooo', dept=null}

练习21 mvc:annotation-driven

<mvc:annotation-driven>会自动注册RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver三个Bean。
还提供以下支持。
1. 支持使用ConversionService实例对表单数据进行类型转换
2. 支持使用@NumberFormatAnnotation、@DateTimeFormat注解完成数据类型格式化。
3. 支持使用@Valid注解对JavaBean实例进行JSR 303验证
4. 支持使用@RequestBody和@ResponseBody注解

对比配置annotation-driven前后的差异

都在org.springframework.web.servlet.DispatcherServlet#doDispatchmv = ha.handle(processedRequest, response, mappedHandler.getHandler());打断点重点查看handlerAdapters
1. 既没有配置<mvc:default-servlet-handler />也没有配置<mvc:annotation-driven>
这里写图片描述
请求正常,但是查看源码AnnotationMethodHandlerAdapter已经有删除线了Spring已经不建议使用了,具体原因这里不阐述了。
2. 配置<mvc:default-servlet-handler />没有配置<mvc:annotation-driven>
这里写图片描述
请求出错 HTTP Status 404 - /user/saveUser
3. 既配置<mvc:default-servlet-handler />也配置<mvc:annotation-driven>
这里写图片描述
请求正常
这样通过对比可以看出<mvc:annotation-driven>的作用,所以在使用了springMVC框架的项目中,建议都增加该配置。

练习22 initBinder

由InitBinder标识的方法可以对webDataBinder对象进行初始化。webDataBinder是DataBinder的子类,用于完成由表单字段到javaBean属性的绑定。
InitBinder方法不能有返回值,必须声明void。
InitBinder方法的参数通常是webDataBinder。
数据绑定流程参考练习20自定义类型转换器中的数据绑定流程
练习代码
controller

@InitBinder
    public void testInitBinder(WebDataBinder binder){
        //不绑定表单name为name的值。
        binder.setDisallowedFields("name");
    }

练习23 数据格式化

如果我们配置了annotation-driver并且使用了格式化注解 例如:NumberFormat或DateTimeFormat,那么springMVC在处理数据绑定的过程中用的转换器就是FormattingConversionServiceFactoryBean。

FormattingConversionServiceFactoryBean内部已注册了:
1. NumberFormatAnnotationFormaterFactory:支持对数字类型的属性用@NumberFormat注解
2. JodaDateTimeFormatAnnotationFormatterFactory:支持对日期类型属性使用@DateTimeFormat注解
装配了FormattingConversionServiceFactoryBean后就可以在springMVC入参绑定及模型数据输出时使用注解驱动了。mvc:annotation-driven默认创建的ConversionService实例即为FormattingConversionServiceFactoryBean。

日期格式化

@DateTimeFormat注解可对java.util.Date、java.util.Calendar、java.long.Long时间类型进行标注:
1. pattern属性:类型为字符串,指定解析和格式化字段数据的模式,如:yyyy-MM-dd
2. ios属性:类型为DateTimeFormat.ISO,指定解析和格式化字段数据的ISO模式,包括四种,ISO.NONE(不使用,默认),ISO.DATE,ISO.TIME,ISO.DATE_TIME
3. style属性,字符串类型,通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二为表示时间的格式,S:短日期/时间 格式,M:中日期/时间 格式,L:长日期/时间 格式,F:完整日期/时间 格式,- :忽略日期或时间格式。

数值格式化

@NumberFormat可对数字类型的属性进行标注,它拥有两个互斥的属性:
1. style:类型为NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型),Style.CURRENCY(货币类型),Style.PERCENT(百分比)
2. pattern:类型为String,自定义样式,如pattern=”#,###,###.#”。

源码分析

断点进入
org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument
查看WebDataBinder binder
这里会有三种情况出现,大家需要注意一下
接参的bean:User.java

public class User {

    private int id;

    private String name;

    private  String pwd;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;

    @NumberFormat(pattern = "#,###,###.#")
    private BigDecimal pay;

    getter  setter ... ...
 }
第一种,配置了annotation-driven且自定义类型转换器是通过ConversionServiceFactoryBean加入的。

这种情况下即便是加入了@DateTimeFormart @NumberFormat都无法正确格式化,会出现404错误。
springMVC配置如下


<mvc:annotation-driven conversion-service="conversionService" ></mvc:annotation-driven>

<!--重点在这里,我们需要格式化参数,这里配置的却是ConversionServiceFactoryBean-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
     <property name="converters">
         <set>
           <ref bean="userConverterService"></ref>
         </set>
     </property>
</bean>

binder相关部分截图
这里写图片描述

第二种,只配置annotation-driven

结果是可以正确格式化参数,但无法使用自定义类型转换器。
springMVC配置如下

    <mvc:annotation-driven ></mvc:annotation-driven>
    
    
  • 1

binder相关截图,可以看出有两个格式化Parser,但是converters有119个,里面我看了。没有我们自己的String –> User的转换器,这个转换器功能见数据绑定流程参考练习20自定义类型转换器中的数据绑定流程
这里写图片描述

第三种,配置了annotation-driven且自定义类型转换器是通过FormattingConversionServiceFactoryBean加入的。

springMVC配置如下

    <mvc:annotation-driven conversion-service="conversionService" ></mvc:annotation-driven>
<!--这里可以看出我们用的是FormattingConversionServiceFactoryBean的转换器-->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <ref bean="userConverterService"></ref>
            </set>
        </property>
    </bean>

binder的相关截图如下,可以看出有两个格式化Parser,并且converters有120个,里面我看了,包含我们自己的类型转换器。
这里写图片描述

结论

通过测验,我们知道,如果我们需要格式化且有自己的类型转换器,那我们需要注意在增加自定义类型转换器时,需要通过FormattingConversionServiceFactoryBean来加入。这样才能正常转换,并且正常格式化。

练习24 数据校验

原理

Spring4.0拥有自己独立的数据校验框架,同时支持JSR303标准的校验框架。
Spring在进行数据绑定时,可同时调用数据校验框架完成数据校验工作,在springMVC中可直接通过注解驱动的方式进行数据校验。
Spring的LocalValidatorFactoryBean既实现了spring的validator接口,也实现了JSR303的validator接口,只要在spring容器中定义一个LocalValidatorFactoryBean即可将其注入到需要校验的bean中。
Spring本身并没有提供JSR303的实现,所以必须将JSR303的实现者的jar包放到类路径下。
<mvc:annotation-driven>会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@Valid注解,即可让springMVC在完成数据绑定后执行数据校验工作。
在已经标注了JSR303注解的表单/命令对象前标注一个@Valid,springMVC框架在将请求参数绑定到该入参对象后,就会调用验证框架根据注解声明的校验规则实施校验。
springMVC是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象(springForm表单中Form标签的modelAttribute属性就是命令对象)的校验结果保存到随后的入参中,这个保存校验结果的入参必须是BindingResult或Errors类型。这两个类都位于org.springframework.validation包中。
需要校验的Bean对象和其绑定结果对象或错误对象是成对出现的,它们之间不允许声明其他入参
Errors接口提供了获取错误信息的方法,如getErrorCount(),result.getFieldErrors()等。
BindingResult扩展了Errors接口

在页面上显示错误

springMVC除了会将表单/命令对象的校验结果保存到对象的BindingResult或Errors对象中,还会将所有的校验结果保存到“隐含模型”中。
即使处理方法的签名中没有对表单/命令对象的结果入参,校验结果也会保存。
隐含对象中的所有数据最终会通过HttpServletRequest的属性列表暴露给JSP视图对象,因此在JSP中可以获取错误信息
在JSP页面上可通过<form:errors path="name"></form:errors> 显示对应属性的错误

国际化

每个属性在数据绑定或数据校验发生错误时,都会生成一个FieldError对象。
当一个属性校验失败后,校验框架会为该属性生成4个教习代码,这些代码以校验注解类名为前缀,结合modelAttribute,属性名及属性类型名生成多个对应的消息代码,例如User类中的pwd属性标注了一个Pattern注解,当属性不满足Pattern的规则时,就会产生以下4个消息代码:
1. Pattern.user.pwd
2. Pattern.pwd
3. Pattern.java.lang.String
4. Pattern
当使用springMVC的Form标签显示错误消息时,springMVC会查看上下文是否装配了对应的国际消息。如果没有,则显示默认的错误消息,若有,就显示对应的国际化消息。
若数据类型转换或数据格式化发生错误时,或该有的参数不存在时,或调用处理方法发生错误时,都会在隐含模型中创建错误消息,其错误代码前缀说明如下。
1. required 必要的参数不存在时,如@RequestParam(“param1”) 标注了一个入参,但该参数不存在
2. typeMismatch 在数据绑定时发生类型不匹配。
3. methodInvocation springMVC在调用处理方法时发生了错误

测试

STEP1 其次要加入依赖库
maven就很容易了。

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>5.1.3.Final</version>
    </dependency>

STEP2 添加校验规则

public class User {
    @NotEmpty
    private String name;

    @NotEmpty
    @Length(max = 5)
    private  String pwd;

    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;
    getter  setter.......
 }

STEP3 按之前的练习14配置资源文件。我这里偷懒就只在中文资源文件i18n_zh_CN.properties里面配置了。

i18n.name=用户名
i18n.pwd=密码
NotEmpty.user.name=name必填
NotEmpty.user.pwd=pwd必填
Length.user.pwd=长度为1~5之间
Past.user.birth=非法的出生日期

typeMismatch.user.birth=出生日期格式错误

STEP4 随后是springMVC配置文件的配置,前提是你已经配置了</mvc:annotation-driven>,这个是必须的。具体可参考练习21

<!--配置国际化资源文件,注意basename的value值,需要对应资源文件-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"></property>
        <property name="defaultEncoding" value="UTF-8"></property>
    </bean>

STEP5 然后再改造练习18的controller,当然最好你可以自己重新写一个。

    @RequestMapping(value = "/saveUser",method = RequestMethod.POST)
    public String saveUser(@Valid User user, BindingResult result //User和其绑定结果的对象,必须成对出现,之间不能有其他入参声明
            ,Map<String,Object> map){
        if(result.getErrorCount() > 0){
            System.out.println("出错了,错误条数为:"+result.getErrorCount());
            for (FieldError e : result.getFieldErrors()){
                System.out.println("ERROR: "+e.getField() + " : "+e.getDefaultMessage());
            }
            /**
             *  这里需要注意的是,错误会绑定到验证的入参对象中,而表单如果需要显示错误,
             *  就需要在<form:form action="xx" method="xx" modelAttribute="user">
             *  <form:errors path="*" ></form:errors> 显示所有错误
             *  <form:errors path="name"></form:errors> 显示对应属性的错误
             */
            return "springForm";
        }
        DataInit.getUsers().add(user);
        map.put("depts", DataInit.getDetps());
        map.put("users",DataInit.getUsers());
        return "redirect:/user/userInput";
    }

STEP6 最后是jsp视图片段

<form:form action="/user/saveUser" method="post" modelAttribute="user">

    <form:errors path="*" ></form:errors>

    <br>

    id:<form:input path="id" ></form:input><br>
    name:<form:input path="name" ></form:input>
        <form:errors path="name"></form:errors><br>
    pwd:<form:input path="pwd" ></form:input>
        <form:errors path="pwd"></form:errors><br>
    birth:<form:input path="birth"></form:input>
        <form:errors path="birth"></form:errors><br>
    pay:<form:input path="pay"></form:input><br>
    deptId:<form:select path="dept.id" items="${depts }" itemLabel="deptName" itemValue="id" ></form:select><br>
    <input type="submit" value="submit">
</form:form>

练习25 返回Json。分析HttpMessageConverter

在实际开发中,很多情况下需要给终端返回Json格式的响应,springMVC给我们提供了非常便捷的返回方式。

返回Json

STEP1 加入Jackson的依赖库
maven 配置文件

<dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.7.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.7.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.7.0</version>
    </dependency>

STEP2 然后controller

    //注意这个注解,这是返回Json必须标注的
    @ResponseBody
    @RequestMapping("/getUserJson")
    public Object getUserJson(){
        List l = DataInit.getUsers();
        return l;//直接返回List
    }

以上两步就可以返回Json了,这是为何呢,其实都是一个叫HttpMessageConverter的对象再起作用。

HttpMessageConverter

一、概述

HttpMessageConverter是spring3.0新添加的一个接口,负责将请求信息转换为一个对象(类型为T),将对象(类型为T)输出为响应信息。
HttpMessageConverter<T>接口定义的方法:
1. Boolean canRead(Class<?> clazz,MediaType mediaType):指定转换器可以读取的对象类型,即转换器是否可以将请求信息转换为clazz类型的对象,同时支持MIME类型(text/html,application/json等)
2. Boolean canWrite(Class<?> clazz,MediaType mediaType):只转换器是否可以将类型为clazz的对象写入到响应流中,响应流中支持的媒体类型在MediaType中定义。
3. List<MediaType> getSupportMediaType():该转换器支持的媒体类型。
4. T read(Class<? extends T> clazz,HttpInputMessage inputMessage):将请求信息流转为T类型的对象。
5. void write(T t,MediaType contentType,HttpOutputMessage outputMessage):将T类型的对象写入到响应流中,同时指定相应的媒体类型为contentType。
使用HttpMessageConverter<T>将请求信息转换并绑定到处理方法的入参中,或将响应结果转换为对应类型的响应信息,spring提供了两种途径。
1. 使用@RequestBody/@ResponseBody对处理方法进行标注
2. 使用HttpEntity<T>/ResponseEntity<T>作为处理方法的入参或返回值。
当控制器使用到以上两种方法时,Spring首先根据请求头或响应头的Accept属性选择匹配的HttpMessageConverter,进而根据参数类型或泛型类型的过滤得到匹配的HttpMessageConverter,若找不到可用的HttpMessageConverter将报错。
注意:@RequestBody/@ResponseBody不需要成对出现
二、HttpMessageConverter工作原理
这里写图片描述
三、HttpMessageConverter的实现类

序号实现类功能说明
1StringHttpMessageConverter将请求信息转为字符串
2FormHttpMessageConverter将表单数据读取到MultiValueMap中
3XmlAwareFormHttpMessageConverter扩展与FormHttpMessageConverter,如果部分表单属性是XML数据,可用该转换器进行读取
4ResourceHttpMessageConverter读写org.springframework.core.io.Resource对象
5BufferedImageHttpMessageConverter读写BufferedImage对象
6ByteArrayHttpMessageConverter读写二进制数据
7SourceHttpMessageConverter读写java.xml.transform.Source类型的对象
8MarshallingHttpMessageConverter通过Spring的org.springframework,xml.Marshaller和Unmarshaller读写XML消息
9Jaxb2RootElementHttpMessageConverter通过JAXB2读写XML消息,将请求消息转换为标注的XmlRootElement和XmlType连接的类中
10MappingJacksonHttpMessageConverter利用Jackson开源包的ObjectMapper读写JSON数据
11RssChannelHttpMessageConverter读写RSS种子消息
12AtomFeedHttpMessageConverter和RssChannelHttpMessageConverter能够读写RSS种子消息

四、加入Jackson的Jar包,转换器的前后对比
DispatchServlet默认装配RequestMappingHandlerAdapter,而RequestMappingHandlerAdapter默认装配如下HttpMessageConverter:
这里写图片描述
加入Jackson相关依赖包之后RequestMappingHandlerAdapter装配的HttpMessageConverter增加了转换Json的转换器
这里写图片描述
为了更好的理解HttpMessageConverter,可以看看这张图(来自网络)
这里写图片描述

小练习

Eg:1 通过@RequestBody/@ResponseBody实现将上传文本文件(并不是真正上传),转换为String 并打印
controller

    @ResponseBody
    @RequestMapping("/testHttpMessageConverter")
    public String testHttpMessageConverter(@RequestBody String file ){
        System.out.println(file);
        return "testHttpMessageConverter date : "+ new Date();
    }

jsp

<form action="/user/testHttpMessageConverter" method="post" enctype="multipart/form-data">
     <input type="file" name="file">
     <input type="submit" name="submit">
</form>

结果
文件内容
这里写图片描述
打印结果
这里写图片描述
Eg:2 通过ResponseEntity<T>实现下载文件
controller

    @RequestMapping("/testResponseEntity")
    public ResponseEntity<byte[]> testResponseEntity(){
        String str = "xttttttsssss >>>>> testResponseEntity";
        byte[] body = str.getBytes();
        HttpHeaders hh = new HttpHeaders();
        hh.add("Content-Disposition","attachment;filename=xxx.txt");
        HttpStatus status = HttpStatus.OK;
        ResponseEntity<byte[]> responseEntity =new ResponseEntity<byte[]>(body,hh,status);
        return  responseEntity;
    }

jsp

<a href="/user/testResponseEntity" >testResponseEntity</a>

下载的结果是
文件为:xxx.txt.html
内容为:
这里写图片描述

练习26 国际化

概述

默认情况下,springMVC会根据Accept-Language参数判断客户端的本地化类型。
当接受请求时,springMVC会在上下文中查找一个本地化解析器(LocalResolver)找到后,使用它获取请求所对应的本地化类型信息。
springMVC还允许装配一个动态更改类型本地化类型的拦截器,这样通过指定一个参数就可用控制单个请求的本地化类型。(因为本地化类型是放到session作用域中)
本地化解析器和本地化拦截器
1. AcceptHeaderLocaleResolver:根据HTTP请求头的Accept-Language来确定本地化类型,如果没有显示定义本地化解析器,springMVC默认使用该解析器。
2. CookieLocaleResolver:根据指定的Cookie值确定本地化类型。
3. SessionLocaleResolver:根据Session中特定的本地化参数确定本地化类型。
4. LocaleChangeInterceptor:从请求中获取本次请求对应的本地化类型。
加入SessionLocaleResolver前后对比
加入前
这里写图片描述
加入后
这里写图片描述
为了更好的理解LocaleChangeInterceptor和SessionLocaleResolver,可以参考如下图(来自网络)
这里写图片描述

小练习

通过默认的本地化解析器AcceptHeaderLocaleResolver这次就不测试了,咱们直接用SessionLocaleResolver来动态改变本地化类型。
STEP1 准备资源文件。其实这些资源文件咱们前面用过的。
在resource目录下,建三个国际化资源文件
i18n.properties和i18n_en_US.properties内容一样

i18n.name=name
i18n.pwd=password

i18n_zh_CN.properties内容如下

i18n.name=用户名
i18n.pwd=密码

STEP2 springMVC增配
这里需要注意的是
1. SessionLocaleResolver的bean的ID一定要是localeResolver,否则会报Cannot change HTTP accept header - use a different locale resolution strategy错误
2. 资源文件要配置_en_US或_zh_CN否则找不到对应本地化类型的资源文件。

    <!--配置国际化资源文件,注意basename的value值,需要对应资源文件-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"></property>
        <property name="defaultEncoding" value="UTF-8"></property>
    </bean>

    <!--配置本地化解析器-->
    <bean id="localeResolver"
          class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>

    <!--配置本地化拦截器-->
    <mvc:interceptors>
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>
    </mvc:interceptors>

STEP3 controller添加目标方法

    @Resource
    private ResourceBundleMessageSource messageSource;

    @RequestMapping("/i18n")
    //Locale这个入参
    public String testI18n(Locale locale){
    //动态改变本地化类型,并且数据本地化后的值
        String str = messageSource.getMessage("i18n.name",null,locale);
        System.out.println(str);
        return "index";
    }

STEP4 jsp

            [ <a href="/user/i18n?locale=zh_CN">i18n_Change_zh_CN</a> ]<br>
            [ <a href="/user/i18n?locale=en_US">i18n_Change_en_US</a> ]
            <br>
            <p> <fmt:message key="i18n.name"></fmt:message></p>
            <p> <fmt:message key="i18n.pwd"></fmt:message></p>

结果:
点击i18n_Change_zh_CN链接,name和pwd变为用户名和密码,
点击i18n_Change_en_US链接,name和pwd变为name和password,它们直接可以互相切换,并且后台console窗口可以输出对应的值。

源码分析

STEP1 进入顶层方法
org.springframework.web.servlet.DispatcherServlet#doDispatch

//执行拦截器的PreHandle方法,随后才执行渲染视图方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    	return;
}

STEP2 进入
org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle
这里写图片描述

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (getInterceptors() != null) {
            for (int i = 0; i < getInterceptors().length; i++) {
            //这里可以看到遍历得到我们的LocaleChangeInterceptor
                HandlerInterceptor interceptor = getInterceptors()[i];
                //执行preHandle方法
                if (!interceptor.preHandle(request, response, this.handler)) {
                    triggerAfterCompletion(request, response, null);
                    return false;
                }
                this.interceptorIndex = i;
            }
        }
        return true;
    }

STEP3 进入
org.springframework.web.servlet.i18n.LocaleChangeInterceptor#preHandle

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws ServletException {
/**可以看到这里获取我们请求的参数。paramName,这个是已经写死的。
*   public static final String DEFAULT_PARAM_NAME = "locale";
*   private String paramName = DEFAULT_PARAM_NAME;
**/
        String newLocale = request.getParameter(this.paramName);
        if (newLocale != null) {
        //根据咱们去的请求参数,获取的结果是SessionLocaleResolver
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver == null) {
                throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
            }
            //这个方法最终执行的是SessionLocaleResolver的setLocaleContext方法
            localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale));
        }
        // Proceed in any case.
        return true;
    }

STEP4 进入
org.springframework.web.servlet.i18n.SessionLocaleResolver#setLocaleContext

    @Override
    public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
        Locale locale = null;
        TimeZone timeZone = null;
        if (localeContext != null) {
            locale = localeContext.getLocale();
            if (localeContext instanceof TimeZoneAwareLocaleContext) {
                timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
            }
        }
        //将locale参数放到session中
        WebUtils.setSessionAttribute(request, LOCALE_SESSION_ATTRIBUTE_NAME, locale);
        WebUtils.setSessionAttribute(request, TIME_ZONE_SESSION_ATTRIBUTE_NAME, timeZone);
    }

STEP5 进入
org.springframework.web.util.WebUtils#setSessionAttribute

    public static void setSessionAttribute(HttpServletRequest request, String name, Object value) {
        Assert.notNull(request, "Request must not be null");
        if (value != null) {
        //关键在这里,看看看,放到session中了。
        //这就实现了单个会话的本地化动态更改。
            request.getSession().setAttribute(name, value);
        }
        else {
            HttpSession session = request.getSession(false);
            if (session != null) {
                session.removeAttribute(name);
            }
        }
    }

STEP6 将STEP1 的mv放到org.springframework.web.servlet.DispatcherServlet#processDispatchResult方法中

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

STEP7 进入processDispatchResult方法

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
            ... ...
            // Did the handler return a view to render?
        if (mv != null && !mv.wasCleared()) {
        //放入了render
            render(mv, request, response);方法
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        ... ..
        }

STEP8 进入render方法

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // Determine locale for request and apply it to the response.
        Locale locale = this.localeResolver.resolveLocale(request);
        //关键在这里,拿到local,写入响应
        response.setLocale(locale);
        ... ...
}

练习27 文件上传

springMVC为文件上传提供了直接的支持,这种支持是通过即插即用的MultipartResolver实现的,spring用Jakarta Commons FileUpload技术实现了一个MultipartResolver实现类CommonsMultipartResolver。
springMVC上下文中默认没有装配MultipartResolver,因此默认情况下不能处理文件上传工作,如需使用,则需要配置MultipartResolver。
配置MultipartResolver时需要注意两个点
1. defaultEncoding 必须和用户JSP的pageEncoding属性一致,以便正确的解析表单内容。
2. 为了让CommonsMultipartResolver正常工作,需要将Jakarta Commons FileUpload及Commons io的包加到项目中。
小练习
STEP1 加入jar包,增加maven配置,就把fileupload和io引入进来

    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.1</version>
    </dependency>

STEP2 springMVC增配

    <!--spring mvc 文件上传-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="1024000"></property>
        <property name="defaultEncoding" value="UTF-8"></property>
        <property name="resolveLazily" value="true"></property>
     </bean>

STEP3 controller增加目标方法

    @RequestMapping("/testFileUpload")
    public String testFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
        System.out.println("getOriginalFilename :"+file.getOriginalFilename());
        System.out.println("getSize :"+file.getSize());
        System.out.println("getInputStream :"+file.getInputStream());
        return "index";
    }

STEP3 jsp增加form表单

<form action="/user/testFileUpload" method="post" enctype="multipart/form-data">
      <input type="file" name="file">
      <input type="submit" name="submit">
</form>

结果,后台正常输出:

getOriginalFilename :屏幕快照 2016-01-13 上午11.32.04.png
getSize :48541
getInputStream :java.io.FileInputStream@1f460230

练习28 自定义拦截器

springMVC可以使用拦截器对请求进行拦截,用户可以自定义拦截器来实现特定的需求,自定义的拦截器必须实现HandlerInterceptor接口。接口中需要实现三个方法
1. preHandler() 这个方法再调用目标方法之前被执行,在该方法中可以对用户请求request 进行处理,如果需要在执行这个方法之后还需要调用其他拦截器,或者目标方法,则必须返回true。否则返回false。
2. postHandler() 这个方法是在目标方法执行完后,且在试图渲染之前执行(也就是DispatchServlet向客户端返回响应前调用),在该方法中对request进行处理。
3. afterCompletion() 这个方法在DispatchServlet完全处理完请求后被调用,可以在该方法中进行一些资源清理的工作。
练习
STEP1 实现HandlerInterceptor接口的实现类MyFirstInterceptor和MySecondInterceptor
MySecondInterceptor和MyFirstInterceptor一样,自己写。

public class MyFirstInterceptor implements HandlerInterceptor{
    /**
     * 执行时间:在调用目标方法之前调用
     * 返回true,执行后续方法,返回false,则都不回执行.
     * 作用:可以用作 权限,日志,事务等.
     *
     * 特殊情况说明:
     * 在多个拦截器出现的时候,如果最后的拦截器返回false,则任然会执行上一个拦截器会的afterCompletion方法
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyFirstInterceptor preHandle" );
        return true;
    }

    /**
     * 执行时间:调用目标方法之后,渲染视图事前
     * 作用:可以对请求域中的属性或视图进行修改
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyFirstInterceptor postHandle" );
    }

    /**
     * 实现时间:渲染视图之后调用
     * 作用:释放资源
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyFirstInterceptor afterCompletion" );
    }
}

STEP2 springMVC增配

    <mvc:interceptors>

        <bean class="com.iboray.smt.controller.MyFirstInterceptor"></bean>
        <!--可以对拦截器做更多设置-->
        <mvc:interceptor>
            <!--代表对/user路劲起作用-->
            <mvc:mapping path="/user/**"/>
            <bean class="com.iboray.smt.controller.MySecondInterceptor"></bean>
        </mvc:interceptor>

        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>

    </mvc:interceptors>

执行/user下的所有目标方法,看后台打印结果,我执行的是<a href="/user/i18n?locale=zh_CN">i18n_Change_zh_CN</a>这个方法,例子 再练习26 国际化中。
后台打印

//顺序执行
MyFirstInterceptor preHandle
MySecondInterceptor preHandle
//目标方法打印
用户名
//反序执行
MySecondInterceptor postHandle
MyFirstInterceptor postHandle
//反序执行
MySecondInterceptor afterCompletion
MyFirstInterceptor afterCompletion

为什么会是这个顺序呢?
因为spring默认会按配置文件是顺序加载自定义的拦截器
如图:
这里写图片描述
为了进一步了解拦截器执行顺序,参考下图(来自网络)
情况1 first和second拦截器的preHandle方法都返回true,也就是正常实行
这里写图片描述
情况2 first拦截器的preHandle方法返回true,second拦截器的preHandle方法返回false,执行流程为实线。
这里写图片描述

练习29 异常处理

springMVC通过HandlerExceptionResolver处理程序异常,包括Handler映射,数据绑定以及目标方法执行时发生的异常。
springMVC提供了HandlerExceptionResolver的实现类
这里写图片描述

DispatchServlet默认装配的HandlerExceptionResolver如果使用<mvc:annotation-driven>,则实现类为
1. ExceptionHandlerExceptionResolver(如果不使用<mvc:annotation-driven>,默认加载的是AnnotationMethodHandlerExceptionResolver)
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver
这里写图片描述

ExceptionHandlerExceptionResolver

主要处理Handler中@ExceptionHandler注解定义的方法
@ExceptionHandler有处理优先级的问题,如发生的是NullPointerException,但声明的是RuntimeException和Exception,此时会根据异常的最近继承关系找到继承深度最浅的那一个@ExceptionHandler注解方法,也就是标记了RuntimeException的方法。
ExceptionHandlerMethodResolver内部若找不到@ExceptionHandler注解的话,会找@ControllerAdvice中的@ExceptionHandler注解方法。并且也会有优先级的问题。
小练习
STEP1 controller加入目标方法和@ExceptionHandler方法

   /**
     * @ExceptionHandler 标注的方法,可以在入参中加入 Exception类型的参数,该参数即对应发生的异常对象.
     * 若希望将异常对象传到页面上,则需要返回ModelAndView,而不能是Map
     */
    @ExceptionHandler({NullPointerException.class})
    public ModelAndView testExceptionHandlerExceptionResolver(Exception ex){
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("ex",ex);
        return mv;
    }
    //测试的目标方法
    @RequestMapping("/testMatch")
    public String testMatch(@RequestParam("a") int a ){
        System.out.println( 10 / a);
        return "index";
    }

STEP2 ControllerAdvice标记的类
需要注意的是,这个类的@ExceptionHandler方法中放入的异常比上面controller中的异常要“小”,也就是继承级别很浅。那如果发生ArithmeticException.class异常,对应处理方法是MyExceptionHandle的testExceptionHandlerExceptionResolver方法。

@ControllerAdvice
public class MyExceptionHandle {
    @ExceptionHandler({ArithmeticException.class})
    public ModelAndView testExceptionHandlerExceptionResolver(Exception ex){
        System.out.println("MyExceptionHandle"+ex);
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("ex",ex);
        return mv;
    }
}

STEP3 error界面承接exception对象

<body>
    ${ex}
</body>

STEP4 测试

<a href="/user/testMatch?a=0" >testMatch</a>

结果打印的是

MyExceptionHandle java.lang.ArithmeticException: / by zero

ResponseStatusExceptionResolver

在异常及异常父类中找到@ResponseStatus注解,然后使用这个注解的属性进行处理
定义一个@ResponseStatus注解修饰的异常类
若再处理器方法中抛出@ResponseStatus修饰的类类型,若ExceptionHandlerExceptionResolver不解析异常,由于触发的异常的类带有@ResponseStatus注解,因此会被ResponseStatusExceptionResolver解析到,最后响应HttpStatus.XXXX代码给客户端,关于其他响应代码参考org.springframework.http.HttpStatus枚举类

小练习

STEP1 添加@ResponseStatus标注的类

@ResponseStatus(value = HttpStatus.NOT_ACCEPTABLE,reason = "非法授权")
public class MyResponseStatusException extends RuntimeException {

}

STEP2 添加controller目标方法

    /**
    *注意:如果将@ResponseStatus放到这里,那么执行完这个方法后会直接返回错误状态码和原因。
    *这样就不用新建那个自己的异常处理类(MyResponseStatusException)了。可以根据自身的业务来做调整。
    **/
    @RequestMapping("/testResponseStatusException")
    public String testResponseStatusException(@RequestParam("a") int a){
        if (a == 10){
            throw new MyResponseStatusException();
        }
        System.out.println("testResponseStatusException  .. . .  . .");
        return "index";
    }

STEP3 测试

<a href="/user/testResponseStatusException?a=1" >testResponseStatusException</a>

结果
这里写图片描述

源码分析

进入doResolveException方法
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#doResolveException

protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
            Object handler, Exception ex) {
        //找到我们标记@ResponseStatus的类或方法
        ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
        if (responseStatus != null) {
            try {
            //执行resolveResponseStatus
                return resolveResponseStatus(responseStatus, request, response, handler, ex);
            }
            catch (Exception resolveEx) {
                logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx);
            }
        }
        return null;
    }

进入resolveResponseStatus
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#resolveResponseStatus

protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //这是注解的两个属性,一个是状态,一个是异常描述
        int statusCode = responseStatus.value().value();
        String reason = responseStatus.reason();
        if (this.messageSource != null) {
            reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale());
        }
        if (!StringUtils.hasLength(reason)) {
        //没有描述就直接写入response
            response.sendError(statusCode);
        }
        else {
        //否则重载sendError方法,把状态码和原因一块写入
            response.sendError(statusCode, reason);
        }
        return new ModelAndView();
    }

DefaultHandlerExceptionResolver

这个异常解析器是对一些特殊的异常进行处理的。如
HttpRequestMethodNotSupportedException
MissingServletRequestParameterException
ServletRequestBindingException
ConversionNotSupportedException
….等。是不是似曾相识呢?对,这些就是springMVC自己的一些异常处理方法。

小练习

STEP1 controller

    //重点是POST请求
    @RequestMapping(value = "/testDefaultHandlerExceptionResolver",method = RequestMethod.POST)
    public String testDefaultHandlerExceptionResolver(){
            System.out.println("testDefaultHandlerExceptionResolver  .. . .  . .");
        return "index";
    }

STEP2 请求

<!--明显是个GET请求-->
<a href="/user/testDefaultHandlerExceptionResolver" >testDefaultHandlerExceptionResolver</a>

结果:
这里写图片描述

源码分析

进入
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#doResolveException

    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
            Object handler, Exception ex) {

        try {
            if (ex instanceof NoSuchRequestHandlingMethodException) {
                return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, request, response,
                        handler);
            }
            else if (ex instanceof HttpRequestMethodNotSupportedException) {
            //这里就返回那个错误页面
                return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request,
                        response, handler);
            }
            ...
    }

SimpleMappingExceptionResolver

SimpleMappingExceptionResolver可以对所有异常进行统一处理,它将异常名映射为视图名,也就是说。捕获异常后,输出到指定的视图。
小练习
STEP1 增加controller

    @RequestMapping("/testSimpleMappingExceptionResolver")
    public String testSimpleMappingExceptionResolver(@RequestParam("a") String a){
        Integer.parseInt(a);
        System.out.println("testSimpleMappingExceptionResolver  .. . .  . .");
        return "index";
    }

STEP2 测试 未配置异常解析器

<a href="/user/testSimpleMappingExceptionResolver?a=a" >testSimpleMappingExceptionResolver</a>

结果
这里写图片描述
STEP3测试 配置异常解析器
增加springMVC配置

    <!--使用简单映射异常解析器-->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!--对应视图的异常参数名-->
        <property name="exceptionAttribute" value="ex"></property>
        <!--异常及对应的视图-->
        <property name="exceptionMappings">
            <props>
                <!--这个异常指向error视图-->
                <prop key="java.lang.NumberFormatException">error</prop>
            </props>
        </property>
    </bean>

再进行STEP2 测试,结果:
这里写图片描述

    • 1
      点赞
    • 1
      收藏
      觉得还不错? 一键收藏
    • 0
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

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

    余额充值