SpringMVC学习总结

第一章 基本的请求和响应

第一节 概述

1、SpringMVC 优势

SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 Java EE 项目表述层开发的首选方案。之所以能做到这一点,是因为 SpringMVC 具备如下显著优势:

  • Spring 家族原生产品,与 IOC 容器等基础设施无缝对接
  • 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
  • 代码清新简洁,大幅度提升开发效率
  • 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
  • 性能卓著,尤其适合现代大型、超大型互联网项目要求

2、表述层框架要解决的基本问题

  • 请求映射
  • 数据输入
  • 视图界面
  • 请求分发
  • 表单回显
  • 会话控制
  • 过滤拦截
  • 异步交互
  • 文件上传
  • 文件下载
  • 数据校验
  • 类型转换

3、SpringMVC 代码对比

①基于原生 Servlet API 开发代码片段

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {   
    
    String userName = request.getParameter("userName");
    
    System.out.println("userName="+userName);
    
}

②基于 SpringMVC 开发代码片段

@RequestMapping("/user/login")
public String login(@RequestParam("userName") String userName){
    
    System.out.println("userName="+userName);
    
    return "result";
}

第二节 HelloWorld

1、功能需求

①访问首页

②在首页点超链接

2、搭建环境

①导入依赖

<dependencies>
    <!-- SpringMVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.1</version>
    </dependency>
    
    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    
    <!-- ServletAPI -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    
    <!-- Spring5和Thymeleaf整合包 -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.12.RELEASE</version>
    </dependency>
</dependencies>

由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。

②日志配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 指定日志输出的位置 -->
    <appender name="STDOUT"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 日志输出的格式 -->
            <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
            <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
        </encoder>
    </appender>
    
    <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
    <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
    <root level="INFO">
        <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
        <appender-ref ref="STDOUT" />
    </root>
    
    <!-- 根据特殊需求指定局部日志级别 -->
    <logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG" />
    
</configuration>

③web.xml

<!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    
    <!-- DispatcherServlet的全类名 -->
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
    <!-- 通过初始化参数指定SpringMVC配置文件位置 -->
    <init-param>
    
        <!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
        <param-name>contextConfigLocation</param-name>
    
        <!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    
    <!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
    <!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
    <load-on-startup>1</load-on-startup>
    
</servlet>
    
<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    
    <!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
    <!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
    <!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

④Spring配置文件

<!-- 自动扫描包 -->
<context:component-scan base-package="com.atguigu.mvc.handler"/>
    
<!-- Thymeleaf视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    <property name="order" value="1"/>
    <property name="characterEncoding" value="UTF-8"/>
    <property name="templateEngine">
        <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
            <property name="templateResolver">
                <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
    
                    <!-- 视图前缀 -->
                    <property name="prefix" value="/WEB-INF/templates/"/>
    
                    <!-- 视图后缀 -->
                    <property name="suffix" value=".html"/>
                    <property name="templateMode" value="HTML5"/>
                    <property name="characterEncoding" value="UTF-8" />
                </bean>
            </property>
        </bean>
    </property>
</bean>

3、代码实现

①创建请求处理器类

[1]名称

SpringMVC 对处理请求的类并没有特殊要求,只要是 POJO 即可。我们自己习惯上有两种命名方式:

  • XxxHandler:意思是 Xxx 处理器的意思
  • XxxController:意思是 Xxx 控制器的意思

这只是一个命名的习惯,不是语法要求。所以往往把处理请求的类叫做『Handler类』,处理请求的方法叫做『Handler方法』。

[2]创建

@Controller
public class Demo01HelloHandler {
    
    
    
}

②实现访问首页

[1]创建 handler 方法
@Controller
public class Demo01HelloHandler {

    // @RequestMapping注解在请求地址和Java方法之间建立映射关系
    @RequestMapping("/")
    public String showPortal() {
        return "portal";
    }

}
[2]在首页编写超链接

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    
    <h1>首页</h1>
    
    <!-- 以后我们会越来越倾向于用一句话来作为请求的URL地址,在这样的一句话中使用“/”分隔各个单词 -->
    <!-- say hello to spring mvc -->
    <!-- /say/hello/to/spring/mvc -->
    <a th:href="@{/say/hello/to/spring/mvc}">HelloWorld</a><br/>
    
</body>
</html>

③实现点击超链接

[1]加入日志记录仪

在 handler 类中声明一个成员变量:

private Logger logger = LoggerFactory.getLogger(this.getClass());
  • Logger:org.slf4j.Logger
  • LoggerFactory:org.slf4j.LoggerFactory

使用日志而不是 sysout 的方式在控制台打印,好处在于:根据日志的级别可以非常方便的控制某些日志是否打印。

不管 sysout 还是日志,只要是在控制台打印数据,底层都是 I/O 操作。在项目中,访问数据库和 I/O 都是典型的高消耗型的操作。

在控制台打印的数据,用户看不到,打印它肯定是为了调试程序等类似需求。所以在项目上线的时候,用于调试程序的控制台打印都应该去掉。

如果我们是通过 sysout 方式打印,那么它们会分散在项目中的各个地方,想要通过手动方式删除会很繁琐也可能会有遗漏。

而使用日志的方式,只要在配置文件中修改一下日志级别,就能全面控制某个级别的日志打印或者不打印。

而且日志框架通常都能够指定日志保存的位置,如果有需要可以将日志数据保存到数据库等位置。

[2]声明 handler 方法
// 以后我们会越来越倾向于用一句话来作为请求的URL地址
// 在这样的一句话中使用“/”分隔各个单词
@RequestMapping("/say/hello/to/spring/mvc")
public String sayHello() {
    
    // 方法内部打印日志,证明 SpringMVC 确实调用了这个方法来处理请求
    logger.debug("我是 SpringMVC 的 Hello world。");
    
    return "target";
}
[3]创建目标页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>目标页面</title>
</head>
<body>
    
    <h1>目标页面</h1>
    
    <a th:href="@{/}">回首页</a>
    
</body>
</html>

④整体流程解析

第三节 @RequestMapping注解

从注解名称上我们可以看到,@RequestMapping注解的作用就是将请求的 URL 地址和处理请求的方式关联起来,建立映射关系。

SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的方法来处理这个请求。

1、匹配方式说明

①精确匹配

在@RequestMapping注解指定 URL 地址时,不使用任何通配符,按照请求地方进行精确匹配。

<a th:href="@{/say/hello/to/spring/mvc}">HelloWorld</a><br/>
@RequestMapping("/say/hello/to/spring/mvc")

②模糊匹配

在@RequestMapping注解指定 URL 地址时,通过使用通配符,匹配多个类似的地址。

<h3>测试@RequestMapping注解匹配方式</h3>
<a th:href="@{/fruit/apple}">@RequestMapping模糊匹配[apple]</a><br/>
<a th:href="@{/fruit/orange}">@RequestMapping模糊匹配[orange]</a><br/>
<a th:href="@{/fruit/banana}">@RequestMapping模糊匹配[banana]</a><br/>
@RequestMapping("/fruit/*")

2、在类级别标记

①超链接的HTML标签

<h3>测试@RequestMapping注解标记在类上</h3>
<a th:href="@{/user/login}">用户登录</a><br/>
<a th:href="@{/user/register}">用户注册</a><br/>
<a th:href="@{/user/logout}">用户退出</a><br/>

②仅标记在方法上的@RequestMapping注解

@RequestMapping("/user/login")
@RequestMapping("/user/register")
@RequestMapping("/user/logout")

③分别标记在类和方法上的@RequestMapping注解

在类级别:抽取各个方法上@RequestMapping注解地址中前面重复的部分

@RequestMapping("/user")

在方法级别:省略被类级别抽取的部分

@RequestMapping("/login")
@RequestMapping("/register")
@RequestMapping("/logout")

3、附加请求方式要求

①请求方式

HTTP 协议定义了八种请求方式,在 SpringMVC 中封装到了下面这个枚举类:

public enum RequestMethod {

	GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE

}

②@RequestMapping附加请求方式

前面代码中,只要求请求地址匹配即可,现在附加了请求方式后,还要求请求方式也必须匹配才可以。

[1]HTML代码
<h3>测试@RequestMapping注解限定请求方式</h3>
<a th:href="@{/emp}">同地址GET请求</a><br/>
<form th:action="@{/emp}" method="post">
    <button type="submit">同地址POST请求</button>
</form>
<br/>
[2]handler方法

处理 GET 请求:

@RequestMapping(value = "/emp", method = RequestMethod.GET)
public String empGet() {
    
    logger.debug("GET 请求");
    
    return "target";
}

处理 POST 请求:

@RequestMapping(value = "/emp", method = RequestMethod.POST)
public String empPost() {
    
    logger.debug("POST 请求");
    
    return "target";
}

③进阶版

原版 进阶版
@RequestMapping(value = "/emp",
method = RequestMethod.GET)
@GetMapping("/emp")
@RequestMapping(value = "/emp",
method = RequestMethod.POST)
@PostMapping("/emp")

除了 @GetMapping、@PostMapping 还有下面几个类似的注解:

  • @see PutMapping
  • @see DeleteMapping
  • @see PatchMapping

另外需要注意:进阶版的这几个注解是从 4.3 版本才开始有,低于 4.3 版本无法使用。

4、Ambiguous mapping异常

出现原因:多个 handler 方法映射了同一个地址,导致 SpringMVC 在接收到这个地址的请求时该找哪个 handler 方法处理。

解决方案:附加上不同 请求处理方式。

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'demo03MappingMethodHandler' method
com.atguigu.mvc.handler.Demo03MappingMethodHandler#empPost()
to { [/emp]}: There is already 'demo03MappingMethodHandler' bean method
com.atguigu.mvc.handler.Demo03MappingMethodHandler#empGet() mapped.

第四节 获取请求参数

1、一名一值

①超链接

<a th:href="@{/param/one/name/one/value(userName='tom')}">一个名字一个值的情况</a><br/>

②@RequestParam注解

