一、SpringMVC导学

image-20221204000043498

二、SpringMVC简介

MVC
M:模型 Model

  • 指工程中的JavaBean
  • 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等
  • 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访
  • 作用是处理数据
    V:视图 View
  • 指工程中的html或jsp等页面
  • 作用是与用户进行交互,展示数据
    C:控制器 Controller
  • 指工程中的servlet
  • 作用是接收请求和响应浏览器

MVC的工作流程: 用户通过视图层发送请求到服务器,在服务器中请求被Controller接收,Controller 调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller再根据请求处理的结果找到相应的View视图,渲染数据后最终响应给浏览器。

三、SpringMVC第一个程序

导入依赖

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

配置web.xml

第一步:在src/main目录下创建webapp目录

第二步:在项目结构中创建web.xml文件,注意路径要修改为src/main/webapp/

image-20221204170642194

第三步:在web.xml中注册dispatcherServlet

1.1、默认配置方式

/不包括.jsp

<!-- 配置 SpringMVC 的前端控制器,对浏览器发送的请求进行统一处理 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <!--
        设置springMVC的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
        但是/不能匹配.jsp请求路径的请求
    -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

注意:这种方式,配置文件必须要在WEB-INF目录下,名称为servlet-name-servlet.xml,例如这里的SpringMVC-serlvet.xml,不能放在resources目录下。

1.2、扩展配置方式

1 元素标记容器是否应该在启动的时候加载这个servlet
负数时,则容器会当该Servlet被请求时,再加载
0或者正整数时,表示容器在应用启动时就加载并初始化这个servle
正整数时,数越小,优先级越高
当值相同时,容器就会自己选择顺序来加载

/中的/表示除了.jsp之外的所有请求
/*表示所有请求

<!-- 配置 SpringMVC 的前端控制器,对浏览器发送的请求进行统一处理 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 配置 SpringMVC 配置文件的位置和名称 -->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springMVC.xml</param-value>
    </init-param>
    <!--
        作为框架的核心组件,在启动过程中有大量的初始化操作要做
        而这些操作放在第一次请求时才执行会严重影响访问速度
        因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
    -->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <!--
        设置springMVC的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
        但是/不能匹配.jsp请求路径的请求
    -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

创建 HelloController.java
注意:添加@Controller注解并不会生效,还需要配置组件扫描器

@Controller
public class HelloController {   
}

配置 springMVC.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 组件扫描 -->
    <context:component-scan base-package="top.lukeewin.springmvc.controller"/>

    <!-- 配置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>
</beans>

1.3、测试HelloWorld

实现访问首页

@RequestMapping注解的value属性可以通过请求地址匹配请求,/表示的当前工程的上下文路径
作用:处理请求和控制器方法之间的映射关系
localhost:8080/springMVC/

@RequestMapping("/")
public String index() {
	// 设置视图名称
	return "index";
}

通过超链接跳转到特定页面

<!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="@{/hello}">HelloWorld</a><br/>
</body>
</html>
@RequestMapping("/hello")
public String HelloWorld() {
	return "target";
}

1.4、总结

浏览器发送请求,若请求地址符合前端控制器的url-pattern,该请求就会被前端控制器DispatcherServlet处理。前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器, 将请求地址和控制器中@RequestMapping注解的value属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面。

四、@ResquestMapping

位置:类,方法
作用:将请求和处理请求的方法关联起来

1.1、@RestquestMapping注解的value属性

该属性必须设置
类型:String[],即可以有多个请求路径,满足其一即可
作用:映射请求路劲到对应的控制器中(方法)
当只有value属性时,可以省略value,直接写值即可
当只有一个value值时,不用写{}
如果有多个属性时,不能省略value

@RequestMapping({"/target", "/test"})
public String target() {
   return "target";
}
<a th:href="@{/target}">访问目标页面target.html--->/target</a>
<br/>
<a th:href="@{/test}">访问目标页面target.html--->/test</a>

1.2、@RestquestMapping注解的method属性

作用:指定请求方式

类型:RestquestMethod[] 枚举类型的数组

常用的请求方式有:POST, GET, PUT, DELETE

满足一种请求方式即可

@RequestMapping(value = "/testMethod", method = {RequestMethod.GET, RequestMethod.POST})
public String testMethod() {
    return "success";
}
<a th:href="@{/testMethod}">访问目标页面success.html--->/testMethod</a>

SpringMVC中提供了@RestquestMapping注解的派生注解

@GetMapping:处理get请求

@PostMapping:处理post请求

@PutMapping:处理put请求

@DeleteMapping:处理delete请求

注意:浏览器只支持getpost请求。

post请求可以通过form表达的方式设置。

如果@RestquestMapping没有设置method属性,那么就可以支持任意的请求类型。

form表达中没有设置method属性,那么默认是使用get方式发送请求。

1.3、@RestquestMapping注解的params属性(了解)

类型:String[]

作用:将请求参数传递到控制器中

要求:必须满足所有条件

<a th:href="@{/testParams(username='admin', password=123456)}">测试带参数的请求</a>
<form th:action="@{/testParams}" method="post">
    <input name="username" value="admin">
    <input name="password" value="123456">
    <input type="submit">
</form>
@RequestMapping(value = "/testParams", params = {"username=admin", "password=123456"})
public String testParams() {
    return "success";
}

注意:如果满足valuemethod但是不满足params属性时,报400错误

Parameter conditions "username=admin, password!=123456" not met for actual request parameters: username={admin}, password={123456}

1.4、@RestquestMapping注解的headers属性(了解)

类型:String[]

作用:携带请求头信息

要求:必须满足所有条件

image-20221206014914947

@RequestMapping(value = "/testHeaders", headers = {"host=localhost:8081"})
public String testHeaders() {
    return "success";
}

如果不满足headers属性,则会报404错误。

image-20221206015418945

1.5、@consumes(重点)

位置:声明在@RequestMapping注解内部

作用:指定处理请求的提交内容类型(Content-Type)

前端设置 Content-Type是什么类型,后端就要使用什么类型消费,否则会报415错误

指定 ContentType 必须为 multipart/form-data(即包含文件域)的请求才能接收处理

@RequestMapping(value = "/testConsumes", method = Request.POST, consumes = "multipart/form-data")

指定 ContentType 不能为为 multipart/form-data(即包含文件域)的请求才能接收处理

@RequestMapping(value = "/testConsumes", method = Request.POST, consumes = "!multipart/form-data")

参考文件

1.6、@produces

位置:声明在@RequestMapping注解内部

作用:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回

前端 Accept 设置什么类型,后端 @produces就应该设置什么内容,否则报错

1.7、SpringMVC支持ant风格的路径(了解)

@RequestMapping中的value

?:表示任意单个字符

@RequestMapping("/a?a/testAnt")
public String testAnt() {
	return "success";
}
<a th:href="@{/aba/testHeaders}">访问目标页面success.html--->/?/testAnt</a>

*:表示任意0个或多个字符

**:表示任意层数的任意目录,错误写法/a**a/testAnt,正确写法/**/testAnt

