Spring MVC 学习

Spring MVC 学习

1 Spring MVC 简介

1.1 MVC 概念

MVC 是一种软件架构的思想,将软件按照模型、视图、控制器来划分

image-20230513171452879

M:Model,模型层,指工程中的 JavaBean,作用是处理数据

JavaBean 分为两类:

  • 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等
  • 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。

V:View,视图层,指工程中的 html 或 jsp 等页面,作用是与用户进行交互,展示数据

C:Controller,控制层,指工程中的 servlet,作用是接收请求和响应浏览器

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

1.2 Spring MVC 概念

Spring MVC 是 Spring 的一个后续产品,是 Spring 的一个子项目,基于原生的 Servlet,是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 Spring MVC 作为 Java EE 项目表述层开发的首选方案

1.3 配置 MVC 的 web.xml

1.3.1 默认配置

在WEB-INF 下添加 web.xml 配置文件,此配置作用下 Spring MVC 的配置文件默认位于 WEB-INF 下,默认名称为 <servlet-name>-servlet.xml,例如,以下配置所对应 Spring MVC 的配置文件位于 WEB-INF 下,文件名为 springMVC-servlet.xml

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

自定义 Spring MVC 配置文件的位置和名称

<!-- 配置 Spring MVC 的前端控制器,对浏览器发送的请求统一进行处理 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 通过初始化参数指定 Spring MVC 配置文件的位置和名称 -->
    <init-param>
        <!-- contextConfigLocation为 固定值 -->
        <param-name>contextConfigLocation</param-name>
        <!-- 使用 classpath: 表示从类路径查找配置文件,例如 maven 工程中的 src/main/resources -->
        <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>
    <!--
        设置 Spring MVC 的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是 /login 或 .html 或.js或 .css 方式的请求路径
        但是/不能匹配 .jsp 请求路径的请求,原因是 jsp 的本质是一个 servlet,不需要我们处理
    -->
    <url-pattern>/</url-pattern>
</servlet-mapping>
1.3.3 springMVC.xml 配置文件
<!-- 自动扫描包 -->
<context:component-scan base-package="com.oizys.mvc.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>

<!-- 
   处理静态资源,例如html、js、css、jpg
  若只设置该标签,则只能访问静态资源,其他请求则无法访问
  此时必须设置<mvc:annotation-driven/>解决问题
 -->
<mvc:default-servlet-handler/>

<!-- 开启mvc注解驱动 -->
<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- 处理响应中文内容乱码 -->
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="defaultCharset" value="UTF-8" />
            <property name="supportedMediaTypes">
                <list>
                    <value>text/html</value>
                    <value>application/json</value>
                </list>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>
1.3.4 总结

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

2 基础功能

2.1 @requestMapping 注解

2.1.1 注解的作用

@RequestMapping标识一个类:设置映射请求的请求路径的前缀信息

@RequestMapping标识一个方法:设置映射请求请求路径的具体信息

2.1.2 注解的重要属性
  • value:指定映射请求的请求路径

  • method:指定请求提交的方式,常用的请求方式有 get,post,put,delete

  • params:规定请求携带的参数(以 uname 为例)

    • uname:必须携带 uname 参数,但值随意
    • !uname:必须不携带 uname 参数
    • uname = admin:必须携带 uname 参数,且值一定为 admin
    • uname != admin:必须携带 uname 参数,且值一定不为 admin
  • headers:

    • header:要求请求映射所匹配的请求必须携带 header 请求头信息

    • !header:要求请求映射所匹配的请求必须不能携带 header 请求头信息

    • header=value:要求请求映射所匹配的请求必须携带 header 请求头信息且值一定为 value

    • header!=value:要求请求映射所匹配的请求必须携带 header 请求头信息且值一定不为 value

2.1.3 模糊匹配和占位符
模糊匹配

?:表示任意的单个字符

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

**:表示任意的一层或多层目录

注意:

  • 在使用 ** 时,只能使用 /**/xxx 的方式,例如 /a**a/ 中 * 不表示模糊匹配,而只是一个字符
  • 不可匹配 / 等有特殊意义的字符
占位符

使用 {} 表示占位

Spring MVC 路径中的占位符常用于 RESTful 风格中,通过 @PathVariable 注解,将占位符所表示的数据赋值给控制器方法的形参

2.2 获取请求参数

2.2.1 通过 ServletAPI 获取

前端通过 form 表单提交数据,此时我们就可以使用 HttpServletRequest 通过参数名来获取请求参数

  • getParameter 只能使用在没有同名参数的时候
  • getParameterValues 可以接收同名参数