[1]最基本的用法
	x@RequestMapping("/param/one/name/one/value")public String oneNameOneValue(        // 使用@RequestParam注解标记handler方法的形参        // SpringMVC 会将获取到的请求参数从形参位置给我们传进来        @RequestParam("userName") String userName) {        logger.debug("获取到请求参数:" + userName);        return "target";}java
[2]@RequestParam注解省略的情况
@RequestMapping("/param/one/name/one/value")
public String oneNameOneValue(
        // 当请求参数名和形参名一致,可以省略@RequestParam("userName")注解
        // 但是,省略后代码可读性下降而且将来在SpringCloud中不能省略,所以建议还是不要省略
        String userName
) {
    
    logger.debug("★获取到请求参数:" + userName);
    
    return "target";
}
[3]必须的参数没有提供

页面信息说明:

  • 响应状态码:400(在 SpringMVC 环境下,400通常和数据注入相关)
  • 说明信息:必需的 String 请求参数 'userName' 不存在

原因可以参考 @RequestParam 注解的 required 属性:默认值为true,表示请求参数默认必须提供

	/**
	 * Whether the parameter is required.
	 * <p>Defaults to {@code true}, leading to an exception being thrown
	 * if the parameter is missing in the request. Switch this to
	 * {@code false} if you prefer a {@code null} value if the parameter is
	 * not present in the request.
	 * <p>Alternatively, provide a {@link #defaultValue}, which implicitly
	 * sets this flag to {@code false}.
	 */
	boolean required() default true;
[4]关闭请求参数必需

required 属性设置为 false 表示这个请求参数可有可无:

@RequestParam(value = "userName", required = false)
[5]给请求参数设置默认值

使用 defaultValue 属性给请求参数设置默认值:

@RequestParam(value = "userName", required = false, defaultValue = "missing")

此时 required 属性可以继续保持默认值:

@RequestParam(value = "userName", defaultValue = "missing")

2、一名多值

①表单

<form action="team" method="post">
    请选择你最喜欢的球队:
    <input type="checkbox" name="team" value="Brazil"/>巴西
    <input type="checkbox" name="team" value="German"/>德国
    <input type="checkbox" name="team" value="French"/>法国
    <input type="checkbox" name="team" value="Holland"/>荷兰
    <input type="checkbox" name="team" value="Italian"/>意大利
    <input type="checkbox" name="team" value="China"/>中国
    <br/>
    <input type="submit" value="保存"/>
</form>

②handler方法

@RequestMapping("/param/one/name/multi/value")
public String oneNameMultiValue(
    
        // 在服务器端 handler 方法中,使用一个能够存储多个数据的容器就能接收一个名字对应的多个值请求参数
        @RequestParam("team") List<String> teamList
        ) {
    
    for (String team : teamList) {
        logger.debug("team = " + team);
    }
    
    return "target";
}

3、表单对应模型

①表单

<form action="emp/save" method="post">
    姓名:<input type="text" name="empName"/><br/>
    年龄:<input type="text" name="empAge"/><br/>
    工资:<input type="text" name="empSalary"/><br/>
    <input type="submit" value="保存"/>
</form>

②实体类

public class Employee {
    
    private Integer empId;
    private String empName;
    private int empAge;
    private double empSalary;
    ……

③handler方法

@RequestMapping("/param/form/to/entity")
public String formToEntity(
    
        // SpringMVC 会自动调用实体类中的 setXxx() 注入请求参数
        Employee employee) {
    
    logger.debug(employee.toString());
    
    return "target";
}

④POST请求的字符乱码问题

到 web.xml 中配置 CharacterEncodingFilter 即可:

<!-- 配置过滤器解决 POST 请求的字符乱码问题 -->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    
    <!-- encoding参数指定要使用的字符集名称 -->
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    
    <!-- 请求强制编码 -->
    <init-param>
        <param-name>forceRequestEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
        
    <!-- 响应强制编码 -->
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

注1:在较低版本的 SpringMVC 中,forceRequestEncoding 属性、forceResponseEncoding 属性没有分开,它们是一个 forceEncoding 属性。这里需要注意一下。

注2:由于 CharacterEncodingFilter 是通过 request.setCharacterEncoding(encoding); 来设置请求字符集,所以在此操作前不能有任何的 request.getParameter() 操作。在设置字符集之前获取过请求参数,那么设置字符集的操作将无效。

4、表单对应实体类包含级联属性

①实体类

public class Student {
    
    private String stuName;
    private School school;
    private List<Subject> subjectList;
    private Subject[] subjectArray;
    private Set<Teacher> teacherSet;
    private Map<String, Double> scores;
    
    public Student() {
        //在各种常用数据类型中,只有Set类型需要提前初始化
        //并且要按照表单将要提交的对象数量进行初始化
        //Set类型使用非常不便,要尽可能避免使用Set
        teacherSet = new HashSet<>();
        teacherSet.add(new Teacher());
        teacherSet.add(new Teacher());
        teacherSet.add(new Teacher());
        teacherSet.add(new Teacher());
        teacherSet.add(new Teacher());
    }
    ……

其他实体类点这里

②表单

表单项中的 name 属性值必须严格按照级联对象的属性来设定:

<!-- 提交数据的表单 -->
<form th:action="@{/caseFour}" method="post">
    stuName:<input type="text" name="stuName" value="tom"/><br/>
    school.schoolName:<input type="text" name="school.schoolName" value="atguigu"/><br/>
    subjectList[0].subjectName:<input type="text" name="subjectList[0].subjectName" value="java"/><br/>
    subjectList[1].subjectName:<input type="text" name="subjectList[1].subjectName" value="php"/><br/>
    subjectList[2].subjectName:<input type="text" name="subjectList[2].subjectName" value="javascript"/><br/>
    subjectList[3].subjectName:<input type="text" name="subjectList[3].subjectName" value="css"/><br/>
    subjectList[4].subjectName:<input type="text" name="subjectList[4].subjectName" value="vue"/><br/>
    subjectArray[0].subjectName:<input type="text" name="subjectArray[0].subjectName" value="spring"/><br/>
    subjectArray[1].subjectName:<input type="text" name="subjectArray[1].subjectName" value="SpringMVC"/><br/>
    subjectArray[2].subjectName:<input type="text" name="subjectArray[2].subjectName" value="mybatis"/><br/>
    subjectArray[3].subjectName:<input type="text" name="subjectArray[3].subjectName" value="maven"/><br/>
    subjectArray[4].subjectName:<input type="text" name="subjectArray[4].subjectName" value="mysql"/><br/>
    tearcherSet[0].teacherName:<input type="text" name="tearcherSet[0].teacherName" value="t_one"/><br/>
    tearcherSet[1].teacherName:<input type="text" name="tearcherSet[1].teacherName" value="t_two"/><br/>
    tearcherSet[2].teacherName:<input type="text" name="tearcherSet[2].teacherName" value="t_three"/><br/>
    tearcherSet[3].teacherName:<input type="text" name="tearcherSet[3].teacherName" value="t_four"/><br/>
    tearcherSet[4].teacherName:<input type="text" name="tearcherSet[4].teacherName" value="t_five"/><br/>
    scores['Chinese']:input type="text" name="scores['Chinese']" value="100"/><br/>
    scores['English']:<input type="text" name="scores['English']" value="95" /><br/>
    scores['Mathematics']:<input type="text" name="scores['Mathematics']" value="88"/><br/>
    scores['Chemistry']:<input type="text" name="scores['Chemistry']" value="63"/><br/>
    scores['Biology']:<input type="text" name="scores['Biology']" value="44"/><br/>
    <input type="submit" value="保存"/>
</form>

③handler方法

@RequestMapping("/param/form/to/nested/entity")
public String formToNestedEntity(
    
        // SpringMVC 自己懂得注入级联属性,只要属性名和对应的getXxx()、setXxx()匹配即可
        Student student) {
    
    logger.debug(student.toString());
    
    return "target";
}

5、要发送的数据是 List

①额外封装一层

public class EmployeeParam {
    
    private List<Employee> employeeList;
    ……

②表单

直接发送 List&lt;Employee&gt;:<br/>
<form th:action="@{/param/list/emp}" method="post">
    1号员工姓名:<input type="text" name="employeeList[0].empName" /><br/>
    1号员工年龄:<input type="text" name="employeeList[0].empAge" /><br/>
    1号员工工资:<input type="text" name="employeeList[0].empSalary" /><br/>
    2号员工姓名:<input type="text" name="employeeList[1].empName" /><br/>
    2号员工年龄:<input type="text" name="employeeList[1].empAge" /><br/>
    2号员工工资:<input type="text" name="employeeList[1].empSalary" /><br/>
    <button type="submit">保存</button>
</form>

③handler方法

@RequestMapping("/param/list/emp")
public String saveEmpList(
        // SpringMVC 访问这里实体类的setEmployeeList()方法注入数据
        EmployeeParam employeeParam
) {
    
    List<Employee> employeeList = employeeParam.getEmployeeList();
    
    for (Employee employee : employeeList) {
        logger.debug(employee.toString());
    }
    
    return "target";
}

第五节 @RequestHeader注解

1、作用

通过这个注解获取请求消息头中的具体数据。

2、用法

@RequestMapping("/request/header")
public String getRequestHeader(
    
        // 使用 @RequestHeader 注解获取请求消息头信息
        // name 或 value 属性:指定请求消息头名称
        // defaultValue 属性:设置默认值
        @RequestHeader(name = "Accept", defaultValue = "missing") String accept
) {
    
    logger.debug("accept = " +accept);
    
    return "target";
}

第六节 @CookieValue注解

1、作用

获取当前请求中的 Cookie 数据。

2、用法

@RequestMapping("/request/cookie")
public String getCookie(
    
        // 使用 @CookieValue 注解获取指定名称的 Cookie 数据
        // name 或 value 属性:指定Cookie 名称
        // defaultValue 属性:设置默认值
        @CookieValue(value = "JSESSIONID", defaultValue = "missing") String cookieValue,
    
        // 形参位置声明 HttpSession 类型的参数即可获取 HttpSession 对象
        HttpSession session
) {
    
    logger.debug("cookieValue = " + cookieValue);
    
    return "target";
}

第七节 页面跳转控制

1、准备工作

  • 准备一个地址在前后缀范围之外的页面
  • 让这个页面能够成功访问

①创建范围之外的页面

<body>
    
    <h1>范围之外页面</h1>
    
</body>

②在 SpringMVC 配置文件加入配置

下面配置是访问静态资源所需配置,后面会专门说,现在先直接拿来用:

<mvc:default-servlet-handler/>    
<mvc:annotation-driven/>
    

2、使用指令

①转发指令

@RequestMapping("/test/forward/command")
public String forwardCommand() {
    
    // 需求:要转发前往的目标地址不在视图前缀指定的范围内,
    // 通过返回逻辑视图、拼接前缀后缀得到的物理视图无法达到目标地址
    
    // 转发到指定的地址:
    return "forward:/outter.html";
}

②重定向指令

@RequestMapping("/test/redirect/command")
public String redirectCommand() {
    
    // 重定向到指定的地址:
    // 这个地址由 SpringMVC 框架负责在前面附加 contextPath,所以我们不能加,我们加了就加多了
    // 框架增加 contextPath 后:/demo/outter.html
    // 我们多加一个:/demo/demo/outter.html
    return "redirect:/outter.html";
}

第二章 其他基础功能

第一节 获取原生Servlet API对象

1、原生 Servlet API

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • ServletContext

原生:最原始的、本真的,没有经过任何的加工、包装和处理。

API:直接翻译过来是应用程序接口的意思。对我们来说,提到 API 这个词的时候,通常指的是在某个特定的领域,已经封装好可以直接使用的一套技术体系。很多时候,特定领域的技术规范都是对外暴露一组接口作为这个领域的技术标准,然后又在这个标准下有具体实现。

2、可以直接拿到的对象

①在 IDEA 中创建代码模板

使用快捷键调出代码模板能够极大的提升开发效率

②测试获取三个可以直接得到的对象

[1]创建超链接


<a th:href="@{/original/api/direct}">可以直接得到的三个</a><br/>
[2]声明 handler 方法
@RequestMapping("/original/api/direct")
public String getOriginalAPIDirect(
        
        // 有需要使用的 Servlet API 直接在形参位置声明即可。
        // 需要使用就写上,不用就不写,开发体验很好,这里给 SpringMVC 点赞
        HttpServletRequest request,
        HttpServletResponse response,
        HttpSession session
) {
    
    logger.debug(request.toString());
    logger.debug(response.toString());
    logger.debug(session.toString());
    
    return "target";
}

ServletContext对象没法通过形参声明的方式直接获取,如果非要在形参位置声明ServletContext类型的变量,那么会抛出下面的异常:

java.lang.IllegalStateException: No primary or single public constructor found for interface javax.servlet.ServletContext - and no default constructor found either

3、获取ServletContext

①方法一:通过HttpSession获取

@RequestMapping("/original/servlet/context/first/way")
public String originalServletContextFirstWay(HttpSession session) {
    
    // 获取ServletContext对象的方法一:通过HttpSession对象获取
    ServletContext servletContext = session.getServletContext();
    logger.debug(servletContext.toString());
    
    return "target";
}

②方法二:通过 IOC 容器注入

// 获取ServletContext对象的方法二:从 IOC 容器中直接注入
@Autowired
private ServletContext servletContext;
    
@RequestMapping("/original/servlet/context/second/way")
public String originalServletContextSecondWay() {
    
    logger.debug(this.servletContext.toString());
    
    return "target";
}

4、原生对象和 IOC 容器关系

第二节 属性域

属性域:请求域、会话域、应用域统称为属性域。

1、在整个应用中属性域的重要作用

2、请求域操作方式

①使用 Model 类型的形参

@RequestMapping("/attr/request/model")
public String testAttrRequestModel(
    
        // 在形参位置声明Model类型变量,用于存储模型数据
        Model model) {
    
    // 我们将数据存入模型,SpringMVC 会帮我们把模型数据存入请求域
    // 存入请求域这个动作也被称为暴露到请求域
    model.addAttribute("requestScopeMessageModel","i am very happy[model]");
    
    return "target";
}

②使用 ModelMap 类型的形参

@RequestMapping("/attr/request/model/map")
public String testAttrRequestModelMap(
    
        // 在形参位置声明ModelMap类型变量,用于存储模型数据
        ModelMap modelMap) {
    
    // 我们将数据存入模型,SpringMVC 会帮我们把模型数据存入请求域
    // 存入请求域这个动作也被称为暴露到请求域
    modelMap.addAttribute("requestScopeMessageModelMap","i am very happy[model map]");
    
    return "target";
}

③使用 Map 类型的形参


@RequestMapping("/attr/request/map")
public String testAttrRequestMap(
    
        // 在形参位置声明Map类型变量,用于存储模型数据
        Map<String, Object> map) {
    
    // 我们将数据存入模型,SpringMVC 会帮我们把模型数据存入请求域
    // 存入请求域这个动作也被称为暴露到请求域
    map.put("requestScopeMessageMap", "i am very happy[map]");
    
    return "target";
}

④使用原生 request 对象

@RequestMapping("/attr/request/original")
public String testAttrOriginalRequest(
    
        // 拿到原生对象,就可以调用原生方法执行各种操作
        HttpServletRequest request) {
    
    request.setAttribute("requestScopeMessageOriginal", "i am very happy[original]");
    
    return "target";
}

⑤使用 ModelAndView 对象


@RequestMapping("/attr/request/mav")
public ModelAndView testAttrByModelAndView() {
    
    // 1.创建ModelAndView对象
    ModelAndView modelAndView = new ModelAndView();
    
    // 2.存入模型数据
    modelAndView.addObject("requestScopeMessageMAV", "i am very happy[mav]");
    
    // 3.设置视图名称
    modelAndView.setViewName("target");
    
    return modelAndView;
}

3、模型的本质

①BindingAwareModelMap

SpringMVC 传入的 Model、ModelMap、Map类型的参数其实本质上都是 BindingAwareModelMap 类型的。

②它们之间的关系

4、框架底层将模型存入请求域

①最终找到的源码位置

所在类:org.thymeleaf.context.WebEngineContext

所在方法:setVariable()

②过程中值得关注的点

5、会话域

使用会话域最简单直接的办法就是使用原生的 HttpSession 对象

@RequestMapping("/attr/session")
public String attrSession(
        // 使用会话域最简单直接的办法就是使用原生的 HttpSession 对象
        HttpSession session) {
    
    session.setAttribute("sessionScopeMessage", "i am haha ...");
    
    return "target";
}

6、应用域

应用域同样是使用原生对象来操作:

@Autowired
private ServletContext servletContext;

@RequestMapping("/attr/application")
public String attrApplication() {
    
    servletContext.setAttribute("appScopeMsg", "i am hungry...");
    
    return "target";
}

第三节 静态资源访问

1、静态资源的概念

资源本身已经是可以直接拿到浏览器上使用的程度了,不需要在服务器端做任何运算、处理。典型的静态资源包括:

  • 纯HTML文件
  • 图片
  • CSS文件
  • JavaScript文件
  • ……

2、SpringMVC 环境下静态资源问题

①情况一:斜杠情况

[1]情景描述

DispatcherServlet 的 url-pattern 标签配置的是“/”。意味着整个 Web 应用范围内所有请求都由 SpringMVC 来处理。

[2]情景重现

在 Web 应用中加入图片资源:

部署目录下不会自动加入,需要我们手动重新构建才行:

[3]访问静态资源

[4]分析原因
  • DispatcherServlet 的 url-pattern 配置的是“/”
  • url-pattern 配置“/”表示整个 Web 应用范围内所有请求都由 SpringMVC 来处理
  • 对 SpringMVC 来说,必须有对应的 @RequestMapping 才能找到处理请求的方法
  • 现在 images/mi.jpg 请求没有对应的 @RequestMapping 所以返回 404
[5]解决办法
  • 在 SpringMVC 配置文件中增加配置:

    <!-- 加入这个配置,SpringMVC 就会在遇到没有 @RequestMapping 的请求时放它过去 -->
    <!-- 所谓放它过去就是让这个请求去找它原本要访问的资源 -->
    <mvc:default-servlet-handler/>
    
  • 再次测试访问图片:

  • 新的问题:其他原本正常的请求访问不了了
  • 进一步解决问题:再增加一个配置
<!-- 开启 SpringMVC 的注解驱动功能。这个配置也被称为 SpringMVC 的标配。 -->
<!-- 标配:因为 SpringMVC 环境下非常多的功能都要求必须打开注解驱动才能正常工作。 -->
<mvc:annotation-driven/>
[6] 访问静态资源小结

访问静态资源时,需要在Spring的配置文件增加以下两个配置:

<!-- 加入这个配置,SpringMVC 就会在遇到没有 @RequestMapping 的请求时放它过去 -->
<!-- 所谓放它过去就是让这个请求去找它原本要访问的资源 -->
<mvc:default-servlet-handler/>

<!-- 开启 SpringMVC 的注解驱动功能。这个配置也被称为 SpringMVC 的标配。 -->
<!-- 标配:因为 SpringMVC 环境下非常多的功能都要求必须打开注解驱动才能正常工作。 -->
<mvc:annotation-driven/>

注意:

1.建议将先写mvc:default-servlet-handler/标签,会导入唯一的mvc名称空间。

2.如果写标签时没有提示,很可能时名称空间没有写对。

[7]default-servlet-handler底层[了解]

所在类:org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler

关键方法:handleRequest()方法

大体机制:SpringMVC 首先查找是否存在和当前请求对应的 @RequestMapping;如果没有,则调用handleRequest()方法转发到目标资源。

@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

    Assert.state(this.servletContext != null, "No ServletContext set");
    RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName);
    if (rd == null) {
        throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" +
        this.defaultServletName + "'");
    }
    
    // 这里执行请求转发操作
    rd.forward(request, response);
}
[8]前面两个配置对请求访问的影响[了解]

我们在前面的操作中发现,使用了 mvc:default-servlet-handler 后必须使用 mvc:annotation-driven。那么这是为什么呢?关键原因是他们加载使用的 HandlerMapping 不同。

  • 没有 default-servlet-handler 和 annotation-driven 时,SpringMVC 加载的 HandlerMapping 是:

org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

DefaultAnnotationHandlerMapping 负责把所有 handler 类中的 handler 方法收集起来。

  • 加入 default-servlet-handler 时,SpringMVC 加载的 HandlerMapping 是:

org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping org.springframework.web.servlet.handler.SimpleUrlHandlerMapping

很明显,DefaultAnnotationHandlerMapping 没了,而 SimpleUrlHandlerMapping 只能映射静态资源。所以我们通过 @RequestMapping 映射的 handler 方法无效了。

  • 再加入 annotation-driven 时,SpringMVC 加载的 HandlerMapping 是:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping org.springframework.web.servlet.handler.SimpleUrlHandlerMapping

加入了 mvc:annotation-driven 后最关键的是增加了 RequestMappingHandlerMapping,从而可以映射我们的handler 方法。

结论:在配置不同的情况下,SpringMVC 底层加载的组件不同,特定功能需要特定组件的支持。当特定功能所需组件没有加入到 IOC 容器中的时候,对应的功能就无法使用了。

②情况二:扩展名情况

[1]修改 url-pattern

<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <!--<url-pattern>/</url-pattern>-->
    
    <!-- 以扩展名方式匹配 SpringMVC 要处理的请求 -->
    <!-- 此时要求请求扩展名必须是 html,SpringMVC 才会处理这个请求 -->
    <url-pattern>*.html</url-pattern>
</servlet-mapping>
[2]效果
  • 图片直接就可以访问了。因为请求扩展名不是 html,不会受到 SpringMVC 影响。
  • 其他请求:做下面两个操作才可以正常访问
    • 需要在超链接地址后面附加 html 扩展名
    • 在 @RequestMapping 注解指定的 URL 地址中也附加 html 扩展名

第四节 mvc:view-controller

1、需求情景

在一个 handler 方法中,仅仅只是完成 @RequestMapping 映射,将请求转发到目标视图,除此之外没有任何其他代码。此时可以使用 SpringMVC 配置文件中的配置代替这样的 handler 方法。

2、具体操作

①配置

在 SpringMVC 配置文件中使用 mvc:view-controller 配置:

<mvc:view-controller path="/index.html" view-name="portal"/>

同时,handler 类中就可以去掉被代替的方法。

②新的问题

加入 mvc:view-controller 配置后,其他正常 @RequestMapping 将失效。此时还是需要加入 mvc:annotation-driven 来解决。

第五节 表单标签

主要的目的是在页面上实现表单回显。最典型的情况是在修改数据时,把之前旧的数据重新显示出来供用户参考。

1、回显简单标签

一个标签回显一个值的情况。

①创建用于测试的实体类

public class Tiger {

    private Integer tigerId;
    private String tigerName;
    private Double tigerSalary;
    ……

②创建 handler 方法

@RequestMapping("/form/redisplay/simple")
public String simpleTagRedisplay(Model model) {
    
    // 1.准备好用来回显表单的实体类对象
    // 在实际功能中,这里的对象应该是从数据库查询得到
    Tiger tiger = new Tiger();
    tiger.setTigerId(5);
    tiger.setTigerName("tomCat");
    tiger.setTigerSalary(666.66);
        
    // 2.将实体类数据存入模型
    model.addAttribute("tiger", tiger);
    
    return "form-simple";
}

③页面表单回显

<h3>回显Tiger数据</h3>
    
<form th:action="@{/save/tiger}" method="post">
    
    <!-- th:value 和 th:field 属性都可以 -->
    老虎的id:<input type="text" name="tigerId" th:value="${tiger.tigerId}" /><br/>
    老虎的名字:<input type="text" name="tigerName" th:field="${tiger.tigerName}" /><br/>
    老虎的工资:<input type="text" name="tigerSalary" th:field="${tiger.tigerSalary}" /><br/>
    
    <button type="submit">保证</button>
</form>

2、回显带选择功能的标签

①总体思路

  • 显示标签本身,需要用到一个集合对象来存储标签本身所需要的数据
  • 对标签执行回显操作,需要用到另外的一个实体类

②创建实体类

[1]用来显示标签的实体类
public class Season {
    
    // 提交给服务器的值
    private String submitValue;
    
    // 给用户看的值
    private String showForUserValue;
    ……
[2]用来回显数据的实体类
public class Paige {
    
    private Integer paigeId;
    private String paigeName;
    private Season season;
    ……
③handler 方法
@RequestMapping("/form/redisplay/choose")
public String chooseTagRedisplay(Model model) {
    
    // 1.准备用来显示标签的数据
    List<Season> seasonList = new ArrayList<>();
    seasonList.add(new Season("spring", "春天"));
    seasonList.add(new Season("summer", "夏天"));
    seasonList.add(new Season("autumn", "秋天"));
    seasonList.add(new Season("winter", "冬天"));
    
    model.addAttribute("seasonList", seasonList);
    
    // 2.准备用来回显表单的实体类数据
    Paige paige = new Paige();
    paige.setPaigeId(6);
    paige.setPaigeName("pig");
    paige.setSeason(new Season("summer", "夏天"));
    
    model.addAttribute("paige", paige);
    
    return "form-choose";
}

③页面表单回显

[1]单选按钮
<!-- th:each属性:指定用来生成这一组标签的集合数据 -->
<!-- th:value属性:获取数据用来设置HTML标签的value属性,成为将来提交给服务器的值 -->
<!-- th:text属性:获取数据用来设置HTML标签旁边给用户看的名字 -->
<!-- th:checked属性:判断是否回显(把适合的标签设置为默认被选中) -->
<input type="radio" name="season.submitValue"
        th:each="season : ${seasonList}"
        th:value="${season.submitValue}"
        th:text="${season.showForUserValue}"
        th:checked="${season.submitValue == paige.season.submitValue}"
/>
[2]下拉列表
<select name="season.submitValue">
    <option th:each="season : ${seasonList}"
            th:value="${season.submitValue}"
            th:text="${season.showForUserValue}"
            th:selected="${season.submitValue == paige.season.submitValue}"/>
</select>
[3]多选框
  • 另外封装一个实体类(Season类需要重写HashCode()与equals()方法,否则在页面回显时,无法成功判断两个season是否相等,因为它们比较的是地址值)
public class John {
    
    private List<Season> seasonList;
    
    public List<Season> getSeasonList() {
        return seasonList;
    }
    
    public void setSeasonList(List<Season> seasonList) {
        this.seasonList = seasonList;
    }
}
  • handler方法
@RequestMapping("/form/redisplay/choose/multi")
public String chooseMulti(Model model) {
    
    // 1.准备用来显示标签的数据
    List<Season> seasonList = new ArrayList<>();
    seasonList.add(new Season("spring", "春天"));
    seasonList.add(new Season("summer", "夏天"));
    seasonList.add(new Season("autumn", "秋天"));
    seasonList.add(new Season("winter", "冬天"));
    
    model.addAttribute("seasonList", seasonList);
    
    // 2.准备用来回显表单的实体类数据
    John john = new John();
    List<Season> seasonListForRedisplay = new ArrayList<>();
    seasonListForRedisplay.add(new Season("summer", "夏天"));
    seasonListForRedisplay.add(new Season("winter", "冬天"));
    
    model.addAttribute("seasonListForRedisplay", seasonListForRedisplay);
    
    return "form-multi";
}
  • 页面标签
<!--
    seasonListForRedisplay.contains(season)
    用包含回显数据的集合调用contains()方法判断是否应该被选中;
    传入contains()方法的是生成具体每一个标签时遍历得到的对象
 -->
<input type="checkbox" name="xxx"
       th:each="season : ${seasonList}"
       th:value="${season.submitValue}"
       th:text="${season.showForUserValue}"
       th:checked="${seasonListForRedisplay.contains(season)}"
/>

第六节 案例

1、准备工作

①创建实体类

public class Movie {
    
    private String movieId;
    private String movieName;
    private Double moviePrice;
    ……

②创建Service

[1]接口
public interface MovieService {
    
    List<Movie> getAll();
    
    Movie getMovieById(String movieId);
    
    void saveMovie(Movie movie);
    
    void updateMovie(Movie movie);
    
    void removeMovieById(String movieId);
    
}
[2]接口的实现类
@Service
public class MovieServiceImpl implements MovieService {
    
    private static Map<String ,Movie> movieMap;
    
    static {
    
        movieMap = new HashMap<>();
    
        String movieId = null;
        Movie movie = null;
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "肖申克救赎", 10.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "泰坦尼克号", 20.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "审死官", 30.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "大话西游之大圣娶亲", 40.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "大话西游之仙履奇缘", 50.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "功夫", 60.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "大内密探凌凌漆", 70.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "食神", 80.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "西游降魔篇", 90.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "西游伏妖篇", 11.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "三傻大闹宝莱坞", 12.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "唐人街探案", 13.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "一个人的武林", 14.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "罗马假日", 15.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "花季雨季", 16.0);
        movieMap.put(movieId, movie);
    
        movieId = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        movie = new Movie(movieId, "夏洛特烦恼", 17.0);
        movieMap.put(movieId, movie);
    }
    
    @Override
    public List<Movie> getAll() {
        return new ArrayList<>(movieMap.values());
    }
    
    @Override
    public Movie getMovieById(String movieId) {
        return movieMap.get(movieId);
    }
    
    @Override
    public void saveMovie(Movie movie) {
        String movieId = UUID.randomUUID().toString().replace("-", "");
    
        movie.setMovieId(movieId);
    
        movieMap.put(movieId, movie);
    }
    
    @Override
    public void updateMovie(Movie movie) {
    
        String movieId = movie.getMovieId();
    
        movieMap.put(movieId, movie);
    
    }
    
    @Override
    public void removeMovieById(String movieId) {
        movieMap.remove(movieId);
    }
}

③测试Service

@SpringJUnitConfig(locations = {"classpath:spring-mvc.xml"})
public class MovieTest {
        
    @Autowired
    private MovieService movieService;
        
    @Test
    public void testServiceGetAll() {
        List<Movie> list = movieService.getAll();
        for (Movie movie : list) {
            System.out.println("movie = " + movie);
        }
    }
    
    @Test
    public void testServiceGetById() {
        List<Movie> movieList = movieService.getAll();
        for (Movie movie : movieList) {
            String movieId = movie.getMovieId();
            Movie movieById = movieService.getMovieById(movieId);
            System.out.println("movieById = " + movieById);
        }
    }
    
    @Test
    public void testGetOne() {
        Movie movie = movieService.getMovieById("178E6B0B0DA14DC59141E06FFA620673");
        System.out.println("movie = " + movie);
    }
    
    @Test
    public void testServiceRemoveById() {
        List<Movie> movieList = movieService.getAll();
        for (Movie movie : movieList) {
            String movieId = movie.getMovieId();
            movieService.removeMovieById(movieId);
        }
    
        movieList = movieService.getAll();
        for (Movie movie : movieList) {
            System.out.println("movie = " + movie);
        }
    }
    
    @Test
    public void testServiceSave() {
        movieService.saveMovie(new Movie(null, "aa", 111.11));
    
        List<Movie> all = movieService.getAll();
        for (Movie movie : all) {
            System.out.println("movie = " + movie);
        }
    }
    
    @Test
    public void testServiceUpdate() {
        List<Movie> all = movieService.getAll();
        for (Movie movie : all) {
            String movieId = movie.getMovieId();
            String movieName = movie.getMovieName() + "~";
    
            Movie movieNew = new Movie(movieId, movieName, movie.getMoviePrice());
            movieService.updateMovie(movieNew);
        }
    
        List<Movie> movieList = movieService.getAll();
        for (Movie movie : movieList) {
            System.out.println("movie = " + movie);
        }
    }
    
}

2、搭建环境

①引入依赖

<dependencies>
    <!-- SpringMVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.1</version>
    </dependency>
    
    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    
    <!-- ServletAPI -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    
    <!-- Spring5和Thymeleaf整合包 -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.12.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.7.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.1</version>
    </dependency>
</dependencies>

②加入配置文件

[1]web.xml
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
    
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceRequestEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
[2]日志配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 指定日志输出的位置 -->
    <appender name="STDOUT"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 日志输出的格式 -->
            <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
            <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
        </encoder>
    </appender>
    
    <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
    <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
    <root level="INFO">
        <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
        <appender-ref ref="STDOUT" />
    </root>
    
    <!-- 根据特殊需求指定局部日志级别 -->
    <logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG" />
    
</configuration>
[3]SpringMVC 配置文件
<!-- 自动扫描的包 -->
<context:component-scan base-package="com.atguigu.demo"/>
    
<!-- 视图解析器 -->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    <property name="order" value="1"/>
    <property name="characterEncoding" value="UTF-8"/>
    <property name="templateEngine">
        <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
            <property name="templateResolver">
                <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
                    <property name="prefix" value="/WEB-INF/templates/"/>
                    <property name="suffix" value=".html"/>
                    <property name="characterEncoding" value="UTF-8"/>
                    <property name="templateMode" value="HTML5"/>
                </bean>
            </property>
        </bean>
    </property>
</bean>
    
<!-- SpringMVC 标配:注解驱动 -->
<mvc:annotation-driven/>
    
<!-- 对于没有 @RequestMapping 的请求直接放行 -->
<mvc:default-servlet-handler/>

3、功能清单

4、具体功能:显示首页

①流程图

②具体实现

[1]配置 view-controller
  <!-- 使用 mvc:view-controller 功能就不必编写 handler 方法,直接跳转 -->
    <mvc:view-controller path="/" view-name="portal"/>
    <mvc:view-controller path="/index.html" view-name="portal"/>
[2]页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body style="text-align: center">
    
    <a th:href="@{/show/list}">显示电影列表</a>
    
</body>
</html>

5、具体功能:显示全部数据

①流程图

②handler 方法

@Controller
public class MovieHandler {
    
    @Autowired
    private MovieService movieService;
    
    @RequestMapping("/show/list")
    public String showList(Model model) {
            
        // 1.调用 Service 方法查询数据
        List<Movie> movieList = movieService.getAll();
            
        // 2.将数据存入模型
        model.addAttribute("movieList", movieList);
    
        // 3.返回逻辑视图名称
        return "movie-list";
    }
        
}

③页面

[1]样式部分
<style type="text/css">
    table {
        border-collapse: collapse;
        margin: 0px auto 0px auto;
    }
    table th,td {
        border: 1px solid black;
        text-align: center;
    }
</style>
[2]数据展示

<table>
    <tr>
        <th>电影ID</th>
        <th>电影名称</th>
        <th>电影票价格</th>
        <th>删除</th>
        <th>更新</th>
    </tr>
    <tbody th:if="${#lists.isEmpty(movieList)}">
        <tr>
            <td colspan="5">抱歉!没有查询到数据!</td>
        </tr>
    </tbody>
    <tbody th:if="${not #lists.isEmpty(movieList)}">
        <tr th:each="movie : ${movieList}">
            <td th:text="${movie.movieId}">这里显示电影ID</td>
            <td th:text="${movie.movieName}">这里显示电影名称</td>
            <td th:text="${movie.moviePrice}">这里显示电影价格</td>
            <td>删除</td>
            <td>更新</td>
        </tr>
        <tr>
            <td colspan="5">添加</td>
        </tr>
    </tbody>
</table>

6、具体功能:删除

①流程图

②超链接


<a th:href="@{/remove/movie(movieId=${movie.movieId})}">删除</a>

③handler 方法

@RequestMapping("/remove/movie")
public String removeMovie(
        // 获取请求参数,从形参这里传进来
        @RequestParam("movieId") String movieId,
    
        Model model
) {
    
    // 1.根据 movieId 执行删除
    movieService.removeMovieById(movieId);
    
    // 方案一:直接使用列表页面的逻辑视图名称
    // 结果:无法显示数据
    // 原因:没有将列表数据存入模型,所以页面上无法从请求域获取数据来展示
    // return "movie-list";
    
    // 方案二:我们自己查数据,存入模型,然后返回列表页面的逻辑视图
    // 结果:能够显示数据
    // 建议:不要使用。因为代码重复了。
    // List<Movie> movieList = movieService.getAll();
    // model.addAttribute("movieList", movieList);
    // return "movie-list";
    
    // 方案三:调用那个返回列表页面的方法
    // 结果:能够显示数据
    // 建议:不要使用。破坏了程序的结构,同时浏览器地址栏显示的还是删除操作的地址,刷新浏览器会重复执行删除操作。
    // return showList(model);
    
    // 方案四:转发到显示列表页面功能的地址
    // 结果:能够显示数据
    // 建议:不要使用。浏览器地址栏显示的还是删除操作的地址,刷新浏览器会重复执行删除操作。
    // return "forward:/show/list";
    
    // 方案五:重定向到显示列表页面功能的地址
    // 结果:能够显示数据
    // 建议:建议使用。
    return "redirect:/show/list";
}

7、具体功能:跳转到添加的表单页面

①流程图

②超链接

在表格中判断列表数据是否存在有两个分支,这两个分支都应该有前往添加页面的超链接:

<tbody th:if="${#lists.isEmpty(movieList)}">
    <tr>
        <td colspan="5">抱歉!没有查询到数据!</td>
    </tr>
    <tr>
        <td colspan="5">
            <a th:href="@{/add/movie/page}">跳转到添加数据的表单页面</a>
        </td>
    </tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(movieList)}">
    <tr th:each="movie : ${movieList}">
        <td th:text="${movie.movieId}">这里显示电影ID</td>
        <td th:text="${movie.movieName}">这里显示电影名称</td>
        <td th:text="${movie.moviePrice}">这里显示电影价格</td>
        <td>
            <a th:href="@{/remove/movie(movieId=${movie.movieId})}">删除</a>
        </td>
        <td>更新</td>
    </tr>
    <tr>
        <td colspan="5">
            <a th:href="@{/add/movie/page}">跳转到添加数据的表单页面</a>
        </td>
    </tr>
</tbody>

③配置view-controller

<mvc:view-controller path="/add/movie/page" view-name="movie-add"/>

④准备表单页面


<form th:action="@{/save/movie}" method="post">
    
    电影名称:<input type="text" name="movieName" /><br/>
    电影票价格:<input type="text" name="moviePrice" /><br/>
    
    <button type="submit">保存</button>
    
</form>

8、具体功能:执行保存

①流程图

②handler 方法


@RequestMapping("/save/movie")
public String saveMovie(
        
        // 表单提交的请求参数会通过实体类的setXx()方法注入
        Movie movie) {
    
    movieService.saveMovie(movie);
        
    return "redirect:/show/list";
}

9、具体功能:前往更新的表单页面

①流程图

②超链接


<a th:href="@{/edit/movie/page(movieId=${movie.movieId})}">更新</a>

③handler 方法

@RequestMapping("/edit/movie/page")
public String editMoviePage(
    
        // 获取请求参数
        @RequestParam("movieId") String movieId,
        
        Model model
) {
    
    // 1.根据id查询movie对象
    Movie movie = movieService.getMovieById(movieId);
        
    // 2.将movie对象存入模型
    model.addAttribute("movie", movie);
    
    // 3.返回逻辑视图
    return "movie-edit";
}

④准备表单页面并回显

<form th:action="@{/update/movie}" method="post">
    
    <input type="hidden" name="movieId" th:value="${movie.movieId}" />
    
    电影名称:<input type="text" name="movieName" th:value="${movie.movieName}" /><br/>
    电影票价格:<input type="text" name="moviePrice" th:value="${movie.moviePrice}" /><br/>
    
    <button type="submit">更新</button>
    
</form>

10、具体功能:执行更新

①流程图

②handler 方法

@RequestMapping("/update/movie")
public String updateMovie(Movie movie) {
    
    movieService.updateMovie(movie);
    
    return "redirect:/show/list";
}

第三章 RESTFul风格交互方式

第一节 RESTFul概述

1、REST 概念

REST:Representational State Transfer,表现层资源状态转移。

  • 定位:互联网软件架构风格
  • 倡导者:Roy Thomas Fielding
  • 文献:Roy Thomas Fielding的博士论文

2、挑战与应对

①一项技术标准规范一统天下

端到端之间的交互协议、技术实现方案有多少种?

  • SOAP
  • WSDL
  • XML-RPC
  • socket
  • SMTP
  • POP3
  • ……

面对各种不同领域的各种不同协议、技术标准、解决方案,我们程序员是真的头秃。增加学习成本不说,还复杂难以调试,再加上参考资料稀缺、晦涩难懂。于是 HTTP 协议来了,它来拯救我们了,它有什么优势呢?

  • 在最广大的范围内,HTTP 协议拥有最广泛的共识。
  • 报文结构清晰,简单明了。
  • 无状态,消除了请求与请求之间的耦合关系
  • 无类型,却能够展示丰富的数据类型
  • 解耦合,双方一旦确定使用 HTTP 协议交互,那就不必关系对方采用什么技术、基于什么平台

②功能还是资源?

传统的软件系统仅在本地工作,但随着项目规模的扩大和复杂化,不但整个项目会拓展为分布式架构,很多功能也会通过网络访问第三方接口来实现。在通过网络访问一个功能的情况下,我们不能轻易假设网络状况文档可靠。所以当一个请求发出后没有接收到对方的回应,那我们该如何判定本次操作成功与否?

下面以保存操作为例来说明一下针对功能和针对资源进行操作的区别:

  • 针对功能设计系统

    保存一个 Employee 对象,没有接收到返回结果,用户角度会判定操作失败,再保存一次。但是其实在服务器端保存操作已经成功了,只是返回结果在网络传输过程中丢失了。而第二次的补救行为则保存了重复、冗余但 id 不同的数据,这对整个系统数据来说是一种破坏。

  • 针对资源设计系统

    针对 id 为 3278 的资源执行操作,服务器端会判断指定 id 的资源是否存在。如果不存在,则执行保存操作新建数据;如果存在,则执行更新操作。所以这个操作不论执行几次,对系统的影响都是一样的。在网络状态不可靠的情况下可以多次重试,不会破坏系统数据。

幂等性:如果一个操作执行一次和执行 N 次对系统的影响相同,那么我们就说这个操作满足幂等性。而幂等性正是 REST 规范所倡导的。

③无论内外一视同仁?

确立 HTTP 协议作为项目架构设计规范和统一标准后,无论系统内部的远程方法还是系统外部的第三方接口,对我们当前所在模块来说都是网络上的资源,完全可以使用相同(或类似)的方式来访问和调用。这非常有利于我们建构大型、超大型互联网项目体系。

3、REST 规范的内涵

①资源

URL:Uniform Resource Locator 统一资源定位器。意思是网络上的任何资源都可以通过 URL 来定位。但是在实际开发中,我们往往是使用 URL 来对应一个具体的功能,而不是资源本身。REST 规范则倡导使用 URL 对应网络上的各种资源,任何一个资源都可以通过一个 URL 访问到,为实现操作幂等性奠定基础。

②状态转移

REST 倡导针对资源本身操作,所以对资源的操作如果满足幂等性,那么操作只会导致资源本身的状态发生变化而不会破坏整个系统数据。

4、REST 规范具体要求

①四种请求方式对应四种常见操作

REST 风格主张在项目设计、开发过程中,具体的操作符合 HTTP 协议定义的请求方式的语义

操作 请求方式
查询操作 GET
保存操作 POST
删除操作 DELETE
更新操作 PUT

另有一种说法:

  • POST 操作针对功能执行,没有锁定资源 id,是非幂等性操作。
  • PUT 操作锁定资源 id,即使操作失败仍然可以针对原 id 重新执行,对整个系统来说满足幂等性。
    • id 对应的资源不存在:执行保存操作
    • id 对应的资源存在:执行更新操作

这个说法更加深刻

②URL 地址风格

REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。还有一点是不要使用请求扩展名。

传统 URL 地址 REST 风格地址
/remove/emp?id=5 /emp/5

5、REST 风格的好处

①含蓄,安全

使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。

②风格统一

URL 地址整体格式统一,从前到后始终都使用斜杠划分各个内容部分,用简单一致的格式表达语义。

③无状态

在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。

④严谨,规范

严格按照 HTTP1.1 协议中定义的请求方式本身的语义进行操作。

⑤简洁,优雅

过去做增删改查操作需要设计4个不同的URL,现在一个就够了。

操作 传统风格 REST 风格
保存 /CRUD/saveEmp URL 地址:/CRUD/emp
请求方式:POST
删除 /CRUD/removeEmp?empId=2 URL 地址:/CRUD/emp/2
请求方式:DELETE
更新 /CRUD/updateEmp URL 地址:/CRUD/emp
请求方式:PUT
查询(表单回显) /CRUD/editEmp?empId=2 URL 地址:/CRUD/emp/2
请求方式:GET

⑥丰富的语义

通过 URL 地址就可以知道资源之间的关系。它能够把一句话中的很多单词用斜杠连起来,反过来说就是可以在 URL 地址中用一句话来充分表达语义。

http://localhost:8080/shop

http://localhost:8080/shop/product

http://localhost:8080/shop/product/cellPhone

http://localhost:8080/shop/product/cellPhone/iPhone

第二节 四种请求方式映射

1、HiddenHttpMethodFilter 与装饰模式

①简介

在 HTML 中,GET 和 POST 请求可以天然实现,但是 DELETE 和 PUT 请求无法直接做到。SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求

②HiddenHttpMethodFilter 源码要点

[1]默认请求参数名常量
public static final String DEFAULT_METHOD_PARAM = "_method";

在 HiddenHttpMethodFilter 中,声明了一个常量:DEFAULT_METHOD_PARAM,常量值是"_method"。这个值是我们转换请求参数需要用到的key的默认值。

[2]和常量配套的成员变量
private String methodParam = DEFAULT_METHOD_PARAM;

之所以会提供这个成员变量和配套的 setXxx() 方法,是允许我们在配置 Filter 时,通过初始化参数来修改这个变量。如果不修改,默认就是前面常量定义的值。如果修改,那么就使用这个值作为我们转换请求参数的key,一般我们不做修改。

[3]以常量值为名称获取请求参数

③原始请求对象的包装

[1]困难
  • 包装对象必须和原始对象是同一个类型 ==》适应各个Servlet容器
  • 保证同一个类型不能通过子类继承父类实现
    • 子类对象:希望改变行为、属性的对象
    • 父类对象:随着 Servlet 容器的不同,各个容器对 HttpServletRequest 接口给出的实现不同。如果继承了 A 容器给出的实现类,那么将来就不能再迁移到 B 容器。
  • 只能让包装对象和被包装对象实现相同接口
    • 虽然使用动态代理技术大致上应该能实现,但是一旦应用代理就必须为被包装的对象的每一个方法都进行代理,操作过于繁琐。
  • 如果我们自己创建一个类实现 HttpServletRequest 接口
    • 困难1:我们其实并不知道具体该怎么做
    • 困难2:抽象方法实在太多
[2] HttpServletRequestWrapper 类

HttpServletRequestWrapper 类能够非常好的帮助我们对原始 request 对象进行包装。它为什么能帮我们解决上面的困难呢?

  • HttpServletRequestWrapper 类替我们实现了HttpServletRequest 接口
  • 为了让包装得到的新对象在任何 Servlet 容器平台上都能够正常工作,HttpServletRequestWrapper 类此处的设计非常巧妙:它借助原始的 request 对象本身来实现所有的具体功能
  • 在我们想通过包装的方式来修改原始对象的行为或属性时,只需要在 HttpServletRequestWrapper 类的子类中重写对应的方法即可
[3]HttpMethodRequestWrapper 类

HttpMethodRequestWrapper 类就是 HiddenHttpMethodFilter 的一个内部类,在 HttpMethodRequestWrapper 类中有如下行为实现了对原始对象的包装:

  • 继承了官方包装类:HttpServletRequestWrapper
  • 在构造器中将原始 request 对象传给了父类构造器
  • 将我们指定的新请求方式传给了成员变量
  • 重写了父类(官方包装类)的 getMethod() 方法
  • 外界想知道新包装对象的请求方式时,会来调用被重写的 getMethod() 方法,从而得到我们指定的请求方式
/**
 * Simple {@link HttpServletRequest} wrapper that returns the supplied method for
 * {@link HttpServletRequest#getMethod()}.
 */
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
	
	private final String method;
	
	public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
        // 在构造器中将原始 request 对象传给了父类构造器
		super(request);
        
        // 将我们指定的新请求方式传给了成员变量
		this.method = method;
	}
	
	@Override
	public String getMethod() {
		return this.method;
	}
}

④装饰者模式

装饰者模式也是二十三种设计模式之一,属于结构型模式,主要特点就是借助原始对象实现和原始对象一样的接口,同时通过重写父类方法修改被包装对象的行为。

2、PUT 请求

以下操作需要在已有的 SpringMVC 环境基础上执行:

①web.xml

<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>

②表单

  • 要点1:原请求方式必须是 post
  • 要点2:新的请求方式名称通过请求参数发送
  • 要点3:请求参数名称必须是_method
  • 要点4:请求参数的值就是要改成的请求方式
<!-- 原请求方式必须是 post -->
<form th:action="@{/emp}" method="post">
    <!-- 通过表单隐藏域携带一个请求参数 -->
    <!-- 请求参数名:_method -->
    <!-- 请求参数值:put -->
    <input type="hidden" name="_method" value="put" />

    <button type="submit">更新</button>
</form>

③handler 方法

// 映射请求地址:URL + 请求方式
@RequestMapping(value = "/emp", method = RequestMethod.PUT)
public String updateEmp() {
    
    logger.debug("现在执行的是 updateEmp() 方法");
    
    return "target";
}

④请求方式 Filter 对字符集 Filter 的影响

[1]结论

当 web.xml 中两个 Filter 并存,一定要让 CharacterEncodingFilter 先执行

[2]原因
  • 在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字符集的
  • request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作
  • 而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作:
String paramValue = request.getParameter(this.methodParam);

3、DELETE 请求

前面为了转换 PUT 请求所配置的环境仍然要继续使用。

①设定场景

通常删除超链接会出现在列表页面:

<h3>将XXX请求转换为DELETE请求</h3>
<table id="dataTable">
    <tr>
        <th>姓名</th>
        <th>年龄</th>
        <th>删除</th>
    </tr>
    <tr>
        <td>张三</td>
        <td>40</td>
        <td>
            <a th:href="@{/emp}" @click="doConvert">删除</a>
        </td>
    </tr>
    <tr>
        <td>李四</td>
        <td>30</td>
        <td>
            <a th:href="@{/emp}" @click="doConvert">删除</a>
        </td>
    </tr>
</table>

②创建负责转换的表单

<!-- 创建一个通用表单,在删除超链接的单击响应函数中通过这个表单把GET请求转换为POST,进而再转DELETE -->
<form method="post" id="convertForm">
    <input type="hidden" name="_method" value="delete" />
</form>

③给删除超链接绑定单击响应函数

[1]引入Vue

<script type="text/javascript" th:src="@{/script/vue.js}"></script>
[2]绑定单击响应函数
new Vue({
    "el":"#dataTable",
    "methods":{
        "doConvert":function (event) {

            // 1.先根据id获取到表单的元素对象
            var formEle = document.getElementById("convertForm");

            // 2.将表单的action属性设置为当前超链接的href属性
            // ①之所以要这样赋值就是为了将来提交表单的时候访问的还是原超链接要访问的地址
            // ②event对象:当前事件对象
            // ③event.target对象:发生事件的元素对象,现在是我们点击的超链接
            // ④event.target.href属性:超链接的href属性值
            // ⑤formEle.action属性:表单的action属性的值
            formEle.action = event.target.href;

            // 3.提交表单
            formEle.submit();

            // 阻止超链接的默认行为(跳转页面)
            event.preventDefault();
        }
    }
});

④handler 方法

@RequestMapping(value = "/emp", method = RequestMethod.DELETE)
public String removeEmp() {
    
    logger.debug("现在执行的是 removeEmp() 方法");
    
    return "target";
}

第三节 @PathVariable注解

1、REST 风格路径参数

请看下面链接:

/emp/20

/shop/product/iphone

如果我们想要获取链接地址中的某个部分的值,就可以使用 @PathVariable 注解,例如上面地址中的20、iphone部分。

2、操作

①传一个值

[1]超链接
<a th:href="@{/emp/20}">传一个值</a><br/>
[2]handler 方法
// 实际访问地址:/emp/20
// 映射地址:/emp/{empId}是把变量部分用大括号标记出来,写入变量名
@RequestMapping("/emp/{empId}")
public String getEmpById(@PathVariable("empId") Integer empId) {
    
    logger.debug("empId = " + empId);
    
    return "target";
}

②传多个值

[1]超链接
<a th:href="@{/emp/tom/18/50}">传多个值</a><br/>
[2]handler 方法
// 实际地址:/emp/tom/18/50
@RequestMapping("/emp/{empName}/{empAge}/{empSalary}")
public String queryEmp(
        @PathVariable("empName") String empName,
        @PathVariable("empAge") Integer empAge,
        @PathVariable("empSalary") Double empSalary
) {
    
    logger.debug("empName = " + empName);
    logger.debug("empAge = " + empAge);
    logger.debug("empSalary = " + empSalary);
    
    return "target";
}

第四节 案例

1、准备工作

和传统 CRUD 一样。

  • 搭建环境
  • 准备实体类
  • 准备Service接口和实现类

2、功能清单

功能 URL 地址 请求方式
访问首页√ / GET
查询全部数据√ /movie GET
删除√ /movie/2 DELETE
跳转到添加数据的表单√ /movie/add/page GET
执行保存√ /movie POST
跳转到更新数据的表单√ /movie/2 GET
执行更新√ /movie PUT

3、具体功能:访问首页

①流程图

②配置view-controller

<mvc:view-controller path="/" view-name="portal"/>

③创建页面


<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body style="text-align: center">
    <h1>首页</h1>
    
    <a th:href="@{/movie}">显示电影列表</a>
    
</body>
</html>

4、具体功能:查询全部数据

①流程图

②handler 方法

@RequestMapping(value = "/movie", method = RequestMethod.GET)
public String getMovieList(Model model) {
        
    // 1.调用 Service 方法查询数据
    List<Movie> movieList = movieService.getAll();
        
    // 2.将数据存入模型
    model.addAttribute("movieList", movieList);
    
    // 3.返回逻辑视图
    return "movie-list";
}

③页面展示数据


<table>
    <tr>
        <th>电影ID</th>
        <th>电影名称</th>
        <th>电影票价格</th>
        <th>删除</th>
        <th>更新</th>
    </tr>
    <tbody th:if="${#lists.isEmpty(movieList)}">
    <tr>
        <td colspan="5">抱歉!没有查询到数据!</td>
    </tr>
    <tr>
        <td colspan="5">
            <a th:href="@{/movie/add/page}">跳转到添加数据的表单页面</a>
        </td>
    </tr>
    </tbody>
    <tbody th:if="${not #lists.isEmpty(movieList)}">
    <tr th:each="movie : ${movieList}">
        <td th:text="${movie.movieId}">这里显示电影ID</td>
        <td th:text="${movie.movieName}">这里显示电影名称</td>
        <td th:text="${movie.moviePrice}">这里显示电影价格</td>
        <td>
            <a th:href="@{/movie/}+${movie.movieId}">删除</a>
        </td>
        <td>
            <a th:href="@{/movie/}+${movie.movieId}">更新</a>
        </td>
    </tr>
    <tr>
        <td colspan="5">
            <a th:href="@{/movie/add/page}">跳转到添加数据的表单页面</a>
        </td>
    </tr>
    </tbody>
</table>

5、具体功能:删除

重点在于将 GET 请求转换为 DELETE。基本思路是:通过一个通用表单,使用 Vue 代码先把 GET 请求转换为 POST,然后再借助 hiddenHttpMethodFilter 在服务器端把 POST 请求转为 DELETE。

①创建通用表单

<!-- 组件名称:通用表单 -->
<!-- 组件作用:把删除超链接的 GET 请求转换为 POST,并携带 _method 请求参数 -->
<form id="convertForm" method="post">
    
    <!-- 请求参数作用:告诉服务器端 hiddenHttpMethodFilter 要转换的目标请求方式 -->
    <!-- 请求参数名:_method,这是 hiddenHttpMethodFilter 中规定的 -->
    <!-- 请求参数值:delete,这是因为我们希望服务器端将请求方式最终转换为 delete -->
    <input type="hidden" name="_method" value="delete"/>
</form>

②删除超链接绑定单击响应函数

在这个单击响应函数中完成转换

[1]引入 Vue

<script type="text/javascript" th:src="@{/script/vue.js}"></script>
[2]绑定单击响应函数

删除超链接:

<a @click="doConvert" th:href="@{/movie/}+${movie.movieId}">删除</a>

Vue代码:

new Vue({
    "el":"#dataTable",
    "methods":{
        "doConvert":function () {
    
            // 1.根据 id 值查询到通用表单的元素对象
            var convertFormEle = document.getElementById("convertForm");
    
            // 2.将当前超链接的 href 属性值赋值给通用表单的元素对象的 action 属性
            // ①event:当前事件对象
            // ②event.target:当前点击的超链接对象
            // ③event.target.href:读取当前超链接对象的 href 属性
            // ④convertFormEle.action:给表单的 action 属性赋值
            convertFormEle.action = event.target.href;
    
            // 3.提交通用表单
            convertFormEle.submit();
    
            // 测试单击响应函数是否正确绑定
            // console.log("你点我了!!!");
    
            // 取消超链接控件的默认行为
            event.preventDefault();
        }
    }
});

③handler 方法

    // 实际访问地址举例:/movie/2345QEA
    // 想要把路径中代表 movieId 的部分给匹配出来
    // 所以在 @RequestMapping 注解中写 URL 地址的时候,把地址中动态的部分用大括号标记出来
    // 在大括号中声明变量名
    // 在 @PathVariable 注解中引用这个变量名
    // 使用 @PathVariable 注解修饰一个形参,SpringMVC 就会将匹配到的值从形参这里传入
    @RequestMapping(value = "/movie/{movieId}", method = RequestMethod.DELETE)
    public String removeMovie(
            @PathVariable("movieId") String movieId
    ) {
    
        // 1.执行删除
        movieService.removeMovieById(movieId);
    
        // 2.重定向到显示列表的页面
        return "redirect:/movie";
    }

6、具体功能:跳转到添加数据的表单

①流程图

②配置view-controller

<mvc:view-controller path="/movie/add/page" view-name="movie-add"/>

③创建页面

<form th:action="@{/movie}" method="post">
    
    电影名称:<input type="text" name="movieName" /><br/>
    电影票价格:<input type="text" name="moviePrice" /><br/>
    
    <button type="submit">保存</button>
    
</form>

7、具体功能:执行保存

①流程图

②handler 方法

 @RequestMapping(value = "/movie", method = RequestMethod.POST)
    public String saveMovie(Movie movie) {
  
        movieService.saveMovie(movie);
  
        return "redirect:/movie";
    }

8、具体功能:跳转到更新数据的表单

①流程图

②handler 方法

 @RequestMapping(value ="/movie/{movieId}", method = RequestMethod.GET)
    public String toEditPage(@PathVariable("movieId") String movieId, Model model) {
 
        Movie movie = movieService.getMovieById(movieId);
 
        model.addAttribute("movie", movie);
 
        return "movie-edit";
    }

③页面表单回显


<form th:action="@{/movie}" method="post">
 
    <input type="hidden" name="_method" value="PUT" />
    <input type="hidden" name="movieId" th:value="${movie.movieId}" />
 
    电影名称:<input type="text" name="movieName" th:value="${movie.movieName}" /><br/>
    电影票价格:<input type="text" name="moviePrice" th:value="${movie.moviePrice}" /><br/>

    <button type="submit">更新</button>
 
</form>

9、具体功能:执行更新

①流程图

②handler 方法


@RequestMapping(value = "/movie", method = RequestMethod.PUT)
public String updateMovie(Movie movie) {
 
    movieService.updateMovie(movie);
 
    return "redirect:/movie";
}

③表单页面

<form th:action="@{/movie}" method="post">
    <input type="hidden" name="_method" value="put"/>
    <input type="hidden" name="movieId" th:value="${movie.movieId}">
    电影名称:<input type="text" name="movieName" th:value="${movie.movieName}" /><br/>
    电影票价格:<input type="text" name="moviePrice" th:value="${movie.moviePrice}"/><br/>

    <button type="submit">保存</button>

</form>

第四章 特定功能单元

第一节 Ajax

1、实验一

请求:发送普通请求参数

handler 方法:使用 @RequestParam 注解接收请求参数

响应:服务器端返回普通文本

①引入 JavaScript 库

<base href="/demo/"/>

<script type="text/javascript" src="script/vue.js"></script>
<script type="text/javascript" src="script/axios.min.js"></script>

②前端代码

new Vue({
    "el":"#btnSpan",
    "methods":{
        "experimentOne":function () {
 
            // 请求:发送普通请求参数
            // 响应:普通文本
            axios({
                "method":"post",
                "url":"ajax/experiment/one",
                "params":{
                    "userName":"tom",
                    "password":"123456"
                }
            }).then(function (response) {
 
                // response接收服务器端返回的响应数据
                console.log(response);
            }).catch(function (response) {
                console.log(response);
            });
 
        }
    }
});

③后端代码

// 使用@ResponseBody注解告诉 SpringMVC:请你拿当前方法的返回值作为响应体,不要再找视图了
// 方法返回值类型有两种情况:
// 情况一:简单类型。SpringMVC 会直接作为响应体数据。
// 情况二:复杂类型。SpringMVC 会把它转换为 JSON 然后再作为响应体。此时需要 Jackson 的支持。
@ResponseBody
@RequestMapping("/ajax/experiment/one")
public String experimentOne(
 
        // Ajax请求发过来的请求参数,对服务器端来说没有区别,还是像以前一样正常接收
        @RequestParam("userName") String userName,
        @RequestParam("password") String password
) {
 
    logger.debug("userName = " + userName);
    logger.debug("password = " + password);
 
    // 服务器端给Ajax程序的响应数据通过handler方法的返回值提供
    return "message from handler as response[来自服务器的问候]";
}

2、实验二

请求:让整个请求体就是一个 JSON 数据

handler 方法:使用 @RequestBody 标记的实体类接收请求体数据

响应:返回普通文本

①前端代码

"experimentTwo":function () {
 
    axios({
        "method":"post",
        "url":"ajax/experiment/two",
 
        // data属性中指定一个 JSON 数据作为请求体
        "data":{
            "stuId": 55,
            "stuName": "tom",
            "subjectList": [
                {
                    "subjectName": "java",
                    "subjectScore": 50.55
                },
                {
                    "subjectName": "php",
                    "subjectScore": 30.26
                }
            ],
            "teacherMap": {
                "one": {
                    "teacherName":"tom",
                    "teacherAge":23
                },
                "two": {
                    "teacherName":"jerry",
                    "teacherAge":31
                },
            },
            "school": {
                "schoolId": 23,
                "schoolName": "atguigu"
            }
        }
    }).then(function (response) {
        console.log(response);
    }).catch(function (error) {
        console.log(error);
    });
 
}

②后端代码

[1]导入依赖
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.1</version>
</dependency>

如果忘记导入这个依赖,会看到下面的错误页面:

关于 SpringMVC 和 Jackson jar包之间的关系,需要注意:当 SpringMVC 需要解析 JSON 数据时就需要使用 Jackson 的支持。但是 SpringMVC 的 jar 包并没有依赖 Jackson,所以需要我们自己导入。

我们自己导入时需要注意:SpringMVC 和 Jackson 配合使用有版本的要求。二者中任何一个版本太高或太低都不行。

SpringMVC 解析 JSON 数据包括两个方向:

  • 从 JSON 字符串到 Java 实体类。
  • 从 Java 实体类到 JSON 字符串。

另外,如果导入了 Jackson 依赖,但是没有开启 mvc:annotation-driven 功能,那么仍然会返回上面的错误页面。

也就是说,我们可以这么总结 SpringMVC 想要解析 JSON 数据需要两方面支持:

  • mvc:annotation-driven
  • 引入 Jackson 依赖

还有一点,如果运行环境是 Tomcat7,那么在 Web 应用启动时会抛出下面异常:

org.apache.tomcat.util.bcel.classfile.ClassFormatException: Invalid byte tag in constant pool: 19

解决办法是使用 Tomcat8 或更高版本。

[2]handler 方法
@ResponseBody
@RequestMapping("/ajax/experiment/two")
public String experimentTwo(

        // 使用 @RequestBody 注解将请求体 JSON 数据解析出来,注入到对应的实体类中
        @RequestBody Student student
        ) {
    
    logger.debug(student.toString());
    
    return "message from handler as response[来自服务器的问候]";
}
[3]@RequestBody注解

适用 @RequestBody 注解的场景:请求体整个是一个 JSON 数据

Request Payload 翻译成中文大致可以说:请求负载。

3、实验三

请求:发送普通的请求参数,请求参数整体正好对应实体类

handler 方法:使用普通实体类接收请求参数

响应:返回普通文本数据

①前端代码

"experimentThree":function () {
 
    axios({
        "method":"post",
        "url":"ajax/experimentThree",
 
        // 普通的请求参数正好对应服务器端的一个实体类
        "params":{
            "soldierId":"666",
            "soldierName":"tigerMan"
        }
    }).then(function (response) {
        console.log(response);
    }).catch(function (error) {
        console.log(error);
    });
 
}

②后端代码

@ResponseBody
@RequestMapping("/ajax/experimentThree")
public String experimentThree(
 
        // 请求参数名正好对这个实体类的属性名,可以通过 setXxx() 方法直接注入
        Soldier soldier) {
 
    logger.debug(soldier.toString());
 
    return "message from handler as response[来自服务器的问候]";
}

③常见错误

如果前端程序使用 axios 的 params 属性发送请求参数,那么请求参数会附着在 URL 地址后面,此时当前请求没有请求体。同时服务器端 handler 方法使用了 @RequestBody 注解,会在日志中看到下面异常信息:

HttpMessageNotReadableException: Required request body is missing:

意思是需要请求体,但是没有找到。

4、实验四

请求:不需要发送任何数据

handler 方法:返回实体类

响应:服务器端返回由实体类生成的 JSON 数据

①前端代码

"experimentFour":function () {
 
    axios({
        "method":"post",
        "url":"ajax/experimentFour"
    }).then(function (response) {
        console.log(response);
 
        // 服务器返回的响应体数据被封装到了 data 属性中
        console.log(response.data);
        console.log("response.data.soldierId = " + response.data.soldierId);
        console.log("response.data.soldierName = " + response.data.soldierName);
    }).catch(function (error) {
        console.log(error);
    });
 
}

②后端代码

// 使用 @ResponseBody 注解标记的方法返回实体类数据时,
// SpringMVC 需要借助 Jackson 来将实体类转换为 JSON 数据
@ResponseBody
@RequestMapping("/ajax/experimentFour")
public Soldier experimentFour() {
 
    return new Soldier(333, "catMan");
}

浏览器控制台打印效果如下:

③常见错误

[1]415 错误

出现上面的错误页面,表示 SpringMVC 为了将 实体类对象转换为 JSON 数据,需要转换器。但是现在找不到转换器。它想要成功完成转换需要两方面支持:

  • mvc:annotation-driven
  • 引入 Jackson 依赖
[2]406 错误

问题出现的原因:

  • 请求地址扩展名:html
  • 服务器端打算返回的数据格式:JSON

上面二者不一致。SpringMVC 要坚守一个商人的良心,不能干『挂羊头,卖狗肉』的事儿。解决办法有三种思路:

  • 第一种方法:不使用请求扩展名
  • 第二种方法:使用和实际返回的数据格式一致的扩展名
<servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>*.html</url-pattern>
        <url-pattern>*.json</url-pattern>
    </servlet-mapping>
  • 第三种方法:使用一个 HTTP 协议中没有被定义的扩展名,例如:*.atguigu

5、@RestController

①提取@ResponseBody

如果类中每个方法上都标记了 @ResponseBody 注解,那么这些注解就可以提取到类上。

②合并

类上的 @ResponseBody 注解可以和 @Controller 注解合并为 @RestController 注解。所以使用了 @RestController 注解就相当于给类中的每个方法都加了 @ResponseBody 注解。

③@RestController源码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
 
    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any (or empty String otherwise)
     * @since 4.0.1
     */
    @AliasFor(annotation = Controller.class)
    String value() default "";
 
}