@RequestMapping("/**/testAnt")
public String testAnt() {
    return "success";
}

1.8、SpringMVC支持路径中的占位符(重点)

rest风格

{}相当于占位符

@RequestMapping("/testPath/{username}/{password}")
public String testPath(@PathVariable("username") String username, @PathVariable("password") String password) {
    System.out.println(username + " : " + password);
    return "success";
}
<a th:href="@{/testPath/admin/123456}">testPath</a>

五、获取请求参数

1.1、通过ServletAPI获取参数(不推荐)

@RequestMapping("/paramServletAPI")
public String getParamByServletAPI(HttpServletRequest request) {
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    return "success";
}
<a th:href="@{/paramServletAPI(username='admin', password = 123456)}">测试ServletAPI获取参数</a>

1.2、通过SpringMVC中的方式获取请求参数

1.2.1、通过形参名获取

条件:请求参数名和方法中的形参名一致

当不一致时要使用@ResquestParam注解指定参数名和形参名的关系

<a th:href="@{/testParam(username='admin', password = 123456)}">测试通过形参名获取参数</a>
@RequestMapping("/testParam")
public String getParam(String username, String password) {
    System.out.println(username + " : " + password);
    return "success";
}

当有个多个同名的参数时,可以使用String接收,也可以使用String[]接收

input标签内部的中的name属性值要和控制层中的方法的参数名一致,如果不一致可以使用@RequestParam处理

<form th:action="@{/multiSameParam}" method="post">
    用户名:<input type="text" name="username"> <br/>
    密 码:<input type="password" name="password"> <br/>
    爱 好:<input type="checkbox" name="hobby" value="a"> a
           <input type="checkbox" name="hobby" value="b"> b
           <input type="checkbox" name="hobby" value="c"> c <br/>
           <input type="submit">
</form>
/*@RequestMapping("/multiSameParam")
public String getMultiSameParam(String username, String password, String hobby) {
    System.out.println(username + " : " + password + " : " + hobby);
    return "success";
}*/

@RequestMapping("/multiSameParam")
public String getMultiSameParam(String username, String password, String[] hobby) {
    System.out.println(username + " : " + password + " : " + Arrays.toString(hobby)
    return "success";
}

1.2.2、@RequestParam

请求参数名和形参名不一致时获取参数的情况时可以使用RequestParam

作用:将请求参数名与形参名映射到一起

<form th:action="@{/multiSameParam}" method="post">
    用户名:<input type="text" name="user_name"> <br/>
    密 码:<input type="password" name="password"> <br/>
    爱 好:<input type="checkbox" name="hobby" value="a"> a
           <input type="checkbox" name="hobby" value="b"> b
           <input type="checkbox" name="hobby" value="c"> c <br/>
           <input type="submit">
</form>
@RequestMapping("/multiSameParam")
public String getMultiSameParam(@RequestParam("user_name") String username,
                                String password,
                                String[] hobby) {
    System.out.println(username + " : " + password + " : " + Arrays.toString(hobby));
    return "success";
}

@RequestParam中的属性

namevalue一样的效果

required:默认为true,表示必须传递参数,如果没有传递参数,则报400错误(前提是没有使用defaultValue属性)

required设置为false,表示可传可不传参数都行,都不会报错。如果不传递参数,则会有默认值null

defaultValue:不传请求参数时,会使用默认值

@RequestMapping("/multiSameParam")
public String getMultiSameParam(@RequestParam(value = "user_name", 
                                              required = false, 
                                              defaultValue = "hehe") String username,
                                String password,
                                String[] hobby) {
    System.out.println(username + " : " + password + " : " + Arrays.toString(hobby));
    return "success";
}

1.2.3、@RequestHeader

作用:获取请求头信息

@RequestMapping("/multiSameParam")
public String getMultiSameParam(@RequestParam(value = "user_name", required = false, defaultValue = "hehe") String username,
                                String password,
                                String[] hobby,
                                @RequestHeader("Host") String host) {
    System.out.println(username + " : " + password + " : " + Arrays.toString(hobby));
    System.out.println(host);
    return "success";
}

1.2.4、@CookieValue

作用:获取请求中的Cookie信息

@RequestMapping("/multiSameParam")
public String getMultiSameParam(@RequestParam(value = "user_name", required = false, defaultValue = "hehe") String username,
                                String password,
                                String[] hobby,
                                @RequestHeader("Host") String host,
                                @CookieValue("JSESSIONID") String JSESSIONID) {
    System.out.println(username + " : " + password + " : " + Arrays.toString(hobby));
    System.out.println(host);
    System.out.println(JSESSIONID);
    return "success";
}

1.2.5、使用POJO方式接收请求参数

作用:通过实体类获取请求参数

要求:表达中的name的值要和实体类中的属性名一致

<form th:action="@{/pojo}" method="post">
    用户名:<input type="text" name="username"> <br/>
    密 码:<input type="password" name="password"> <br/>
    性 别:<input type="radio" name="sex" value="男">
           <input type="radio" name="sex" value="女"> <br/>
    年 龄:<input type="text" name="age"> <br/>
    邮 箱:<input type="email" name="email"> <br/>
    <input type="submit">
</form>
@RequestMapping("/pojo")
public String testPojo(User user) {
    System.out.println(user);
    return "success";
}

1.2.5.1、解决中文乱码问题

位置:web.xml

注意:可以配置多个过滤器,但是编码过滤器一定要配置在其它的过滤器之前

<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>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

参考文件

六、域对象共享数据

作用:在不同页面中实现数据的共享

三个域:request域、session域、application域

1.1、基于ServletAPI中的request域实现数据共享(不推荐)

@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request) {
    request.setAttribute("testScope", "ServletAPI");
    return "test_scope";
}
<p th:text="${testScope}"></p>