// 前端请求:**/testParam?username=admin&password=123456
@RequestMapping("/testParam")
public String testParam(HttpServletRequest request){
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    System.out.println("username:"+username+",password:"+password);
    return "success";
}
2.2.2 通过控制器方法的形参获取请求参数

所有前端发过来的请求都先由 DispatcherServlet 处理进行匹配,找到相应的 controller 的方法根据它的参数将值注入,再转由相应的 controller 方法进行处理

// 前端请求:**/testParam?username=admin&password=123456
@RequestMapping("/testParam")
public String testParam(String username, String password){
    System.out.println("username:"+username+",password:"+password);
    return "success";
}
2.2.3 使用 @RequestParam 注解

在我们使用形参获取请求参数时,如果形参的名字和请求参数名不一致,我们可以使用 @RequestParam 注解

@RequestParam 注解一共有三个属性:

  • value:指定为形参赋值的请求参数的参数名

  • required:设置是否必须传输此请求参数,默认值为 true

  • defaultValue:当 value 所指定的请求参数没有传输或传输的值为 “” 时,使用默认值为形参赋值

// 前端请求:**/testParam?username=admin&password=123456
@RequestMapping("/testParam")
public String testParam(
    	@RequestParam("username")String user_name,
    	String password){
    System.out.println("username:"+username+",password:"+password);
    return "success";
}

注:

@RequestHeader 注解和 @RequestParam 类似,用来将请求头信息和控制器方法的形参创建映射关系

@CookieValue 注解和 @RequestParam 类似,用来将 cookie 数据和控制器方法的形参创建映射关系

2.2.4 使用实体类获取请求参数

可以在控制器方法的形参位置设置一个实体类类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值

@Data
public class User{
    private String username;
    private String password;
}
// 前端请求:**/testParam?username=admin&password=123456
@RequestMapping("/testParam")
public String testParam(User user){
    System.out.println("username:"+username+",password:"+password);
    return "success";
}
2.2.5 乱码问题
get请求

get请求的乱码问题是 tomcat 造成的,想要解决我们需要修改 tomcat 的配置文件

<!-- 文件地址:\tomcat安装地址\conf\server.xml -->
<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" URIEncoding="UTF-8"/>
post请求

解决获取请求参数的乱码问题,可以使用 Spring MVC 提供的编码过滤器 CharacterEncodingFilter,但是必须在 web.xml中 进行注册且一定要配置到其他过滤器之前

<!--配置springMVC的编码过滤器-->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <!-- 需要和 CharacterEncodingFilter 过滤器中的成员变量名保持一致 -->
        <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>

2.3 域对象共享数据

2.3.1 request 域对象

范围:一次请求

使用 servletAPI 向 request 域对象更共享数据
@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
    // 设置一个属性(属性名,属性值)
    request.setAttribute("testScope", "hello,servletAPI");
    return "success";
}
使用 ModelAndView 向 request 域对象共享数据

ModelAndView 有 Model 和 View 的功能:

  • Model:主要用于向请求域共享数据
  • View:主要用于设置视图,实现页面跳转
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
    ModelAndView mav = new ModelAndView();
    // 向请求域共享数据
    mav.addObject("testScope", "hello,ModelAndView");
    // 设置视图,实现页面跳转
    mav.setViewName("success");
    // 当我们使用 ModelAndView 后,该方法的返回值必须是 ModelAndView
    return mav;
}
使用 Model 向 request 域对象共享数据
@RequestMapping("/testModel")
public String testModel(Model model){
    // 向请求域共享数据
    model.addAttribute("testScope", "hello,Model");
    return "success";
}
使用 map 向 request 域对象共享数据
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
    // 向请求域共享数据
    map.put("testScope", "hello,Map");
    return "success";
}
使用 ModelMap 向 request 域对象共享数据
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
    // 向请求域共享数据
    modelMap.addAttribute("testScope", "hello,ModelMap");
    return "success";
}
总结:Model、ModelMap、Map 的关系

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

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

image-20230425150400329

不管我们使用哪种方式放入数据,控制器的所有方法,最终在 DispatcherServlet 中创建 ModelAndView 对象在 doDispatch 方法中进行返回

2.3.2 session 域对象

范围:一次会话(浏览器开启到关闭)

钝化:如果服务器关闭,但是浏览器没有关闭,会将 session 中的数据序列化到磁盘上

活化:如果浏览器没有关闭,但是服务器重新开启,会将钝化的数据反序列化回 session