6、SpringMVC 4 版本响应体字符集设置

// 当返回响应体数据包含乱码时,在@RequestMapping注解中设置
// produces属性给响应体设置内容类型
@ResponseBody
@RequestMapping(value = "/ajax/get/message", produces = "text/html;charset=UTF-8")
public String getMessage() {
    return "message from server:你好";
}
 
// 如果返回 JSON 数据时遇到乱码问题,那么内容类型应设置为:application/json;charset=UTF-8
// 这里需要注意:JSON 属于 application 这个大类,不属于 text
@ResponseBody
@RequestMapping(value = "/ajax/get/entity", produces = "application/json;charset=UTF-8")
public Emp getEntity() {
 
    Emp emp = new Emp();
 
    emp.setEmpName("舔狗");
 
    return emp;
}

第二节 拦截器

1、概念

①拦截器和过滤器解决类似的问题

[1]生活中坐地铁的场景

为了提高乘车效率,在乘客进入站台前统一检票:

[2]程序中

在程序中,使用拦截器在请求到达具体 handler 方法前,统一执行检测。

②拦截器 VS 过滤器

[1]相似点

三要素相同

  • 拦截:必须先把请求拦住,才能执行后续操作
  • 过滤:拦截器或过滤器存在的意义就是对请求进行统一处理
  • 放行:对请求执行了必要操作后,放请求过去,让它访问原本想要访问的资源