1.2、使用ModelAndView向request域对象共享数据

不管用什么方式设置域数据,最终在 SpringMVC 的底层都会被封装成一个 ModelAndView,所以直接用 ModelAndView 即可

位置:返回值类型

@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
    /**
     * ModelAndView有Model和View的功能
     * Model主要用于向请求域共享数据
     * View主要用于设置视图,实现页面跳转
     */
    ModelAndView mav = new ModelAndView();
    //向请求域共享数据
    mav.addObject("testScope", "hello,ModelAndView");
    //设置视图,实现页面跳转
    mav.setViewName("test_scope");
    return mav;
}

1.3、使用Model向request域中共享数据

位置:形参

@RequestMapping("/testModel")
public String testModel(Model model) {
    model.addAttribute("testScope", "hello,Model");
    return "test_scope";
}

1.4、使用Map向request域对象共享数据

位置:形参

@RequestMapping("/testMap")
public String testMap(Map<String, Object> map) {
    map.put("testScope", "hello,Map");
    return "test_scope";
}

1.5、使用ModelMap向request域对象共享数据

位置:形参

@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap) {
    modelMap.addAttribute("testScope", "hello,ModelMap");
    return "test_scope";
}

1.6、Model、Map、ModelMap三者之间的联系

这三者都是由BindingAwareModelMap实例化的

最终都会封装到ModelAndView

public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}

1.7、基于session域共享数据

<p th:text="${session.testScope}"></p>
@RequestMapping("/testSession")
public String testSession(HttpSession session) {
    session.setAttribute("testScope", "hello,Session");
    return "test_scope";
}

1.8、基于application域共享数据

ServletContext即是application

有三种方式获取ServletContext

request.getServletContext()

session.getServletContext()

servletConfig.getServletContext()

<p th:text="${application.testScope}"></p>
@RequestMapping("/testApplication")
public String testApplication(HttpSession session) {
    ServletContext application = session.getServletContext();
    application.setAttribute("testScope", "hello,Application");
    return "test_scope";
}

七、SpringMVC视图

1.1、ThymeleafView

只要控制器中的方法返回值没有forwardredirect前缀的时候,创建的都是ThymeleafView视图

@RequestMapping("/testHello")
public String testHello(){
	return "hello";
}

1.2、InternalResourceView

作用:转发

发生在服务器内部,浏览器中的 url 不改变

前缀forward:

创建的是InternalResourceView

@RequestMapping("/testForward")
public String testForward() {
    return "forward:/testThymeleafView";
}

image-20221210195119457

1.3、RedirectView

作用:重定向

发生在浏览器,浏览器重新发送请求,url 改变

前缀redirect:

创建的是RedirectView

重定向填的不是页面,应该填的是一个请求,因为浏览器不能直接访问页面,必须经过ThymeleafView

@RequestMapping("/testRedirect")
public String testRedirect() {
    return "redirect:/testThymeleafView";
}

1.4、配置view-controller

位置:SpringMVC.xml

注意:

  1. 添加了<mvc:view-controller>后,所有控制器方法将失效,必须要配置<mvc:annotation-driver>才能让所有的控制器方法生效。
  2. 需要把控制器中的对应方法注释掉。
<mvc:view-controller path="/" view-name="index"/>
<mvc:annotation-driven/>

1.5、配置InternalResourceView

配置视图解析器

SpringMVC.xml

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/templates/"/>
    <property name="suffix" value=".jsp"/>
</bean>

index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>index</title>
</head>
<body>
    <h1>首页</h1>
    <a href="${pageContext.request.contextPath}/testJsp">测试jsp</a>
</body>
</html>

注意:index.jsp要在webapp目录下才能直接访问。

image-20221210210403256

八、RESTFul

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

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

image-20221211011207226

<a th:href="@{/user}">查询所有用户信息</a> <br/>
<a th:href="@{/user/1}">根据用户 id 查询用户信息</a> <br/>
<form th:action="@{/user}" method="post">
    用户名:<input type="text" name="username"> <br/>
    密 码:<input type="password" name="password"> <br/>
    <input type="submit" value="添加用户信息">