@RequestMapping("/testSession")
public String testSession(HttpSession session){
    session.setAttribute("testSessionScope", "hello,session");
    return "success";
}
2.3.3 application 域对象(也叫 ServletContext 对象)

范围:整个应用(服务器开启到关闭)

@RequestMapping("/testApplication")
public String testApplication(HttpSession session){
	ServletContext application = session.getServletContext();
    application.setAttribute("testApplicationScope", "hello,application");
    return "success";
}

3 视图

Spring MVC 中的视图是 View 接口,视图的作用渲染数据,将模型 Model 中的数据展示给用户。Spring MVC 视图的种类很多,默认有转发视图和重定向视图

当工程引入 jstl 的依赖,转发视图会自动转换为 JstlView

若使用 Thymeleaf 在 Spring MVC 的配置文件中配置了 Thymeleaf 的视图解析器,由此视图解析器解析之后所得到的是 ThymeleafView

3.1 转发视图

Spring MVC 中默认的转发视图是 InternalResourceView,当我们在控制器返回时以 forword: 前缀开头,此时默认会创建 InternalResourceView 转发视图,此时的视图名称不会被 Spring MVC 配置文件中所配置的视图解析器解析,而是会将前缀 forword: 去掉,剩余部分作为最终路径通过转发的方式实现跳转

@RequestMapping("/testForward")
public String testForward(){
    // 会转发到 /testHello 请求
    return "forward:/testHello";
}

image-20210706201316593

3.2 重定向视图

Spring MVC 中默认的转发视图是 RedirectView,当我们在控制器返回时以 redirect: 前缀开头,此时默认会创建 RedirectView 重定向视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀 redirect: 去掉,然后会判断剩余部分是否以 / 开头,若是则会自动拼接上下文路径,然后作为最终路径通过重定向的方式实现跳转

@RequestMapping("/testRedirect")
public String testRedirect(){
    // 会重定向到 /testHello 请求
    return "redirect:/testHello";
}

image-20210706201602267

3.3 视图控制器

当我们的控制器方法只负责跳转页面,没有别的功能时,我们可以在 springMVC.xml 配置文件中通过视图控制器完成跳转。

<!--
	path:设置处理的请求地址
	view-name:设置请求地址所对应的视图名称
-->
<mvc:view-controller path="/" view-name="index"></mvc:view-controller>

<!-- 开启 mvc 的注解驱动 -->
<mvc:annotation-driven />

注:

当SpringMVC中设置任何一个view-controller时,其他控制器中的请求映射将全部失效,此时需要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签:<mvc:annotation-driven />

注:

什么时候用 <mvc:annotation-driven />

  1. 使用视图控制器之后
  2. 配置 servlet 控制静态资源之后
  3. 在配置文件中配置 Java 对象转 json 之后

4 RESTful

RESTful 即 Representational State Transfer,表现层资源状态转移。它不是一种技术,而是一种软件编写的规范。

4.1 RESTful 基本概念

4.1.1 资源

我们把服务器上的所有东西都看作是资源,每一个资源都是服务器上的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,**可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。**一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互。

4.1.2 资源的描述(资源的状态)

资源的描述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移(交换)。资源的表述可以有多种格式,例如 HTML/XML/JSON/纯文本/图片/视频/音频 等等。资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式。

4.1.3 状态转移

在客户端和服务器端之间转移代表资源状态的表述。通过转移和操作资源的表述,来间接实现操作资源的目的。

4.2 RESTful的实现

具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。

它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。

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

操作传统方式REST风格请求方式
查询操作/getUserById?id=1/user/1GET
保存操作/saveUser/userPOST
删除操作/deleteUser?id=1/user/1DELETE
更新操作/updateUser/userPUT

5 扩展功能

5.1 处理静态资源

原始的静态资源是由 tomcat 中的 web.xml 中的 defaultServlet 来处理的,由于它的映射是 / ,和 dispatchServlet 冲突,所以不再起作用。

解决方法

<!-- 开放对静态资源的访问,当 dispatchServlet 遇到处理不了的请求时,会交给 defaultServlet 处理 -->
<mvc:default-servlet-handler />

<!-- 并且我们需要加上注解的配置,来处理请求的映射 -->
<mvc:annotation-driven />

注:

tomcat 中的 web.xml 作用于所有个工程

我们的 web.xml 只作用于当前项目,并继承 tomcat 中的 web.xml

5.2 http 报文转换器