[2]不同点
  • 工作平台不同
    • 过滤器工作在 Servlet 容器中
    • 拦截器工作在 SpringMVC 的基础上
  • 拦截的范围
    • 过滤器:能够拦截到的最大范围是整个 Web 应用
    • 拦截器:能够拦截到的最大范围是整个 SpringMVC 负责的请求
  • IOC 容器支持
    • 过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
    • 拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持
[3]选择

功能需要如果用 SpringMVC 的拦截器能够实现,就不使用过滤器。

2、使用

①创建拦截器类

[1]实现接口
public class Process01Interceptor implements HandlerInterceptor {
 
    Logger logger = LoggerFactory.getLogger(this.getClass());
 
    // 在处理请求的目标 handler 方法前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        logger.debug("Process01Interceptor preHandle方法");
         
        // 返回true:放行
        // 返回false:不放行
        return true;
    }
 
    // 在目标 handler 方法之后,渲染视图之前
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
 
        logger.debug("Process01Interceptor postHandle方法");
        
    }
 
    // 渲染视图之后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        
        logger.debug("Process01Interceptor afterCompletion方法");
        
    }
}

单个拦截器执行顺序

  • preHandle() 方法
  • 目标 handler 方法
  • postHandle() 方法
  • 渲染视图
  • afterCompletion() 方法