</form>
// 查询
@GetMapping("/user")
public String getAllUsers() {
    System.out.println("查询所有用户信息");
    return "success";
}

// 查询
@GetMapping("/user/{id}")
public String getUser(@PathVariable("id") Integer id) {
    System.out.println("查询用户 id 为" + id + "的用户信息");
    return "success";
}

// 添加
@PostMapping("/user")
public String insertUser(String username, String password) {
    System.out.println("添加用户信息" + username + "," + password);
    return "success";
}

// 修改
@PutMapping("/user")
public String modifyUser() {
    System.out.println("修改用户信息");
    return "success";
}

// 删除
@DeleteMapping("/user/{id}")
public String deleteUser(@PathVariable("id") Integer id) {
    System.out.println("删除用户 id 为" + id + "的用户信息");
    return "success";
}

1.1、HiddentHttpMethodFilter

作用:由于前端 form 表单中只能发送 get 或 post 请求,不能发送 put 和 delete 请求,所以需要配置 HiddentHttpMethodFilter

位置:web.xml(在设置字符集之后)

前端表单中设置<input type="hidden" name="_method" value="put">

<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>
<form th:action="@{/user}" method="post">
    <input type="hidden" name="_method" value="put">
    用户名:<input type="text" name="username"> <br/>
    密 码:<input type="password" name="password"> <br/>
    <input type="submit" value="修改用户信息">
</form>
@PutMapping("/user")
public String modifyUser(String username, String password) {
    System.out.println("修改用户信息" + username + "," + password);
    return "success";
}

1.1.1、HiddentHttpMethodFilter源码分析

需要满足两个条件:

  1. 前端表单以post方式提交。
  2. 表单中包含_method参数和参数对应的值
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {

    HttpServletRequest requestToUse = request;

    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            if (ALLOWED_METHODS.contains(method)) {
                requestToUse = new HttpMethodRequestWrapper(request, method);
            }
        }
    }

    filterChain.doFilter(requestToUse, response);
}

1.2、案例

实现简单的增删改查功能

创建实体类

public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    //1 male, 0 female
    private Integer gender;
    // 省略空参构造,全参构造,setter,getter
}

创建Dao

@Repository
public class EmployeeDao {

    private static Map<Integer, Employee> employees = null;

    static{
        employees = new HashMap<Integer, Employee>();
        employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1));
        employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1));
        employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0));
        employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0));
        employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1));
    }

    private static Integer initId = 1006;

    public void save(Employee employee) {
        // id 为 null 时,添加员工
        if (employee.getId() == null) {
            employee.setId(initId++);
        }
        // id 不为 null 时,修改员工
        employees.put(employee.getId(), employee);
    }

    public Collection<Employee> getAll(){
        return employees.values();
    }

    public Employee get(Integer id){
        return employees.get(id);
    }

    public void delete(Integer id){
        employees.remove(id);
    }
}

SpringMVC.xml中配置视图控制器(也可以在控制层中配置)

必须要开启注解驱动,否则,不会把请求交给DispatcherServlet处理

<mvc:view-controller path="/" view-name="index"/>
<mvc:annotation-driven/>

1.2.1、实现查询功能

控制器

请求方式:get

通过Model把后端中的数据共享到前端页面中

@Controller
public class EmployeeController {
    @Autowired
    private EmployeeDao employeeDao;

    @GetMapping("/employee")
    public String getAllEmployee(Model model) {
        Collection<Employee> employeeList = employeeDao.getAll();
        model.addAttribute("employeeList", employeeList);
        return "employee_list";
    }
}

前端:首页

点击查看员工信息链接后跳转到员工信息展示页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <a th:href="@{/employee}">查看员工信息</a>
</body>
</html>

前端:员工信息展示页

先创建表格,用于显示信息

注意:这里使用th:each进行循环遍历

<table id="dataTable" border="1" cellspacing="0" cellpadding="0" style="text-align: center">
    <tr>
        <th colspan="5">Employee Info</th>
    </tr>
    <tr>
        <th>id</th>
        <th>lastName</th>
        <th>email</th>
        <th>gender</th>
        <th>options</th>
    </tr>
    <tr th:each="employee : ${employeeList}">
        <td th:text="${employee.id}"></td>
        <td th:text="${employee.lastName}"></td>
        <td th:text="${employee.email}"></td>
        <td th:text="${employee.gender}"></td>
        <td>
            <a href="">delete</a>
            <a href="">update</a>
        </td>
    </tr>
</table>

1.2.2、实现删除功能

有两种方式

<!-- 方式一 -->
<!--<a th:href="@{/employee/} + ${employee.id}">delete</a>-->
<!-- 方式二 -->
<a @click="deleteEmployee" th:href="@{'/employee/' + ${employee.id}}">delete</a>

引入vue.js

编写js代码

<script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
<script type="text/javascript">
    var vue = new Vue({
        el:"#dataTable",
        methods:{
            deleteEmployee:function (event) {
                // 根据 id 获取表单元素
                var deleteForm = document.getElementById("deleteForm");
                // 把当前事件的超链接中的 href 属性赋值给表单的 action 属性
                deleteForm.action = event.target.href;
                // 提交表单
                deleteForm.submit();
                // 阻止超链接的默认行为,即阻止超链接的跳转行为
                event.preventDefault();
            }
        }
    });
</script>

添加一个form表单

<form id="deleteForm" method="post">
    <input type="hidden" name="_method" value="delete">
</form>

控制器

请求方式:delete