HttpMessageConverter(报文信息转换器),将请求报文转换为 Java 对象,或将 Java 对象转换为响应报文。HttpMessageConverter 提供了两个注解和两个类型:@RequestBody,@ResponseBody,RequestEntity,ResponseEntity

5.2.1 @RequestBody 注解

@RequestBody 可以获取请求体,并根据控制器方法形参的名字为它赋值,使用 @RequestBody 进行标识后的控制器方法,当前请求的请求体就会为当前注解所标识的形参赋值

{
    "uname" : "admin",
    "passwd":"123"
}
@Data
public class User{
    private String uname;
    private String passwd;
}
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody User user){
    System.out.println("uname:"+user.uname);
     System.out.println("passwd:"+user.passwd);
    return "success";
}
5.2.2 @ResponseBody 注解

@ResponseBody 用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器

@RequestMapping("/testResponseBody")
@ResponseBody
public String testResponseBody(){
    return "success";
}

结果:浏览器页面显示 success

5.2.3 RequestEntity 类型

RequestEntity 封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息

@RequestMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> requestEntity){
    System.out.println("requestHeader:"+requestEntity.getHeaders());
    System.out.println("requestBody:"+requestEntity.getBody());
    return "success";
}

输出结果:
requestHeader:[host:“localhost:8080”, connection:“keep-alive”, content-length:“27”, cache-control:“max-age=0”, sec-ch-ua:“” Not A;Brand";v=“99”, “Chromium”;v=“90”, “Google Chrome”;v=“90"”, sec-ch-ua-mobile:“?0”, upgrade-insecure-requests:“1”, origin:“http://localhost:8080”, user-agent:“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36”]
requestBody:uname=admin&passwd=123

5.2.4 ResponseEntity 类型

ResponseEntity 用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文

5.2.5 @RestController 注解

@RestController 注解是 Spring MVC 提供的一个复合注解,标识在控制器的类上,就相当于为类添加了 @Controller 注解,并且为其中的每个方法添加了 @ResponseBody 注解

5.3 文件上传和下载

5.3.1 文件上传

使用 ResponseEntity 实现下载文件的功能

文件下载的步骤:

  • 通过 Spring 的 Context 对象和文件的地址获取到文件
  • 通过流将文件加载到一个 byte[] 里面
  • 创建 ResponseEntity 对象
    • 在响应头中设置要下载方式以及下载文件的名字
    • 设置文件的各个属性
  • 返回 ResponseEntity 对象
@RequestMapping("/testDown")
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);
    // 创建字节数组,available() 为文件的字节数
    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;
}
5.3.2 文件下载

文件上传要求 form 表单的请求方式必须为 post,并且添加属性 enctype=“multipart/form-data”

Spring MVC 中将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息

文件上传的步骤:

  • 添加依赖:

    <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
    
  • 在SpringMVC的配置文件中添加配置:

    <!-- 必须通过文件解析器的解析才能将文件转换为 MultipartFile 对象 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>
    
  • 控制器方法:

    @RequestMapping("/testUp")
    public String testUp(MultipartFile photo, HttpSession session) throws IOException {
        // 获取上传的文件的文件名
        String fileName = photo.getOriginalFilename();
        // 处理文件重名问题
        String hzName = fileName.substring(fileName.lastIndexOf("."));
        fileName = UUID.randomUUID().toString() + hzName;
        // 通过 HttpSession 对象获取服务器中 photo 目录的路径
        ServletContext servletContext = session.getServletContext();
        String photoPath = servletContext.getRealPath("photo");
        File file = new File(photoPath);
        if(!file.exists()){
            file.mkdir();
        }
        String finalPath = photoPath + File.separator + fileName;
        // 实现上传功能
        photo.transferTo(new File(finalPath));
        return "success";
    }
    

5.4 拦截器

5.4.1 拦截器与过滤器的区别

image-20230513180613380

5.4.2 拦截器的配置

Spring MVC 中的拦截器用于拦截控制器方法的执行

Spring MVC 中的拦截器需要实现 HandlerInterceptor 接口

Spring MVC 的拦截器必须在 Spring MVC 的配置文件中进行配置:

<mvc:interceptors> 
    <!-- <bean class="com.oizys.interceptor.FirstInterceptor"></bean> -->
    <!-- <ref bean="firstInterceptor"></ref> -->
    <!-- 以上两种配置方式都是对 DispatcherServlet 所处理的所有的请求进行拦截 -->
    <mvc:interceptor>
        <!-- 拦截所有的请求 -->
        <mvc:mapping path="/**"/>
        <!-- 排除的请求 -->
        <mvc:exclude-mapping path="/testRequestEntity"/>
        <ref bean="firstInterceptor"></ref>
    </mvc:interceptor>
    <!-- 以上配置方式可以通过 ref 或 bean 标签设置拦截器,通过 mvc:mapping 设置需要拦截的请求,通过 mvc:exclude-mapping 设置需要排除的请求,即不需要拦截的请求 -->