[2]继承类

在较低版本的 SpringMVC 中,实现 HandlerInterceptor 接口需要把所有抽象方法都实现。但是又不是每个方法都需要使用,导致代码比较繁琐。

此时可以通过继承 HandlerInterceptorAdapter 类同样可以创建拦截器类。HandlerInterceptorAdapter 类中已经给 HandlerInterceptor 接口提供了默认实现,我们继承后不需要把每个方法都实现,只需要把有用的方法重写即可。

在 SpringMVC 较高版本(例如:5.3版本以上)中,HandlerInterceptor 接口已经借助 JDK 1.8 新特性让每个抽象方法都给出了默认实现,所以 HandlerInterceptorAdapter 这个类被标记为过时。

@Deprecated
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {

}

②注册拦截器

[1]默认拦截全部请求
<!-- 注册拦截器 -->
<mvc:interceptors>
    
    <!-- 直接通过内部 bean 配置的拦截器默认拦截全部请求(SpringMVC 范围内) -->
    <bean class="com.atguigu.mvc.interceptor.Process01Interceptor"/>
</mvc:interceptors>
[2]配置拦截路径
(1)精确匹配
<!-- 具体配置拦截器可以指定拦截的请求地址 -->
<mvc:interceptor>
    <!-- 精确匹配 -->
    <mvc:mapping path="/common/request/one"/>
    <bean class="com.atguigu.mvc.interceptor.Process03Interceptor"/>