注意:这里使用的是重定向,而不是直接转发

使用重定向的原因:我们删除员工信息后需要重新返回到展示页面,不能使用employee_list,因为我们要携带后端的数据到前端显示,总不能在删除的控制器方法中再写一次从数据库查询所有数据的代码了,所以这里应该使用重定向,即让浏览器再发生一次查询请求

@DeleteMapping("/employee/{id}")
public String deleteEmployee(@PathVariable("id") Integer id) {
    employeeDao.delete(id);
    return "redirect:/employee";
}

SpringMVC.xml中开放对静态资源的访问

注意:如果不开放,那么所有静态资源都会交给DispatchServlet处理,而DispatchServlet是不能处理vue.js的,所以会导致报错,同时还必须把<mvc:annotation-driven/>添加上,否则会把所有的请求都交给DefaultSerlvet处理

<mvc:default-servlet-handler/>

1.2.3、实现更新功能

在展示页面中添加如下内容

<a th:href="@{'/employee/' + ${employee.id}}">update</a>

控制器

作用:回显要更新的数据到更新页面中

请求方式:get

@GetMapping("/employee/{id}")
public String getEmployeeById(@PathVariable("id") Integer id, Model model) {
    Employee employee = employeeDao.get(id);
    model.addAttribute("employee", employee);
    return "employee_update";
}

控制器

作用:更新员工信息

请求方式:put

@PutMapping("/employee")
public String updateEmployee(Employee employee) {
    employeeDao.save(employee);
    return "redirect:/employee";
}

创建一个页面来显示要更新的内容

注意:

  1. 要记得把id也写上,即<input type="hidden" name="id" th:value="${employee.id}">
  2. <input type="hidden" name="_method" value="put">
  3. value="1" th:field="${employee.gender}"表示当th:field="${employee.gender}"value的值相等时,就会添加checked=checked,即表示被选中状态
<form th:action="@{/employee}" method="post">
    <input type="hidden" name="_method" value="put">
    <input type="hidden" name="id" th:value="${employee.id}">
    lastName: <input type="text" name="lastName" th:value="${employee.lastName}"> <br/>
    email: <input type="text" name="email" th:value="${employee.email}"> <br/>
    gender: <input type="radio" name="gender" value="1" th:field="${employee.gender}">male
    <input type="radio" name="gender" value="0" th:field="${employee.gender}">female <br/>
    <input type="submit" value="update">
</form>

1.2.4、实现添加功能

创建添加页面

<form th:action="@{/employee}" method="post">
    lastName: <input type="text" name="lastName"> <br/>
    email: <input type="text" name="email"> <br/>
    gender: <input type="radio" name="gender" value="1">male
    <input type="radio" name="gender" value="0">female <br/>
    <input type="submit" value="add">
</form>

添加视图控制器

作用:跳转到添加员工页面

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

控制器

请求方式:post

@PostMapping("/employee")
public String addEmployee(Employee employee) {
    employeeDao.save(employee);
    return "redirect:/employee";
}

1.2.5、完整代码

前端完整代码

展示员工信息页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Employee Info</title>
</head>
<body>
    <table id="dataTable" border="1" cellspacing="0" cellpadding="0" style="text-align: center">
        <tr>
            <th colspan="5">Employee Info</th>
        </tr>
        <tr>
            <th>id</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>options(<a th:href="@{/toAdd}">add</a>)</th>
        </tr>
        <tr th:each="employee : ${employeeList}">
            <td th:text="${employee.id}"></td>
            <td th:text="${employee.lastName}"></td>
            <td th:text="${employee.email}"></td>
            <td th:text="${employee.gender}"></td>
            <td>
                <!-- 方式一 -->
                <!--<a th:href="@{/employee/} + ${employee.id}">delete</a>-->
                <!-- 方式二 -->
                <a @click="deleteEmployee" th:href="@{'/employee/' + ${employee.id}}">delete</a>
                <a th:href="@{'/employee/' + ${employee.id}}">update</a>
            </td>
        </tr>
    </table>

    <form id="deleteForm" method="post">
        <input type="hidden" name="_method" value="delete">
    </form>

    <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
    <script type="text/javascript">
        var vue = new Vue({
            el:"#dataTable",
            methods:{
                deleteEmployee:function (event) {
                    // 根据id获取表单元素
                    var deleteForm = document.getElementById("deleteForm");
                    // 把当前事件的超链接中的href属性赋值给表单的action属性
                    deleteForm.action = event.target.href;
                    // 提交表单
                    deleteForm.submit();
                    // 取消 a 标签的默认行为
                    event.preventDefault();
                }
            }
        });
    </script>
</body>
</html>

更新员工信息页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>update employee</title>
</head>
<body>
    <form th:action="@{/employee}" method="post">
        <input type="hidden" name="_method" value="put">
        <input type="hidden" name="id" th:value="${employee.id}">
        lastName: <input type="text" name="lastName" th:value="${employee.lastName}"> <br/>
        email: <input type="text" name="email" th:value="${employee.email}"> <br/>
        gender: <input type="radio" name="gender" value="1" th:field="${employee.gender}">male
        <input type="radio" name="gender" value="0" th:field="${employee.gender}">female <br/>
        <input type="submit" value="update">
    </form>
</body>
</html>

添加员工信息页面

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>add employee</title>
</head>
<body>
    <form th:action="@{/employee}" method="post">
        lastName: <input type="text" name="lastName"> <br/>
        email: <input type="text" name="email"> <br/>
        gender: <input type="radio" name="gender" value="1">male
        <input type="radio" name="gender" value="0">female <br/>
        <input type="submit" value="add">
    </form>