</mvc:interceptors>
5.4.3 拦截器的三个抽象方法

SpringMVC中的拦截器有三个抽象方法:

preHandle:控制器方法执行之前执行,其 boolean 类型的返回值表示是否拦截或放行,返回 true 为放行,即调用控制器方法;返回 false 表示拦截,即不调用控制器方法

postHandle:控制器方法执行之后执行

afterComplation:处理完视图和模型数据,渲染视图完毕之后执行

5.4.4 多个拦截器的执行顺序
  1. 若每个拦截器的 preHandle() 都返回 true

此时多个拦截器的执行顺序和拦截器在 Spring MVC 的配置文件的配置顺序有关:

preHandle() 会按照配置的顺正序执行,而 postHandle() 和afterComplation() 会按照配置的反序执行

  1. 若某个拦截器的 preHandle() 返回了 false

它之前的拦截器的 preHandle() 都会执行,postHandle() 都不执行,返回 false 的拦截器之前的拦截器的 afterComplation() 会执行

5.5 异常处理器

5.5.1 基于配置的异常处理

Spring MVC 提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver

HandlerExceptionResolver 接口的实现类有:

  • DefaultHandlerExceptionResolver
  • SimpleMappingExceptionResolver

Spring MVC 提供了自定义的异常处理器 SimpleMappingExceptionResolver,使用方式:

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
        	<!--
        		properties 的键表示处理器方法执行过程中出现的异常
        		properties 的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
        	-->
            <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <!--
    	exceptionAttribute 属性设置一个属性名,将出现的异常信息在请求域中进行共享
    -->
    <property name="exceptionAttribute" value="ex"></property>
</bean>
5.5.2 基于注解的异常处理
// @ControllerAdvice 将当前类标识为异常处理的组件
@ControllerAdvice
public class ExceptionController {

    // @ExceptionHandler 用于设置所标识方法处理的异常
    @ExceptionHandler(ArithmeticException.class)
    // ex 表示当前请求处理中出现的异常对象
    public String handleArithmeticException(Exception ex, Model model){
        model.addAttribute("ex", ex);
        return "error";
    }

}

6 注解配置 Spring MVC

使用配置类和注解代替 web.xml 和 springMVC.xml 配置文件

6.1 创建初始化类,代替 web.xml

在 Servlet3.0 环境中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer 接口的类,如果找到的话就用它来配置 Servlet 容器

Spring 提供了这个接口的实现,名为 SpringServletContainerInitializer,这个类反过来又会查找实现 WebApplicationInitializer 的类并将配置的任务交给它们来完成。

Spring3.2 引入了一个便利的 WebApplicationInitializer 基础实现,叫 AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展它并将其部署到 Servlet3.0 容器的时候,容器会自动发现它,并用它来配置Servlet 上下文从而代替 web.xml

// web工程的初始化类,用来代替 web.xml
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

    /**
     *  注册过滤器
     */
    @Override
    protected Filter[] getServletFilters() {
        // 配置编码过滤器
        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("UTF-8");
        encodingFilter.setForceRequestEncoding(true);
        // 处理模拟的请求方式
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
    }
}

6.2 创建SpringConfig配置类,代替spring的配置文件

@Configuration
public class SpringConfig {
	//ssm整合之后,spring的配置信息写在此类中
}

6.3 创建WebConfig配置类,代替SpringMVC的配置文件