</mvc:interceptor>
(2)模糊匹配:匹配单层路径
<mvc:interceptor>
    <!-- /*匹配路径中的一层 -->
    <mvc:mapping path="/common/request/*"/>
    <bean class="com.atguigu.mvc.interceptor.Process04Interceptor"/>
</mvc:interceptor>
(3)模糊匹配:匹配多层路径
<mvc:interceptor>
    <!-- /**匹配路径中的多层 -->
    <mvc:mapping path="/common/request/**"/>
    <bean class="com.atguigu.mvc.interceptor.Process05Interceptor"/>
</mvc:interceptor>
[3]配置不拦截路径
        <mvc:interceptor>
            <!-- /**匹配路径中的多层 -->
            <mvc:mapping path="/common/request/**"/>
 
            <!-- 使用 mvc:exclude-mapping 标签配置不拦截的地址 -->
            <mvc:exclude-mapping path="/common/request/two/bbb"/>
 
            <bean class="com.atguigu.mvc.interceptor.Process05Interceptor"/>
        </mvc:interceptor>

③多个拦截器执行顺序

  • preHandle()方法:和配置的顺序一样
  • 目标handler方法
  • postHandle()方法:和配置的顺序相反
  • 渲染视图
  • afterCompletion()方法:和配置的顺序相反

第三节 类型转换

SpringMVC 将『把请求参数注入到 POJO 对象』这个操作称为『数据绑定』,英文单词是 binding。数据类型的转换和格式化就发生在数据绑定的过程中。 类型转换和格式化是密不可分的两个过程,很多带格式的数据必须明确指定格式之后才可以进行类型转换。最典型的就是日期类型。

1、自动类型转换

HTTP 协议是一个无类型的协议,我们在服务器端接收到请求参数等形式的数据时,本质上都是字符串类型。请看 javax.servlet.ServletRequest 接口中获取全部请求参数的方法:

public Map<String, String[]> getParameterMap();

而我们在实体类当中需要的类型是非常丰富的。对此,SpringMVC 对基本数据类型提供了自动的类型转换。例如:请求参数传入“100”字符串,我们实体类中需要的是 Integer 类型,那么 SpringMVC 会自动将字符串转换为 Integer 类型注入实体类。

2、日期和数值类型

①通过注解设定数据格式

public class Product {
 
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date productDate;
 
    @NumberFormat(pattern = "###,###,###.###")
    private Double productPrice;

②表单

<form th:action="@{/save/product}" method="post">
    生产日期:<input type="text" name="productDate" value="1992-10-15 17:15:06" /><br/>
    产品价格:<input type="text" name="productPrice" value="111,222,333.444" /><br/>
    <button type="submit">保存</button>
</form>

③handler 方法

@RequestMapping("/save/product")
public String saveProduct(Product product) {
 
    logger.debug(product.toString());
 
    return "target";
}

3、转换失败后处理方式

①默认结果

②BindingResult 接口

BindingResult 接口和它的父接口 Errors 中定义了很多和数据绑定相关的方法,如果在数据绑定过程中发生了错误,那么通过这个接口类型的对象就可以获取到相关错误信息。

③重构 handler 方法

@RequestMapping("/save/product")
public String saveProduct(
        Product product,
 
	    // 在实体类参数和 BindingResult 之间不能有任何其他参数
        // 封装数据绑定结果的对象
        BindingResult bindingResult) {
 
    // 判断数据绑定过程中是否发生了错误
    if (bindingResult.hasErrors()) {
        // 如果发生了错误,则跳转到专门显示错误信息的页面
        // 相关错误信息会自动被放到请求域
        return "error";
    }
 
    logger.debug(product.toString());
 
    return "target";
}

④在页面上显示错误消息

	<!-- 从请求域获取实体类信息时,属性名是按照类名首字母小写的规则 -->
	<!-- ${注入请求参数的实体类.出问题的字段} -->
    <p th:errors="${product.productDate}">这里显示具体错误信息</p>

4、自定义类型转换器

在实际开发过程中,难免会有某些情况需要使用自定义类型转换器。因为我们自己自定义的类型在 SpringMVC 中没有对应的内置类型转换器。此时需要我们提供自定义类型来执行转换。

我们学习的知识点可以分成:

  • 拼死学会

  • 以防万一

  • 增长见闻

    自定义类型转换器的定位就是以防万一

①创建实体类

[1]Address
public class Address {
 
    private String province;
    private String city;
    private String street;
    ……
[2]Student
public class Student {
 
    private Address address;
    ……

②表单

现在我们希望通过一个文本框输入约定格式的字符串,然后转换为我们需要的类型,所以必须通过自定义类型转换器来实现,否则 SpringMVC 无法识别。

<h3>自定义类型转换器</h3>
<form th:action="@{/save/student}" method="post">
    地址:<input type="text" name="address" value="aaa,bbb,ccc" /><br/>
</form>

③handler 方法

@RequestMapping("/save/student")
public String saveStudent(Student student) {
 
    logger.debug(student.getAddress().toString());
 
    return "target";
}

在目前代码的基础上,我们没有提供自定义类型转换器,所以处理请求时看到如下错误日志:

Field error in object 'student' on field 'address': rejected value [aaa,bbb,ccc]; codes [typeMismatch.student.address,typeMismatch.address,typeMismatch.com.atguigu.mvc.entity.Address,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.address,address]; arguments []; default message [address]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.atguigu.mvc.entity.Address' for property 'address'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.atguigu.mvc.entity.Address' for property 'address': no matching editors or conversion strategy found]]]

页面返回 400。

④创建自定义类型转换器类

实现接口:org.springframework.core.convert.converter.Converter<S,T>

泛型 S:源类型(本例中是 String 类型)

泛型 T:目标类型(本例中是 Address 类型)

public class AddressConverter implements Converter<String, Address> {
    @Override
    public Address convert(String source) {
  
        // 1.按照约定的规则拆分源字符串
        String[] split = source.split(",");
         
        String province = split[0];
        String city = split[1];
        String street = split[2];
 
        // 2.根据拆分结果创建 Address 对象
        Address address = new Address(province, city, street);
         
        // 3.返回转换得到的对象
        return address;
    }
}

⑤在 SpringMVC 中注册

<!-- 在 mvc:annotation-driven 中注册 FormattingConversionServiceFactoryBean -->
<mvc:annotation-driven conversion-service="formattingConversionService"/>
 
<!-- 在 FormattingConversionServiceFactoryBean 中注册自定义类型转换器 -->
<bean id="formattingConversionService"
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">

    <!-- 在 converters 属性中指定自定义类型转换器 -->
    <property name="converters">
        <set>
            <bean class="com.atguigu.mvc.converter.AddressConverter"/>
        </set>
    </property>
 
</bean>

第四节 数据校验

在 Web 应用三层架构体系中,表述层负责接收浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的数据隔绝在业务逻辑层之外。

1、校验概述

JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。

注解 规则
@Null 标注值必须为 null
@NotNull 标注值不可为 null
@AssertTrue 标注值必须为 true
@AssertFalse 标注值必须为 false
@Min(value) 标注值必须大于或等于 value
@Max(value) 标注值必须小于或等于 value
@DecimalMin(value) 标注值必须大于或等于 value
@DecimalMax(value) 标注值必须小于或等于 value
@Size(max,min) 标注值大小必须在 max 和 min 限定的范围内
@Digits(integer,fratction) 标注值值必须是一个数字,且必须在可接受的范围内
@Past 标注值只能用于日期型,且必须是过去的日期
@Future 标注值只能用于日期型,且必须是将来的日期
@Pattern(value) 标注值必须符合指定的正则表达式

JSR 303 只是一套标准,需要提供其实现才可以使用。Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:

注解 规则
@Email 标注值必须是格式正确的 Email 地址
@Length 标注值字符串大小必须在指定的范围内
@NotEmpty 标注值字符串不能是空字符串
@Range 标注值必须在指定的范围内

Spring 4.0 版本已经拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC 中,可直接通过注解驱动 mvc:annotation-driven 的方式进行数据校验。Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean中。Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的实现者的jar包放到类路径下。

配置 mvc:annotation-driven 后,SpringMVC 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Validated 注解即可让 SpringMVC 在完成数据绑定后执行数据校验的工作。

2、操作演示

请在 SpringMVC 环境基础上做下面的操作:

①导入依赖

<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>6.2.0.Final</version>
</dependency>

注:需要 Tomcat 版本至少是 8。

②应用校验规则

[1]标记规则注解

在实体类中需要附加校验规则的成员变量上标记校验规则注解:

    // 字符串长度:[3,6]
    @Size(min = 3, max = 6)
    
    // 字符串必须满足Email格式
    @Email
    private String email;

[2]在handler 方法形参标记注解

@RequestMapping("/save/president")
public String savePresident(@Validated President president) {
 
    logger.debug(president.getEmail());
 
    return "target";
}
[3]校验失败效果

日志:

Field error in object 'president' on field 'email': rejected value [aa]; codes [Email.president.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [president.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@4a6addb7,.*]; default message [不是一个合法的电子邮件地址]
Field error in object 'president' on field 'email': rejected value [aa]; codes [Size.president.email,Size.email,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [president.email,email]; arguments []; default message [email],6,3]; default message [个数必须在3和6之间]]]

同时页面返回 400。

③显示友好的错误提示

[1]重构 handler 方法
@RequestMapping("/save/president")
public String savePresident(
         
        // 在实体类参数和 BindingResult 之间不能有任何其他参数
        @Validated President president, BindingResult bindingResult) {
 
    if (bindingResult.hasErrors()) {
        return "error";
    }
     
    logger.debug(president.getEmail());
 
    return "target";
}
[2]准备错误信息页面
    <h1>系统信息</h1>
    <!-- 从请求域获取实体类信息时,属性名是按照类名首字母小写的规则 -->
    <!-- ${注入请求参数的实体类.出问题的字段} -->
    <p th:errors="${president.email}">这里显示系统提示消息</p>

第五节 异常映射

1、概念

①微观

将异常类型和某个具体的视图关联起来,建立映射关系。好处是可以通过 SpringMVC 框架来帮助我们管理异常。

  • 声明式管理异常:在配置文件中指定异常类型和视图之间的对应关系。在配置文件或注解类中统一管理。
  • 编程式管理异常:需要我们自己手动 try ... catch ... 捕获异常,然后再手动跳转到某个页面。

②宏观

一个项目中会包含很多个模块,各个模块需要分工完成。如果张三负责的模块按照 A 方案处理异常,李四负责的模块按照 B 方法处理异常……各个模块处理异常的思路、代码、命名细节都不一样,那么就会让整个项目非常混乱。

2、异常映射的好处

  • 使用声明式代替编程式来实现异常管理
    • 让异常控制和核心业务解耦,二者各自维护,结构性更好
  • 整个项目层面使用同一套规则来管理异常
    • 整个项目代码风格更加统一、简洁
    • 便于团队成员之间的彼此协作

3、基于 XML 的异常映射

①配置

SpringMVC 会根据异常映射信息,在捕获到指定异常对象后,将异常对象存入请求域,然后转发到和异常类型关联的视图。

<bean id="exceptionResolver"
      class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
 
    <!-- 配置异常映射关系 -->
    <property name="exceptionMappings">
        <props>
            <!-- key属性:指定异常类型 -->
            <!-- 文本标签体:和异常类型对应的逻辑视图 -->
            <prop key="java.lang.ArithmeticException">error-arith</prop>
        </props>
    </property>
 
    <!-- 使用 exceptionAttribute 属性配置将异常对象存入请求域时使用的属性名 -->
    <!-- 这个属性名默认是exception -->
    <property name="exceptionAttribute" value="atguiguException"/>
</bean>

②异常范围

如果在配置文件中,发现有多个匹配的异常类型,那么 SpringMVC 会采纳范围上最接近的异常映射关系。

<prop key="java.lang.ArithmeticException">error-arith</prop>
<prop key="java.lang.RuntimeException">error-runtime</prop>

4、基于注解的异常映射

①创建异常处理器类

②异常处理器类加入 IOC 容器

<context:component-scan base-package="com.atguigu.mvc.handler,com.atguigu.mvc.exception"/>

③给异常处理器类标记注解

// 异常处理器类需要使用 @ControllerAdvice 注解标记
@ControllerAdvice
public class MyExceptionHandler {
    
}

④声明处理异常的方法

// @ExceptionHandler注解:标记异常处理方法
// value属性:指定匹配的异常类型
// 异常类型的形参:SpringMVC 捕获到的异常对象
@ExceptionHandler(value = NullPointerException.class)
public String resolveNullPointerException(Exception e, Model model) {
 
    // 我们可以自己手动将异常对象存入模型
    model.addAttribute("atguiguException", e);
 
    // 返回逻辑视图名称
    return "error-nullpointer";
}

当同一个异常类型在基于 XML 和注解的配置中都能够找到对应的映射,那么以注解为准。

5、区分请求类型

①分析

异常处理机制和拦截器机制都面临这样的问题:

②判断依据

查看请求消息头中是否包含 Ajax 请求独有的特征:

  • Accept 请求消息头:包含 application/json
  • X-Requested-With 请求消息头:包含 XMLHttpRequest

两个条件满足一个即可。

/**
 * 判断当前请求是否为Ajax请求
 * @param request 请求对象
 * @return
 * 		true:当前请求是Ajax请求
 * 		false:当前请求不是Ajax请求
 */