</body>
</html>

后端控制器层完整代码

@Controller
public class EmployeeController {

    @Autowired
    private EmployeeDao employeeDao;

    @GetMapping("/employee")
    public String getAllEmployee(Model model) {
        Collection<Employee> employeeList = employeeDao.getAll();
        model.addAttribute("employeeList", employeeList);
        return "employee_list";
    }

    @DeleteMapping("/employee/{id}")
    public String deleteEmployee(@PathVariable("id") Integer id) {
        employeeDao.delete(id);
        return "redirect:/employee";
    }

    @PostMapping("/employee")
    public String addEmployee(Employee employee) {
        employeeDao.save(employee);
        return "redirect:/employee";
    }

    @GetMapping("/employee/{id}")
    public String getEmployeeById(@PathVariable("id") Integer id, Model model) {
        Employee employee = employeeDao.get(id);
        model.addAttribute("employee", employee);
        return "employee_update";
    }

    @PutMapping("/employee")
    public String updateEmployee(Employee employee) {
        employeeDao.save(employee);
        return "redirect:/employee";
    }
}

九、HttpMessageConverter

提供了两个注解和两个类型:@RequestBody@ResponseBodyRequestEntityResponseEntity

image-20221214155440390

1.1、@RequestBody

位置:控制器方法形参中

作用:获取浏览器发送过来的请求体信息,把json对象转为Java对象

@PostMapping("/testRequestBody")
public String testRequestBody(@RequestBody String requestBody) {
    System.out.println(requestBody);
    return "success";
}

输出结果

username=lukeewin&password=123

1.2、RequestEntity

位置:控制器方法形参中

作用:获取完整的请求信息,包括请求头和请求体

@PostMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> requestEntity) {
    System.out.println("请求头:" + requestEntity.getHeaders());
    System.out.println("请求体:" + requestEntity.getBody());
    return "success";
}

1.3、@ResponseBody

位置:控制器方法的上面

作用:响应浏览器的响应体,需要搭配json的相关jar实现响应json数据到浏览器的效果,添加该注解后会自动将Java对象转为json字符串,然后传输到前端中。

注意:添加该注解后,不会转发或重定向效果,而是直接把数据转为json字符串后传递到前端。

控制器

@GetMapping("/testResponseBody")
@ResponseBody
public String testResponseBody() {
    return "hello,responseBody";
}

运行效果

image-20221214161155895

扩展:使用原生的ServletAPI也可以实现一样的功能

@GetMapping("/testResponse")
public void testResponse(HttpServletResponse response) throws IOException {
    response.getWriter().print("hello,response");
}

如果要给前端返回一个json数据(注意:不能直接返回json对象,因为json对象是JavaScript中的对象,在Java中返回的是json字符串),那么必须添加json的依赖。

注意:如果返回值为User,Map对象,那么前端接收到的是json对象,如果返回的是List,那么前端接收到的是json数组。(json数据就两种类型,一种是json对象,一种是json数组,json对象使用大括号括起来,json数组使用中括号括起来)

添加处理json数据的依赖

<!-- jackson -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.1</version>
</dependency>

控制器

@GetMapping("/testResponseUser")
@ResponseBody
public User testResponseUser() {
    return new User(1001, "admin", "123456", 23, "男");
}

运行效果

{"id":1001,"username":"admin","password":"123456","age":23,"sex":"男"}

1.3.1、前端使用axios传递数据

注意:引入vue.jsaxios.js后要记得重新打包,否则在target下找不到这两个js文件,就会报错。

image-20221214163357058
<div id="app">
    <a @click="testAxios" th:href="@{/testAxios}">SpringMVC处理Ajax</a>
</div>
<script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
<script type="text/javascript" th:src="@{/static/js/axios.js}"></script>
<script type="text/javascript">
    new Vue({
    el:"#app",
    methods:{
        testAxios:function (event) {
            axios({
                method:"post",
                url:event.target.href,
                params:{
                    username:"admin",
                    password:"123456"
                }
            }).then(function (response) {
                alert(response.data);
            });
            event.preventDefault();
        }
    }
});
</script>
@RequestMapping("/testAxios")
@ResponseBody
public String testAxios(String username, String password) {
    System.out.println(username + " : " + password);
    return "hello,axios";
}

1.4、ResponseEntity

位置:方法返回值

作用:自定义一个响应前端的响应实体,包括响应头和响应体

应用:文件下载

在做文件下载时,需要重新打包,要下载的文件必须要出现在target的子目录下。

下载的真实路径:D:\Works\IdeaProjects\SpringMVC-demo01\springmvc-demo3\target\springmvc-demo3-1.0-SNAPSHOT\static\img\1.jpg

@RequestMapping("/testDownload")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    // 获取 ServletContext 对象
    ServletContext servletContext = session.getServletContext();
    // 获取服务器中文件的真实路径
    String realPath = servletContext.getRealPath("/static/img/1.jpg");
    // 创建输入流
    InputStream is = new FileInputStream(realPath);
    // 创建字节数组
    byte[] bytes = new byte[is.available()];
    // 将流读到字节数组中
    is.read(bytes);
    // 创建 HttpHeaders 对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    // 设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=1.jpg");
    // 设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    // 创建 ResponseEntity 对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
    // 关闭输入流
    is.close();
    return responseEntity;
}

1.5、@RestController

@RestController=@ResponseBody+@Controller

1.6、文件上传

注意:文件下载不用导入其它依赖,但是文件下载需要导入其它依赖

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

前端

<form th:action="@{/testUp}" method="post" enctype="multipart/form-data">
    头像:<input type="file" name="photo"> <br/>
    <input type="submit" value="上传">