@Configuration
// 扫描组件
@ComponentScan("com.oizys.mvc.controller")
// 开启 MVC 注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // 使用默认的 servlet 处理静态资源
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

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

    // 配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        FirstInterceptor firstInterceptor = new FirstInterceptor();
        registry.addInterceptor(firstInterceptor).addPathPatterns("/**");
    }
    
    // 配置视图控制
   
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }
    
    // 配置异常映射
    /*@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("ex");
        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;
    }


}

7 Spring MVC 执行流程

7.1 SpringMVC常用组件

  • DispatcherServlet:前端控制器,不需要工程师开发,由框架提供

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

  • HandlerMapping:处理器映射器,不需要工程师开发,由框架提供

​ 作用:根据请求的 url、method 等信息查找 Handler,即控制器方法

  • Handler:处理器,需要工程师开发

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

  • HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供

​ 作用:通过 HandlerAdapter 对处理器(控制器方法)进行执行

  • ViewResolver:视图解析器,不需要工程师开发,由框架提供

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

  • View:视图

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

7.2 DispatcherServlet初始化过程

DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。

sdfretxcv

7.2.1 初始化 WebApplicationContext

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

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
                     将 Spring 的 IoC 容器设置为 Spring MVC 的父容器
                    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
         创建WebApplicationContext
        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.
         将 MVC 的 IoC 容器在应用域共享
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
7.2.2 创建 WebApplicationContext

所在类: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;
}
7.2.3 DispatcherServlet初始化策略

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

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

protected void initStrategies(ApplicationContext context) {
   initMultipartResolver(context);
   initLocaleResolver(context);
   initThemeResolver(context);
   initHandlerMappings(context);
   initHandlerAdapters(context);
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   initViewResolvers(context);
   initFlashMapManager(context);
}

7.3 DispatcherServlet调用组件处理请求

7.3.1 processRequest()

FrameworkServlet 重写 HttpServlet 中的 service() 和 doXxx(),这些方法中调用了 processRequest(request, response)

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

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
		 执行服务,doService()是一个抽象方法,在DispatcherServlet中进行了重写
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        logResult(request, response, failureCause, asyncManager);
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}
7.3.2 doService()

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

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    logRequest(request);

    // Keep a snapshot of the request attributes in case of an include,
    // to be able to restore the original attributes after the include.
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap<>();
        Enumeration<?> attrNames = request.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = (String) attrNames.nextElement();
            if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }
    }

    // Make framework objects available to handlers and view objects.
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    RequestPath requestPath = null;
    if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
        requestPath = ServletRequestPathUtils.parseAndCache(request);
    }

    try {
         处理请求和响应
        doDispatch(request, response);
    }
    finally {
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // Restore the original attribute snapshot, in case of an include.
            if (attributesSnapshot != null) {
                restoreAttributesAfterInclude(request, attributesSnapshot);
            }
        }
        if (requestPath != null) {
            ServletRequestPathUtils.clearParsedRequestPath(request);
        }
    }
}
7.3.3 doDispatch()

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

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            
            /*
            	mappedHandler:调用链
                包含handler、interceptorList、interceptorIndex
            	handler:浏览器发送的请求所匹配的控制器方法
            	interceptorList:处理控制器方法的所有拦截器集合
            	interceptorIndex:拦截器索引,控制拦截器 afterCompletion() 的执行
            */
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
            通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }
			
             调用拦截器的 preHandle()
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
             由处理器适配器调用具体的控制器方法,最终获得 ModelAndView 对象
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);
             调用拦截器的 postHandle()
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
         后续处理:处理模型数据和渲染视图
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                               new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}
7.3.4 processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                   @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                                   @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
         处理模型数据和渲染视图
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    if (mappedHandler != null) {
        // Exception (if any) is already handled..
         调用拦截器的 afterCompletion()
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

7.4 Spring MVC 的执行流程

image-20230513185632991

  1. 用户向服务器发送请求,请求被 Spring MVC 前端控制器 DispatcherServlet 捕获。

  2. DispatcherServlet 对请求 URL 进行解析,得到请求资源标识符(URI),判断请求 URI 对应的映射:

a) 不存在

i. 再判断是否配置了 mvc:default-servlet-handler

ii. 如果没配置,则控制台报映射查找不到,客户端展示404错误

image-20210709214911404

image-20210709214947432

iii. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误

image-20210709215255693

image-20210709215336097

b) 存在则执行下面的流程

  1. 根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 执行链对象的形式返回。

  2. DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter。

  3. 如果成功获得 HandlerAdapter,此时将开始执行拦截器的 preHandler(…) 方法【正向】

  4. 提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler(Controller) 方法,处理请求。在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:

a) HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息

b) 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等

c) 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等

d) 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中

  1. Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象。

  2. 此时将开始执行拦截器的 postHandle(…) 方法【逆向】。

  3. 根据返回的 ModelAndView(此时会判断是否存在异常:如果存在异常,则执行 HandlerExceptionResolver 进行异常处理)选择一个适合的 ViewResolver 进行视图解析,根据 Model和View,来渲染视图。

  4. 渲染视图完毕执行拦截器的 afterCompletion(…) 方法【逆向】。

  5. 将渲染结果返回给客户端。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值