public static boolean judgeRequestType(HttpServletRequest request) {
 	
	// 1.获取请求消息头
	String acceptHeader = request.getHeader("Accept");
	String xRequestHeader = request.getHeader("X-Requested-With");
 	
	// 2.判断
	return (acceptHeader != null && acceptHeader.contains("application/json"))
 			
			||
 			
			(xRequestHeader != null && xRequestHeader.equals("XMLHttpRequest"));
}

③兼容两种请求的处理方法

@ExceptionHandler(value = Exception.class)
public String resolveException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
 
    // 调用工具方法判断当前请求是否是 Ajax 请求
    boolean judgeResult = MVCUtil.judgeRequestType(request);
 
    if (judgeResult) {
 
        // 对 Ajax 请求返回字符串作为响应体
        String message = e.getMessage();
 
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(message);
 
        // 上面已经使用原生 response 对象返回了响应,这里就不返回视图名称了
        return null;
    }
 
    // 对普通请求返回逻辑视图名称
    return "error-exception";
}

第六节 文件上传

1、表单

  • 第一点:请求方式必须是 POST
  • 第二点:请求体的编码方式必须是 multipart/form-data(通过 form 标签的 enctype 属性设置)
  • 第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框
<form th:action="@{/atguigu/upload}" method="post" enctype="multipart/form-data">
    
    <input type="file" name="picture" />
    <button type="submit">上传头像</button>
     
</form>

2、SpringMVC 环境要求

①依赖

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

②配置

在 SpringMVC 的配置文件中加入 multipart 类型数据的解析器:

<bean id="multipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    
    <!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
    <property name="defaultEncoding" value="UTF-8"/>
    
</bean>

3、handler 方法接收数据


@RequestMapping("/simple/upload")
public String doUpload(
 
        // 表单提交的数据仍然是请求参数,所以使用 @RequestParam 注解接收
        @RequestParam("nickName") String nickName,
 
        // 对于上传的文件使用 MultipartFile 类型接收其相关数据
        @RequestParam("picture") MultipartFile picture
        ) throws IOException {
 
    String inputName = picture.getName();
    logger.debug("文件上传表单项的 name 属性值:" + inputName);
 
    // 获取这个数据通常都是为了获取文件本身的扩展名
    String originalFilename = picture.getOriginalFilename();
    logger.debug("文件在用户本地原始的文件名:" + originalFilename);
 
    String contentType = picture.getContentType();
    logger.debug("文件的内容类型:" + contentType);
 
    boolean empty = picture.isEmpty();
    logger.debug("文件是否为空:" + empty);
 
    long size = picture.getSize();
    logger.debug("文件大小:" + size);
 
    byte[] bytes = picture.getBytes();
    logger.debug("文件二进制数据的字节数组:" + Arrays.asList(bytes));
 
    InputStream inputStream = picture.getInputStream();
    logger.debug("读取文件数据的输入流对象:" + inputStream);
 
    Resource resource = picture.getResource();
    logger.debug("代表当前 MultiPartFile 对象的资源对象" + resource);
 
    return "target";
}

4、MultipartFile 接口

5、文件转存

①底层机制

②三种去向

[1]本地转存

(1)实现方式
<1>创建保存文件的目录

这个目录如果是空目录,那么服务器部署运行时很容易会忽略这个目录。为了避免这个问题,在这个目录下随便创建一个文件,随便写点内容即可。

<2>编写转存代码

下面是负责处理文件上传请求的 handler 方法的转存部分:

……
 
// 1、准备好保存文件的目标目录
// ①File 对象要求目标路径是一个物理路径(在硬盘空间里能够直接找到文件的路径)
// ②项目在不同系统平台上运行,要求能够自动兼容、适配不同系统平台的路径格式
//      例如:Window系统平台的路径是 D:/aaa/bbb 格式
//      例如:Linux系统平台的路径是 /ttt/uuu/vvv 格式
//      所以我们需要根据『不会变的虚拟路径』作为基准动态获取『跨平台的物理路径』
// ③虚拟路径:浏览器通过 Tomcat 服务器访问 Web 应用中的资源时使用的路径
String destFileFolderVirtualPath = "/head-picture";
 
// ④调用 ServletContext 对象的方法将虚拟路径转换为真实物理路径
String destFileFolderRealPath = servletContext.getRealPath(destFileFolderVirtualPath);
 
// 2、生成保存文件的文件名
// ①为了避免同名的文件覆盖已有文件,不使用 originalFilename,所以需要我们生成文件名
// ②我们生成文件名包含两部分:文件名本身和扩展名
// ③声明变量生成文件名本身
String generatedFileName = UUID.randomUUID().toString().replace("-","");
 
// ④根据 originalFilename 获取文件的扩展名
String fileExtname = originalFilename.substring(originalFilename.lastIndexOf("."));
 
// ⑤拼装起来就是我们生成的整体文件名
String destFileName = generatedFileName + "" + fileExtname;
 
// 3、拼接保存文件的路径,由两部分组成
//      第一部分:文件所在目录
//      第二部分:文件名
String destFilePath = destFileFolderRealPath + "/" + destFileName;
 
// 4、创建 File 对象,对应文件具体保存的位置
File destFile = new File(destFilePath);
 
// 5、执行转存
picture.transferTo(destFile);
 
……
(2)缺陷
  • Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
  • 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
  • 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。
  • 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。

[2]文件服务器(采纳)
(1)总体机制

(2)好处
  • 不受 Web 应用重新部署影响
  • 在应用服务器集群环境下不会导致数据不一致
  • 针对文件读写进行专门的优化,性能有保障
  • 能够实现动态扩容

(3)文件服务器类型
  • 第三方平台:
    • 阿里的 OSS 对象存储服务
    • 七牛云
  • 自己搭建服务器:FastDFS等
[3]上传到其他模块

这种情况肯定出现在分布式架构中,常规业务功能不会这么做,采用这个方案的一定的特殊情况。

在 MultipartFile 接口中有一个对应的方法:

/**
 * Return a Resource representation of this MultipartFile. This can be used
 * as input to the {@code RestTemplate} or the {@code WebClient} to expose
 * content length and the filename along with the InputStream.
 * @return this MultipartFile adapted to the Resource contract
 * @since 5.1
 */
default Resource getResource() {
    return new MultipartFileResource(this);
}

注释中说:这个 Resource 对象代表当前 MultipartFile 对象,输入给 RestTemplate 或 WebClient。而 RestTemplate 或 WebClient 就是用来在 Java 程序中向服务器端发出请求的组件。

第七节 文件下载

1、初始形态

使用链接地址指向要下载的文件。此时浏览器会尽可能解析对应的文件,只要是能够在浏览器窗口展示的,就都会直接显示,而不是提示下载。

<a href="download/hello.atguigu">下载</a><br/>
<a href="download/tank.jpg">下载</a><br/>
<a href="download/chapter04.zip">下载</a><br/>

上面例子中,只有 chapter04.zip 文件是直接提示下载的,其他两个都是直接显示。

2、明确要求浏览器提示下载

@Autowired
private ServletContext servletContext;

@RequestMapping("/download/file")
public ResponseEntity<byte[]> downloadFile() {

    // 1.获取要下载的文件的输入流对象
    // 这里指定的路径以 Web 应用根目录为基准
    InputStream inputStream = servletContext.getResourceAsStream("/images/mi.jpg");

    try {
        // 2.将要下载的文件读取到字节数组中
        // ①获取目标文件的长度
        int len = inputStream.available();

        // ②根据目标文件长度创建字节数组
        byte[] buffer = new byte[len];

        // ③将目标文件读取到字节数组中
        inputStream.read(buffer);

        // 3.封装响应消息头
        // ①创建MultiValueMap接口类型的对象,实现类是HttpHeaders
        MultiValueMap responseHeaderMap = new HttpHeaders();

        // ②存入下载文件所需要的响应消息头
        responseHeaderMap.add("Content-Disposition", "attachment; filename=mi.jpg");

        // ③创建ResponseEntity对象
        ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(buffer, responseHeaderMap, HttpStatus.OK);

        // 4.返回responseEntity对象
        return responseEntity;
    } catch (IOException e) {
        e.printStackTrace();
    } finally {

        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    return null;
}

3、典型应用场景举例

我们目前实现的是一个较为简单的下载,可以用在下面的一些场合:

  • 零星小文件下载
  • 将系统内部的数据导出为 Excel、PDF 等格式,然后以下载的方式返回给用户

第八节 其他不重要内容

1、SpringMVC 配置文件的默认位置

①配置要求

  • 配置文件存放目录:/WEB-INF 目录
  • 文件名格式:[servlet-name]-servlet.xml
    • servlet-name 部分是在 web.xml 中配置 DispatcherServlet 时,servlet-name 标签的值
  • 省略原理的 init-param

②为什么不建议

除 web.xml 是 Tomcat 要求放在 WEB-INF 下,其他配置文件习惯上是放在类路径下。

2、请求映射其他方式

①根据请求参数情况映射

使用 @RequestMapping 注解的 params 参数实现,表达式语法参见下面的例子:

需求 映射方式
请求参数中必须包含userName @RequestMapping(value = "/xxx",
params="userName")
请求参数中不能包含userName @RequestMapping(value = "/xxx",
params="!userName")
请求参数中必须包含userName
且值必须为Tom2015
@RequestMapping(value = "/xxx",
params="userName=Tom2015")
请求参数中必须包含userName
但值不能为Tom2015
@RequestMapping(value = "/xxx",
params="userName=!Tom2015")
请求参数中必须包含userName
且值为Tom2015,
同时必须包含userPwd但值不限
@RequestMapping(value = "/xxx",
params={"userName=Tom2015","userPwd"} )

②根据请求消息头内容映射

使用 @RequestMapping 注解的 headers 参数实现,表达式语法参见下面的例子:

需求 映射方式
根据 Accept-Language:zh-CN,zh;q=0.8 映射 @RequestMapping (
value="/xxx",
headers= "Accept-Language=zh-CN,en;q=0.8" )

③Ant 风格通配符

  • 英文问号:匹配一个字符
  • 一个星号:匹配路径中的一层
  • 两个连续星号:匹配路径中的多层

3、@ModelAttribute 注解

handler 类中,选定一个方法标记 @ModelAttribute 注解。

  • 效果1:在每个 handler 方法前执行
  • 效果2:可以将某些数据提前存入请求域
@Controller
public class ModelAttrHandler {
 
    @ModelAttribute
    public void doSthBefore(Model model) {
        model.addAttribute("initAttr", "initValue");
    }
 
    @RequestMapping("/test/model/attr/one")
    public String testModelAttrOne(Model model) {
 
        Object modelAttribute = model.getAttribute("initAttr");
        System.out.println("modelAttribute = " + modelAttribute);
 
        return "target";
    }
 
    @RequestMapping("/test/model/attr/two")
    public String testModelAttrTwo(Model model) {
 
        Object modelAttribute = model.getAttribute("initAttr");
        System.out.println("modelAttribute = " + modelAttribute);
 
        return "target";
    }
 
    @RequestMapping("/test/model/attr/three")
    public String testModelAttrThree(Model model) {
 
        Object modelAttribute = model.getAttribute("initAttr");
        System.out.println("modelAttribute = " + modelAttribute);
 
        return "target";
    }
 
}

第五章 SpringMVC运行原理

第一节 启动过程

1、Servlet 生命周期回顾

生命周期环节 调用的方法 时机 次数
创建对象 无参构造器 默认:第一次请求 修改:Web应用启动时 一次
初始化 init(ServletConfig servletConfig) 创建对象后 一次
处理请求 service(ServletRequest servletRequest, ServletResponse servletResponse) 接收到请求后 多次
清理操作 destroy() Web应用卸载之前 一次

2、初始化操作调用路线图

3、IOC容器创建

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
    }
    
    // 通过反射创建 IOC 容器对象
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    
    // 设置父容器
    wac.setParent(parent);
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