</form>

控制器

将上传的文件封装到MultipartFile

@RequestMapping("/testUp")
public String testUp(MultipartFile photo) {
    System.out.println(photo.getName()); // 获取的是input标签中的name的属性值
    System.out.println(photo.getOriginalFilename()); // 获取的是文件的原始名称
    return "success";
}

必须在SpringMVC.xml中配置文件上传解析器,并且是根据id来装载的,并且id的值只能是multipartResolver

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

运行结果

photo
java异常处理分析.png

文件上传功能的完整代码

@RequestMapping("/testUp")
public String testUp(MultipartFile photo, HttpSession session) throws IOException {
    // 获取上传文件名
    String fileName = photo.getOriginalFilename();
    // 获取文件的后缀
    String suffixName = fileName.substring(fileName.lastIndexOf("."));
    // UUID 作为文件名
    String uuid = UUID.randomUUID().toString().replaceAll("-", "");
    // 将 UUID 和 文件后缀进行拼接
    fileName = uuid + suffixName;
    // 通过 ServletContext 获取服务器中的 photo 目录的路径
    ServletContext servletContext = session.getServletContext();
    String photoPath = servletContext.getRealPath("photo");
    File file = new File(photoPath);
    // 判断是否存在 photo 目录
    if (!file.exists()) {
        // 如果不存在,则创建
        file.mkdir();
    }
    String finalPath = photoPath + File.separator + fileName;
    photo.transferTo(new File(finalPath));
    return "success";
}

十、拦截器

过滤器和拦截器的区别:

  1. 过滤器时在DispatcherServlet执行之前执行。
  2. 拦截器是在DispatcherServlet执行之后执行。
  3. 拦截器是在控制器方法执行前后执行、或渲染完毕之后执行。

拦截器分为前置拦截器,后置拦截器和最终拦截器,分别在控制器方法之前执行,控制器方法之后执行,渲染完毕之后执行。

image-20221214180943598

image-20221214182039619 image-20221214182120893 image-20221214182302908

1.1、自定义一个拦截器

需要实现HandlerInterceptor接口,同时需要实现以下的三个方法。

preHandle:返回值为true表示放行,返回值为false表示拦截。

这三个方法的执行顺序:preHandler --> postHandler --> afterCompletion

public class FirstInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("FirstInterceptor-->preHandler");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("FirstInterceptor-->postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("FirstInterceptor-->afterCompletion");
    }
}

SpringMVC.xml中配置拦截器

注意:该配置会拦截所有请求

在使用<ref bean="firstInterceptor"/>时,必须要把自定义的拦截器交给Spring IOC容器,可以在自定义的拦截器类上添加@Component

<!-- 配置拦截器 -->
<mvc:interceptors>
    <!-- 对所有请求进行拦截 -->
    <bean class="top.lukeewin.spring.interceptors.FirstInterceptor"/>
</mvc:interceptors>

运行结果

FirstInterceptor-->preHandler
FirstInterceptor-->postHandle
FirstInterceptor-->afterCompletion

不拦截所有请求

注意:/*只能拦截一层目录,例如:http://localhost:8080/springmvc/testInterceptor可以被拦截,但是http:localhost:8080/springmvc/a/testInterceptor不能被拦截。(/springmvc是应用上下文)

<!-- 配置拦截器 -->
<mvc:interceptors>
    <mvc:interceptor>
        <!-- /*只能拦截一层目录 /** 才能拦截多层目录 -->
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/"/>
        <ref bean="firstInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

1.2、拦截器的执行顺序

preHandler:控制器方法执行之前执行,返回值类型为false表示不放行,为true表示放行,放行即调用控制器方法。

postHandler:控制器方法执行之后执行。

afterCompletion:处理完视图和模型数据,渲染视图完成之后执行。

1.3、多个拦截器之间的执行顺序

与配置文件配置拦截器的先后顺序有关

<mvc:interceptors>
    <ref bean="firstInterceptor"/>
    <ref bean="secondInterceptor"/>
</mvc:interceptors>
@Component
public class FirstInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("FirstInterceptor-->preHandler");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("FirstInterceptor-->postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("FirstInterceptor-->afterCompletion");
    }
}
@Component
public class SecondInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("SecondInterceptor-->preHandler");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("SecondInterceptor-->postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("SecondInterceptor-->afterCompletion");
    }
}

运行结果

FirstInterceptor-->preHandler
SecondInterceptor-->preHandler
SecondInterceptor-->postHandle
FirstInterceptor-->postHandle
SecondInterceptor-->afterCompletion
FirstInterceptor-->afterCompletion

如果SecondInterceptor中的preHandler方法返回false,那么运行结果如下。

FirstInterceptor-->preHandler
SecondInterceptor-->preHandler
FirstInterceptor-->afterCompletion

1.3.1、结论

  1. 若每个拦截器的preHandle()都返回true此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关。
    • preHandle()会按照配置的顺序执行,而postHandle()和afterCompletion()会按照配置的反序执行。
  2. 若某个拦截器的preHandle()返回了false。
    • preHandler()返回false的拦截器和它之前的所有拦截器的preHandler()都会执行,所有拦截器中的postHandler()都不会被执行,preHandler()返回false之前的所有拦截器的afterCompletion()会被执行。

十一、异常处理器

有两个关键的类SimpleMappingExceptionResolverDefaultHandlerExceptionResolver,这两个类都间接实现了HandlerExceptionResolver接口。

DefaultHandlerExceptionResolver:是SpringMVC处理异常的默认行为。

SimpleMappingExceptionResolver:可以自定义异常行为。

1.1、基于配置方式的异常处理器

位置:SpringMVC.xml

Properties对象的形式:其中key表示出现的异常,value表示出现异常后要跳转的页面。

<!-- 配置映射异常解析器 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <!-- 将异常信息存储到 ex 中,通过请求域传递到前端(ex 是请求域中的 key ,异常信息是请求域中的 value) -->
    <property name="exceptionAttribute" value="ex"/>
</bean>

控制器

@GetMapping("/testExceptionHandler")
public String testExceptionHandler() {
    System.out.println(1/0);
    return "success";
}

前端

<h1>error</h1>
<p th:text="${ex}"></p>

运行结果

image-20221215002845305

1.2、基于注解方式的异常处理

必须添加@ControllerAdvice@ExceptionHandler

@ExceptionHandler指定发生哪些异常时调用该方法

要想获取异常信息,可以声明Exception类型的形参,然后可以通过请求域共享到前端。

@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler({ArithmeticException.class, NullPointerException.class})
    public String testException(Exception ex, Model model) {
        model.addAttribute("ex", ex);
        return "error";
    }
}

1.3、基于配置和注解两个方式之间的区别

视图控制器抛出的异常,只能用 基于XML的异常处理方式。

image-20221215004729908

十二、注解配置SpringMVC

Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。

编写WebInit.java代替web.xml

public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 指定 Spring 的配置类
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    /**
     * 指定 SpringMVC 的配置类
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    /**
     * 指定 DispatcherServlet 的映射规则,即 url-pattern
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 注册过滤器
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceResponseEncoding(true);
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, methodFilter};
    }
}

编写WebConfig.java代替SpringMVC.xml

// 将当前类标识为一个配置类
@Configuration
// 组件扫描
@ComponentScan("top.lukeewin.springmvc.controller")
// mvc 注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // default-servlet-handler
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    // 拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        TestInterceptor testInterceptor = new TestInterceptor();
        registry.addInterceptor(testInterceptor).addPathPatterns("/**");
    }

    // view-controller
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/hello").setViewName("hello");
    }

    // 文件上传解析器
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
        return commonsMultipartResolver;
    }

    // 异常处理
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties prop = new Properties();
        prop.setProperty("java.lang.ArithmeticException", "error");
        exceptionResolver.setExceptionMappings(prop);
        exceptionResolver.setExceptionAttribute("exception");
        resolvers.add(exceptionResolver);
    }

    // 配置生成模板解析器
    @Bean
    public ITemplateResolver templateResolver() {
        WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
        // ServletContextTemplateResolver 需要一个 ServletContext 作为构造参数,可通过 WebApplicationContext 的方法获得
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(webApplicationContext.getServletContext());
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setCharacterEncoding("UTF-8");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        return templateResolver;
    }

    // 生成模板引擎并为模板引擎注入模板解析器
    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    // 生成视图解析器并未解析器注入模板引擎
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }
}

十三、SpringMVC的执行流程

1.1、SpringMVC常用组件

DispatcherServlet:调度器

作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求。

HandlerMapping:处理器映射器

作用:根据请求中的ulr,method等信息查找Handler。

Handler:处理器(控制器方法)

作用:在DispatcherServlet的控制下,Handler对具体的用户请求进行处理。

HandlerAdapter:处理器适配器

作用:通过处理器适配器对处理器(控制器方法)进行执行。

ViewResovler:视图解析器

作用:进行视图解析,得到相应的视图,例如:ThymeleafView,InternalResourceView,RedirectView。

View:视图

作用:将模型数据通过页面展示给用户。

1.2、DispatcherServlet初始化过程

image-20221216024933163

初始化 WebApplicationContext

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

    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        synchronized (this.onRefreshMonitor) {
            // 刷新 WebApplicationContext
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        // 将 IOC 容器在应用域共享
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

创建 WebApplicationContext

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

DispatcherServlet 初始化策略

FrameworkServlet 创建 WebApplicationContext 后,刷新容器,调用 onRefresh(wac),此方法在 DispatcherServlet 中进行了重写,调用了 initStrategies(context) 方法,初始化策略,即初始化 DispatcherServlet 的各个组件

image-20221216025736695

1.3、DispatcherServlet服务请求过程

image-20221217020227729

1.4、SpringMVC流程分析

  1. 浏览器发送请求到服务器中的DispatcherServlet中。
  2. DispatcherServlet对请求中的URL进行处理,得到URI,判断URI对应的映射。
    • 如果没有找到映射,那么再去判断是否配置了mvc:default-servlet-handler
      • 如果没配置,则报错404。
      • 如果配置了,则访问这些资源(静态资源)。
    • 如果找到了映射,则执行以下流程:
      • 根据URI去调用HandlerMapping来获取相关的Handler配置的所有相关的对象,比如Handler,以及Handler对应的拦截器,最后以HandlerExecutionChain的方式返回。
      • DispatcherServlet获取到Handler后,会选择一个合适的HandlerAdapter。
      • HandlerAdapter会去调用相应的Handler,但是在调用Handler执行会先去调用拦截器preHandle(…)法。
      • 在调用Handler过程中,HandlerAdapter会做一些处理,比如数据类型转换,把请求中的参数值赋值给控制器方法的形参中。
      • 调用完Handler后,会返回一个ModelAndView对象。
      • 执行完Handler之后,会去执行拦截器postHandle(…)方法。
      • 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图。
      • 渲染视图完毕执行拦截器的afterCompletion(…)方法。
      • 将渲染结果返回给浏览器。

Q.E.D.


热爱生活,热爱程序