4、将 IOC 容器对象存入应用域

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 创建 IOC 容器
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        synchronized (this.onRefreshMonitor) {
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // 获取存入应用域时专用的属性名
        String attrName = getServletContextAttributeName();
        
        // 存入
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

看到这一点的意义:SpringMVC 有一个工具方法,可以从应用域获取 IOC 容器对象的引用。

工具类:org.springframework.web.context.support.WebApplicationContextUtils

工具方法:getWebApplicationContext()

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

5、请求映射初始化

FrameworkServlet.createWebApplicationContext()→configureAndRefreshWebApplicationContext()→wac.refresh()→触发刷新事件→org.springframework.web.servlet.DispatcherServlet.initStrategies()→org.springframework.web.servlet.DispatcherServlet.initHandlerMappings()

6、小结

整个启动过程我们关心如下要点:

  • DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。
  • DispatcherServlet 的父类是 FrameworkServlet。
    • FrameworkServlet 负责框架本身相关的创建和初始化。
    • DispatcherServlet 负责请求处理相关的初始化。
  • FrameworkServlet 创建 IOC 容器对象之后会存入应用域。
  • FrameworkServlet 完成初始化会调用 IOC 容器的刷新方法。
  • 刷新方法完成触发刷新事件,在刷新事件的响应函数中,调用 DispatcherServlet 的初始化方法。
  • 在 DispatcherServlet 的初始化方法中初始化了请求映射等。

第二节 请求处理过程

1、总体阶段

  • 目标 handler 方法执行
    • 建立调用链,确定整个执行流程
    • 拦截器的 preHandle() 方法
    • 注入请求参数
    • 准备目标 handler 方法所需所有参数
  • 调用目标 handler 方法
  • 目标 handler 方法执行
    • 拦截器的 postHandle() 方法
    • 渲染视图
    • 拦截器的 afterCompletion() 方法

2、调用前阶段

①建立调用链

[1]相关组件

全类名:org.springframework.web.servlet.HandlerExecutionChain

拦截器索引默认是 -1,说明开始的时候,它指向第一个拦截器前面的位置。每执行一个拦截器,就把索引向前移动一个位置。所以这个索引每次都是指向当前拦截器。所以它相当于拦截器的指针

[2]对应操作

所在类:org.springframework.web.servlet.handler.AbstractHandlerMapping

结论:调用链是由拦截器和目标 handler 对象组成的。

②调用拦截器 preHandle()

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

具体调用细节:正序调用

所在类:org.springframework.web.servlet.HandlerExecutionChain

所在方法:applyPreHandle

③注入请求参数

[1]相关组件

接口:org.springframework.web.servlet.HandlerAdapter

作用:字面含义是适配器的意思,具体功能有两个

  • 将请求参数绑定到实体类对象中
  • 调用目标 handler 方法
[2]创建并获取这个组件

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

[3]具体操作:调用目标 handler 方法

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

[4]具体操作:注入请求参数

3、调用后阶段

①调用拦截器的 postHandle() 方法

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

调用细节:从拦截器集合长度 - 1 开始循环,循环到 0 为止。所以是倒序执行。

②渲染视图

[1]所有后续操作的入口

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

[2]后续细节1:处理异常

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:processDispatchResult()

[3]后续细节2:渲染视图

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:processDispatchResult()

补充细节:模型数据存入请求域的具体位置

所在类:org.thymeleaf.context.WebEngineContext.RequestAttributesVariablesMap

所在方法:setVariable()

③调用拦截器的 afterCompletion() 方法

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:processDispatchResult()

调用细节:从拦截器索引开始循环,直到循环变量 i 被减到 0 为止。这样的效果是前面执行拦截器到哪里,就从哪里倒回去执行;前面没有执行的拦截器,现在也不执行。

4、所有断点总结

断点位置基准:SpringMVC 版本采用 5.3.1 且源码已经下载,包含注释。

所在类 所在方法 断点行数 作用
DispatcherServlet doDispatch() 1037 创建调用链对象
DispatcherServlet doDispatch() 1044 创建 HandlerAdapter 对象
DispatcherServlet doDispatch() 1056 调用拦截器 preHandle()方法
DispatcherServlet doDispatch() 1061 执行目标 handler 方法
DispatcherServlet doDispatch() 1068 调用拦截器 postHandle()方法
DispatcherServlet doDispatch() 1078 执行所有后续操作
AbstractHandlerMapping getHandlerExecutionChain() 592 创建调用链对象
AbstractHandlerMapping getHandlerExecutionChain() 599 在调用链中添加拦截器
HandlerExecutionChain applyPreHandle() 146 调用拦截器 preHandle()方法
HandlerExecutionChain applyPostHandle() 163 调用拦截器 postHandle()方法
HandlerExecutionChain triggerAfterCompletion 175 调用拦截器 afterCompletion()方法
DataBinder doBind() 747 执行数据绑定
RequestMappingHandlerAdapter invokeHandlerMethod() 868 创建 ModelAndViewContainer 对象
RequestMappingHandlerAdapter invokeHandlerMethod() 893 将ModelAndViewContainer 对象传入调用目标 handler 的方法
DispatcherServlet processDispatchResult() 1125 处理异常
DispatcherServlet processDispatchResult() 1139 渲染视图
DispatcherServlet processDispatchResult() 1157 调用拦截器 afterCompletion()方法
WebEngineContext的内部类: RequestAttributesVariablesMap setVariable() 783 将模型数据存入请求域

第三节 ContextLoaderListener

1、提出问题

目前情况:DispatcherServlet 加载 spring-mvc.xml,此时整个 Web 应用中只创建一个 IOC 容器。将来整合Mybatis、配置声明式事务,全部在 spring-mvc.xml 配置文件中配置也是可以的。可是这样会导致配置文件太长,不容易维护。

所以想到把配置文件分开:

  • 处理浏览器请求相关:spring-mvc.xml 配置文件
  • 声明式事务和整合Mybatis相关:spring-persist.xml 配置文件

配置文件分开之后,可以让 DispatcherServlet 加载多个配置文件。例如:

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

如果希望这两个配置文件使用不同的机制来加载:

  • DispatcherServlet 加载 spring-mvc.xml 配置文件:它们和处理浏览器请求相关
  • ContextLoaderListener 加载 spring-persist.xml 配置文件:不需要处理浏览器请求,需要配置持久化层相关功能

此时会带来一个新的问题:在 Web 一个应用中就会出现两个 IOC 容器

  • DispatcherServlet 创建一个 IOC 容器
  • ContextLoaderListener 创建一个 IOC 容器

注意:本节我们探讨的这个技术方案并不是『必须』这样做,而仅仅是『可以』这样做。

2、 配置 ContextLoaderListener

①创建 spring-persist.xml

②配置 ContextLoaderListener


<!-- 通过全局初始化参数指定 Spring 配置文件的位置 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-persist.xml</param-value>
</context-param>
 
<listener>
    <!-- 指定全类名,配置监听器 -->
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

③ContextLoaderListeners

方法名 执行时机 作用
contextInitialized() Web 应用启动时执行 创建并初始化 IOC 容器
contextDestroyed() Web 应用卸载时执行 关闭 IOC 容器

④ContextLoader

[1]指定配置文件位置的参数名

/**
 * Name of servlet context parameter (i.e., {@value}) that can specify the
 * config location for the root context, falling back to the implementation's
 * default otherwise.
 * @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
 */
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
[2]初始化 IOC 容器

方法名:initWebApplicationContext()

[3]创建 IOC 容器

方法名:createWebApplicationContext()

3、探讨两个IOC容器之间的关系

打印两个 IOC 容器对象的 toString() 方法:


Object springIOC = servletContext.getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");
logger.debug(springIOC.toString());

Object springMVCIOC = servletContext.getAttribute("org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet");
logger.debug(springMVCIOC.toString());

打印效果是:

Root WebApplicationContext, started on Thu Jun 17 14:49:17 CST 2021

WebApplicationContext for namespace 'dispatcherServlet-servlet', started on Thu Jun 17 14:49:18 CST 2021, parent: Root WebApplicationContext

结论:两个组件分别创建的 IOC 容器是父子关系。

  • 父容器:ContextLoaderListener 创建的 IOC 容器
  • 子容器:DispatcherServlet 创建的 IOC 容器

父子关系是如何决定的?

  • ContextLoaderListener 初始化时如果检查到有已经存在的根级别 IOC 容器,那么会抛出异常。
  • DispatcherServlet 创建的 IOC 容器会在初始化时先检查当前环境下是否存在已经创建好的 IOC 容器。
    • 如果有:则将已存在的这个 IOC 容器设置为自己的父容器
    • 如果没有:则将自己设置为 root 级别的 IOC 容器
  • 同时 Tomcat 在读取 web.xml 之后,加载组件的顺序就是监听器、过滤器、Servlet。

DispatcherServlet 创建的 IOC 容器设置父容器的源码截图:

所在类:org.springframework.web.servlet.FrameworkServlet

所在方法:createWebApplicationContext()

4、探讨两个 IOC 容器之间 bean 的互相访问

spring-mvc.xml配置方式:


<context:component-scan base-package="com.atguigu.spring.component.controller"/>

spring-persist.xml配置方式:


<context:component-scan base-package="com.atguigu.spring.component.service,com.atguigu.spring.component.dao"/>

所以bean所属IOC容器的关系:

  • 父容器
    • EmpService
    • EmpDao
  • 子容器
    • EmpController

结论:子容器中的 EmpController 装配父容器中的 EmpService 能够正常工作。说明子容器可以访问父容器中的bean。

分析:“子可用父,父不能用子”的根本原因是子容器中有一个属性 getParent() 可以获取到父容器这个对象的引用。

源码依据:

  • 在 AbstractApplicationContext 类中,有 parent 属性
  • 在 AbstractApplicationContext 类中,有获取 parent 属性的 getParent() 方法
  • 子容器可以通过 getParent() 方法获取到父容器对象的引用
  • 进而调用父容器中类似 “getBean()” 这样的方法获取到需要的 bean 完成装配
  • 而父容器中并没有类似 “getChildren()“ 这样的方法,所以没法拿到子容器对象的引用

5、有可能重复创建对象

①查看日志确认是否重复创建了对象

Root WebApplicationContext: initialization started

……

Creating shared instance of singleton bean 'helloDao' Creating shared instance of singleton bean 'helloHandler' Creating shared instance of singleton bean 'helloService'

……

Root WebApplicationContext initialized in 1150 ms

……

Refreshing WebApplicationContext for namespace 'dispatcherServlet-servlet'

……

Creating shared instance of singleton bean 'helloDao'

Creating shared instance of singleton bean 'helloHandler'

Creating shared instance of singleton bean 'helloService'

……

②重复创建对象的问题

  • 浪费内存空间

  • 两个 IOC 容器能力是不同的

    • spring-mvc.xml:仅配置和处理请求相关的功能。所以不能给 service 类附加声明式事务功能。

      结论:基于 spring-mvc.xml 配置文件创建的 EmpService 的 bean 不带有声明式事务的功能

      影响:DispatcherServlet 处理浏览器请求时会调用自己创建的 EmpController,然后再调用自己创建的EmpService,而这个 EmpService 是没有事务的,所以处理请求时没有事务功能的支持

    • spring-persist.xml:配置声明式事务。所以可以给 service 类附加声明式事务功能。

      结论:基于 spring-persist.xml 配置文件创建的 EmpService 有声明式事务的功能

      影响:由于 DispatcherServlet 的 IOC 容器会优先使用自己创建的 EmpController,进而装配自己创建的EmpService,所以基于 spring-persist.xml 配置文件创建的有声明式事务的 EmpService 用不上。

③解决重复创建对象的问题

[1]解决方案一[建议使用]

让两个配置文件配置自动扫描的包时,各自扫描各自的组件。

  • SpringMVC 就扫描 XxxHandler
  • Spring 扫描 XxxService 和 XxxDao
[2]解决方案二

如果由于某种原因,必须扫描同一个包,确实存在重复创建对象的问题,可以采取下面的办法处理。

  • spring-mvc.xml 配置文件在整体扫描的基础上进一步配置:仅包含被 @Controller 注解标记的类。
  • spring-persist.xml 配置在整体扫描的基础上进一步配置:排除被 @Controller 注解标记的类。

具体spring-mvc.xml配置文件中的配置方式如下:


<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 为了解决重复创建对象的问题,需要进一步制定扫描组件时的规则 -->
<!-- 目标:『仅』包含@Controller注解标记的类 -->
<!-- use-default-filters="false"表示关闭默认规则,表示什么都不扫描,此时不会把任何组件加入IOC容器;
        再配合context:include-filter实现“『仅』包含”效果 -->
<context:component-scan base-package="com.atguigu.spring.component" use-default-filters="false">

    <!-- context:include-filter标签配置一个“扫描组件时要包含的类”的规则,追加到默认规则中 -->
    <!-- type属性:指定规则的类型,根据什么找到要包含的类,现在使用annotation表示基于注解来查找 -->
    <!-- expression属性:规则的表达式。如果type属性选择了annotation,那么expression属性配置注解的全类名 -->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

具体spring-persist.xml配置文件中的配置方式如下:


<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 在默认规则的基础上排除标记了@Controller注解的类 -->
<context:component-scan base-package="com.atguigu.spring.component">

    <!-- 配置具体排除规则:把标记了@Controller注解的类排除在扫描范围之外 -->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

6、小结

  • DispatcherServlet 和 ContextLoaderListener 并存
    • DispatcherServlet 负责加载 SpringMVC 的配置文件,例如:spring-mvc.xml
    • ContextLoaderListener 负责加载 Spring 的配置文件,例如:spring-persist.xml
  • 两个 IOC 容器的关系:
    • ContextLoaderListener 创建的容器是父容器
    • DispatcherServlet 创建的容器是子容器
  • bean 的装配
    • 子容器可以访问父容器中的 bean
    • 父容器不能访问子容器中的 bean
  • 两个容器扫描同一个包会导致重复创建对象
    • 解决办法一:各自扫描各自的包
    • 解决办法二:
      • DispatcherServlet 创建的容器仅扫描 handler
      • ContextLoaderListener 创建的容器不扫描 handler
posted @ 2021-07-01 20:56  沙滩拾贝  阅读(399)  评论(0编辑  收藏  举报