第八章SpringMVC进阶学习

RESTful

关于资源的概念

资源

资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互。

URL和URI的区别和联系

什么是URI
URI就类似一个独一无二的身份标识。准确的说是某个网络资源的特有标识(用来区别于其他资源的独一无二的标识)

有这样一个需求:
要求找到一本书(书就是资源),这本书在A省份/B市/C区/D街道/xx栋/392-1住户/1号房间/名字叫做《xxx》 (这里就是模拟我们输入网址进行HTTP请求)

  • 那么此时的 《xxx》 这本书 对于 A省份/B市/C区/D街道/xx栋/392-1住户/1号房间/ 来说就是URI
  • 此时的D街道/xx栋/392-1住户/1号房间/名字叫做《xxx》这本书 对于 A省份/B市/C区 来说就是URI
  • 可以看出URI是不固定的,是相对来说的,具体是什么就看你的参照角度是什么。(不同请求参照角度不一样,所以他们的返回uri有差异)
  • 由此总结:URI是一个标识,用来区别于其他资源的标识。

什么是URL
那么再来说一说什么是URL。URL就是每次我们输入网址访问某个网站时,浏览器上输入的那一行内容。比如:http://baidu.com这是一个URL

  • 类比上面的例子,我们的A省份/B市/C区/D街道/xx栋/392-1住户/1号房间/名字叫做《xxx》就是一个URL,类似一个绝对地址,根据这个地址肯定能找到这本书
  • 但是只根据URI,我们是找不到这本书的,必须有前提条件
  • 我们的URL包含了协议,ip地址,端口号,和资源的在服务器下的相对路径,类似绝对路径,也就肯定能找到这个数据

URI和URL的关系
URI是URL的父级,URL是URI的子级。
明明是URL包含了URI为啥URI反而是父级

  • 请注意,我这里用的是级别来描述,而不是包含。
  • 我没有说URL是URI的一部分,而是说是他的子级。
  • 想要理解这个概念,最好的说明就是Java的继承关系。URL继承了URI。
  • 因为URL继承了所有URI的内容,所以它比URI更加详细,但是URI是它的父级。

有什么作用
URL的作用

  • URL一般是一个完整的链接,我们可以直接通过这个链接(url)访问到一个网站或者对应的资源,或者把这个url复制到浏览器访问网站。

URI的作用

  • URI并不是一个直接访问的链接,而是相对地址(当然如果相对于浏览器那么uri等同于url了)。这种概念更多的是用于编程中,因为我们没必要每次编程都用绝对url来获取一些页面,这样还需要进行分割“http://xx/xxx”前面那一串,所以编程的时候直接request.getRequestURI就行了,当然如果是重定向的话,就用URL。

资源的表述

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

RESTful简介

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

RESTFUL是一种网络应用程序的设计风格和开发方式基于HTTP,可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点。

REST 是面向资源的,每个资源都有一个唯一的资源定位符(URI)。每个URI代表一种资源(resource),所以URI中不能有动词,只能有名词,而且所用的名词往往与数据库的表名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以URI中的名词也应该使用复数。

RESTful的实现

  • 具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE用来删除资源。
  • REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。
操作传统方式REST风格
查询操作getUserById?id=1user/1–>get请求方式
保存操作saveUseruser–>post请求方式
删除操作deleteUser?id=1user/1–>delete请求方式
更新操作updateUseruser–>put请求方式

没有RESTful之前的用法

http://127.0.0.1/user/query/1 GET 根据用户id查询用户数据
http://127.0.0.1/user/save POST 新增用户
http://127.0.0.1/user/update POST 修改用户信息
http://127.0.0.1/user/delete/1 GET/POST 删除用户信息

RESTful用法:
http://127.0.0.1/user/1 GET 根据用户id查询用户数据
http://127.0.0.1/user POST 新增用户
http://127.0.0.1/user PUT 修改用户信息
http://127.0.0.1/user/1 DELETE 删除用户信息

之前的操作有什么问题呢?你每次请求的接口或者地址,都在做描述,例如查询的时候用了query,新增的时候用了save,其实完全没有这个必要,我使用了get请求,就是查询.使用post请求,就是新增的请求,我的意图很明显,完全没有必要做描述,这就是为什么有了restful.

HiddenHttpMethodFilter

由于浏览器只支持发送get和post方式的请求,那么该如何发送put和delete请求呢?
SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求HiddenHttpMethodFilter 处理put和delete请求的条件:

  • 当前请求的请求方式必须为post
  • 当前请求必须传输请求参数_method

满足以上条件,HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数

  • _method的值,因此请求参数_method的值才是最终的请求方式
  • 在web.xml中注册HiddenHttpMethodFilter

目前为止,SpringMVC中提供了两个过滤器:CharacterEncodingFilter和HiddenHttpMethodFilter
在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter
原因:
在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字符集的 request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作

String paramValue = request.getParameter(this.methodParam);  

实例

查询操作

@Controller
public class TestRestController {
    @GetMapping("/user")
    public String getAllUser(){
        System.out.println("查询所有的用户信息-->/user-->get");
        return "success";
    }
}
//http://localhost:8080/springmvc/user 在浏览器直接输入,是get方法
//输出结果
查询所有的用户信息-->/user-->get
@Controller
public class TestRestController {
@GetMapping("user/{id}")
    public String getUserById(@PathVariable("id") Integer id){
        System.out.println("根据id查询用户信息-->/user/"+id+"-->get");
        return "success";
    }
}
//http://localhost:8080/springmvc/user/1 在浏览器直接输入,是get方法
//输出结果
根据id查询用户信息-->/user/1-->get

保存操作

<form th:action="@{/user}" th:method="post">
    <input type="submit" value="测试Restful">
</form>
<!--利用表单发送post请求-->
@Controller
public class TestRestController {
    @PostMapping("/user")
    public String insertUser(){
        System.out.println("添加用户信息-->/user-->post");
        return "success";
    }
}
//输出结果
添加用户信息-->/user-->post

更新操作

<!--利用表单和HiddenHttpMethodFilter来将post转换为别的请求-->
<form th:action="@{/user}" th:method="post">
    <input type="hidden" name="_method" value="put">
    <input type="submit" value="测试Restful的put">
</form>
@Controller
public class TestRestController {
	@PutMapping("/user")
    public String updateUser(){
        System.out.println("修改用户信息-->/user-->put");
        return "success";
    }
}
//输出结果
修改用户信息-->/user-->put

删除操作

<form th:action="@{/user/5}" th:method="post">
    <input type="hidden" name="_method" value="delete">
    <input type="submit" value="测试Restful的delete">
</form>
@Controller
public class TestRestController {
    @DeleteMapping("/user/{id}")
    public String deleteUser(@PathVariable("id") Integer id){
        System.out.println("删除用户信息-->/user/"+id+"-->delete");
        return "success";
    }
}
//输出结果
删除用户信息-->/user/5-->delete

SpringMVC处理AJAX请求

@RequestBody

@RequestBody可以获取请求体信息,使用@RequestBody注解标识控制器方法的形参,当前请求的请求体就会为当前注解所标识的形参赋值

前端页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<p>首页页面</p>
<form th:action="@{/testAjax}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit">
</form>
</body>
</html>

后端代码

@Controller
public class TestAjax {
        @RequestMapping("/")
        public String index() {
            //设置视图名称
            return "index";
        }
        @PostMapping("/testAjax")
        public String testRequestBody(@RequestBody String requestBody){
        System.out.println("requestBody:"+requestBody);
         return "success";
        }
 //   requestBody:username=lsc&password=123456
}
  • 注意我们的Get是没有请求体的,我们Get的参数传递是通过我们的url中的请求参数进行传递

@RequestBody获取json格式的请求参数

在使用了axios发送ajax请求之后,浏览器发送到服务器的请求参数有两种格式:

  • name=value&name=value…,此时的请求参数可以通过request.getParameter()获取,对应SpringMVC中,可以直接通过控制器方法的形参获取此类请求参数

  • {key:value,key:value,…},此时无法通过request.getParameter()获取,之前我们使用操作json的相关jar包gson或jackson处理此类请求参数,可以将其转换为指定的实体类对象或map集合。在SpringMVC中,直接使用@RequestBody注解标识控制器方法的形参即可将此类请求参数转换为java对象

  • 3、在控制器方法的形参位置,设置json格式的请求参数要转换成的java类型(实体类或map)的参数,并使用@RequestBody注解标识

前提准备

导入jackson的依赖

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

SpringMVC的配置文件中设置开启mvc的注解驱动

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

对应前后端代码

前端代码

<body>
<div id="app">
    <h1>index.html</h1>
    <input type="button" @click="testRequestBodyJson1" value="测试RequestBody的JSON格式使用String接收"><br>
    <input type="button" @click="testRequestBodyJson2" value="测试RequestBody的JSON格式使用Map接收"><br>
    <input type="button" @click="testRequestBodyJson3" value="测试RequestBody的JSON格式使用对象接收"><br>
</div>
</body>

对应的Axios代码

	 <script type="text/javascript" th:src="@{js/vue.js}" ></script>
    <script type="text/javascript" th:src="@{js/axios.min.js}"></script>
    <script type="text/javascript">
        window.onload=function () {
            var vue=new Vue({
                el:"#app",
                data:{
                    username:"lsc",
                    password:"123"
                },
                methods:{
                    testRequestBodyJson1:function () {
                        axios({
                            method:"POST",
                            url:"/springmvc/testRequestBody1/json",
                            data: {
                                username: vue.username,
                                password: vue.password
                            }
                        }).then(function (value) {
                            console.log(value.data)
                        })
                    },
                    testRequestBodyJson2:function () {
                        axios.post(
                            "/springmvc/testRequestBody2/json",
                            {username:vue.username,password:vue.password}
                        ).then(function(value){
                            console.log(value.data)
                        })
                    },
                    testRequestBodyJson3:function () {
                        axios.post(
                            "/springmvc/testRequestBody3/json",
                            {username:vue.username,password:vue.password}
                        ).then(function(value){
                            console.log(value.data)
                        })
                    }
                }
            });
        }
    </script>
  • url表示的是请求路径

  • method表示的是请求方法的类型

    • 如果是axios.post这种类型,就不要再写method属性
  • params表示的是以请求参数的方式来传递信息 ?name=value&pwd=value

    • 这种方式不管我们的请求方式是post还是get,最终都会被拼接到我们url中
    • 这种方式传递的参数可以通过request.getParameter来获取
  • data表示的会将参数放入到我们的请求体的请求报文中

    • 会以json的格式来传递参数
    • 这种方式传递的参数不可以通过request.getParameter来获取

后端代码

我们的三种方式的后端代码的方法返回值都是void,为什么呢?因为我们的ajax请求是局部刷新,也就是不会更新整个页面(不刷新浏览器窗口不做页面跳转),所以我们的转发或者是重定向就不会生效

用字符串接收

@PostMapping("/testRequestBody1/json")
public void testRequestBodyJson1(@RequestBody String requestBody,HttpServletResponse response) throws IOException {
     System.out.println("requestBody:"+requestBody);
     response.getWriter().write("hello,RequestBody");
}
//输出结果
requestBody:{"username":"lsc","password":"123"}
  • 我们可以看到利用axios的data传输数据是通过json格式进行传输

用Map接收

@PostMapping("/testRequestBody2/json")
public void testRequestBodyJson2(@RequestBody Map<String,Object> map, HttpServletResponse response) throws IOException {
    System.out.println(map);
    response.getWriter().write("hello,RequestBody");
}
//输出结果
{username=lsc, password=123}

用实体类对象接收

public class User {
    private String username;
    private String password;
}
 @PostMapping("/testRequestBody3/json")
public void testRequestBodyJson3(@RequestBody User user, HttpServletResponse response) throws IOException {
    System.out.println(user);
    response.getWriter().write("hello,RequestBody");
}
//输出结果
User{username='lsc', password='123'}

@ResponseBody

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

<input type="button" @click="testResponseBody" value="测试ResponseBody">
@GetMapping("/testResponseBody")
@ResponseBody
public String testResponseBody(){
    return "success";
}
@GetMapping("/testResponseBody")
public String testThymeleaf(){
    return "success";
}
  • 对于testThymeleaf()方法,会经过thymeleaf渲染,会跳转到逻辑视图success所以对应的页面
  • testResponseBody()方法,直接会响应浏览器数据success

@ResponseBody响应浏览器json数据

  • 服务器处理ajax请求之后,大多数情况都需要向浏览器响应一个java对象,此时必须将java对象转换为json字符串才可以响应到浏览器
  • 之前我们使用操作json数据的jar包gson或jackson将java对象转换为json字符串。
  • 在SpringMVC中,我们可以直接使用@ResponseBody注解实现此功能

前提

  • 导入jackson依赖
  • SpringMVC配置文件中开启mvc的注解驱动
<input type="button" value="测试@ResponseBody响应浏览器json格式的数据" @click="testResponseBody"><br>
<input type="button" value="测试@ResponseBody响应浏览器json格式的数据之Map集合" @click="testResponseBody2"><br>
<input type="button" value="测试@ResponseBody响应浏览器json格式的数据之对象" @click="testResponseBody3"><br>
 <script type="text/javascript">
        window.οnlοad=function () {
            var vue=new Vue({
                el:"#app",
                data:{
                    username:"lsc",
                    password:"123"
                },
                methods:{
                    testResponseBody1:function (){
                        axios.post(
                            "/springmvc/testResponseBody/json/list"
                        ).then(function (value) {
                            console.log(value.data)
                        })
                    },
                    testResponseBody2:function (){
                        axios.post(
                            "/springmvc/testResponseBody/json/map"
                        ).then(function (value) {
                            console.log(value.data)
                        })
                    },
                    testResponseBody3:function (){
                        axios.post(
                            "/springmvc/testResponseBody/json/class"
                        ).then(function (value) {
                            console.log(value.data)
                        })
                    }
                }
            });
        }
    </script>

响应浏览器list集合

@PostMapping("/testResponseBody/json")
@ResponseBody
public List<User> testResponseBodyJson(){
   User user1=new User("Lsc","123");
   User user2=new User("Lsc","123");
   User user3=new User("Lsc","123");
   List<User> list= Arrays.asList(user1,user2,user3);
   return list;
}

image-20230131112337383

响应浏览器map集合

 @PostMapping("/testResponseBody/json/map")
 @ResponseBody
 public Map<String, Object>  testResponseBodyJson2(){
            User user1=new User("Lsc","123");
            User user2=new User("Lkx","123");
            User user3=new User("Xsg","123");
            Map<String,Object> map=new HashMap<>();
            map.put("1",user1);
            map.put("2",user2);
            map.put("3",user3);
            return map;
 }

image-20230131114221984

响应浏览器实体类对象

@RequestMapping("/testResponseBody/json/class")
@ResponseBody
public User  testResponseBodyJson3(){
    User user=new User("LSC","12345");
    return user;
}

image-20230131114701260

@RestController注解

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

文件上传和下载

文件下载

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

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

 	@RequestMapping("/testDown")
    public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
        //获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
        //获取服务器中文件的真实路径
        String realPath = servletContext.getRealPath("/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;
    }

文件上传

文件上传要求form表单的请求方式必须为post,并且添加属性enctype=“multipart/form-data” SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息

前提准备

导入相关依赖

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

前端代码

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

后端代码

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

拦截器

  • SpringMVC中的拦截器用于拦截控制器方法的执行
  • SpringMVC中的拦截器需要实现HandlerInterceptor
  • SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置:

拦截器的配置

第一种配置方式

	 <mvc:interceptors>
        <!-- <bean class="com.lsc.springmvc.interceptor.FirstInterceptor"></bean>-->
             <ref bean="firstInterceptor"></ref>
         
    </mvc:interceptors>
  • 两种bean的配置方式都可以找到对应的拦截器对象
  • 对于这种方式的配置,拦截器默认会拦截所有交给DispatcherServlet的请求和响应

第二种配置方式

 <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <mvc:exclude-mapping path="/"/>
            <ref bean="firstInterceptor"></ref>
        </mvc:interceptor>
    </mvc:interceptors>
  • mvc:mapping表示这个拦截器要拦截具体的请求是什么
    • /**表示任意深度的请求 比如可以是 /test /test/hello
    • /*只能表示 /test 一个深度的请求
  • mvc:exclude表示排除什么请求不会进行拦截

拦截器的三个方法

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

  • preHandle:控制器方法执行之前执行preHandle(),
    • 其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法
  • postHandle:控制器方法执行之后执行postHandle()
  • afterCompletion:处理完视图和模型数据,渲染视图完毕之后执行afterCompletion()
@Component
public class FirstInterceptor  implements HandlerInterceptor  {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("控制器方法执行之前执行 preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("控制器方法执行之后执行postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("渲染视图完毕之后执行afterCompletion");
    }
}

多个拦截器的执行顺序

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

异常处理器

基于xml配置的异常处理

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

HandlerExceptionResolver接口的实现类有:

  • DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver
    • DefaultHandlerExceptionResolver 如果没有进行配置,那么SpringMVC默认使用这个
 <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="java.lang.ArithmeticException">error</prop>
            </props>
        </property>
     	 <property name="exceptionAttribute" value="ex"></property>
    </bean>
  • exceptionMappings :key表示设置要处理的异常,value设置出现该异常是要进行跳转的页面对应的逻辑视图
  • exceptionAttribute 属性设置一个属性名,将出现的异常信息在请求域中进行共享

基于注解的异常处理

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

  • @ControllerAdvice 将当前类标识为异常处理的组件
  • @ExceptionHandler 用于设置所标识方法要处理的异常
  • model.addAttribute();向请求域中添加数据

注解配置SpringMVC

  • 之前我们前面使用了web.xml和springmvc.xml进行配置
  • 我们也可以通过注解的方式来代替这两个文件的功能

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

  • 在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的对应的实现类话就用它来配置Servlet容器。
    • Spring提供了这个接口的实现,名为SpringServletContainerInitializer 这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。
      • Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。
/**
 * 这个类用来代替web.xml
 */
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     *  指定spring的配置类来代替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.setForceEncoding(true);//设置响应的编码也是utf-8
        //创建处理请求方式的过滤器 为了满足REST风格的编码
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter,hiddenHttpMethodFilter};
    }
}

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

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

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

  • 我们之前在SpringMVC中配置的东西
    1. 扫描组件——用来扫描注解标识的类是否为bean对象,被SpringIoc容器进行管理
    2. 视图解析器——用来进行thymeleaf的视图渲染和转发和对应的逻辑视图的前缀和后缀的添加
    3. 默认的servlet
      • 配置默认的sevlet是为了处理静态资源 若只配置了<mvc:default-servlet-handler />,此时浏览器发送的所有请求都会被DefaultServlet处理
    4. mvc的注解驱动
      • 在我们为了让浏览器发送的请求会先被DispatcherServlet处理,无法处理在交给DefaultServlet处理的时候需要给默认的servlet处理
    5. 文件上传解析器——用于文件上传时
    6. 拦截器——对于控制器方法进行对应的拦截处理
    7. 异常解析器——用于发生异常可以跳转到对应的页面
    8. 视图控制器——用xml的方式进行将对应的请求对应到配置逻辑视图,然后经过视图解析器到对应的页面

​ 配置默认的servlet处理静态资源
​ 当前工程的web.xml配置的前端控制器DispatcherServlet的url-pattern是/
​ tomcat的web.xml配置的DefaultServlet的url-pattern也是/
​ 此时,浏览器发送的请求会优先被DispatcherServlet进行处理,但是DispatcherServlet无法处理静态资源

/**
 * 用来代替springMVC的配置文件
 */
@Configuration//将当前类配置为配置类
//扫描组件
@ComponentScan("com.lsc.springmvc")
@EnableWebMvc()//开启MVC的注解驱动
public class WebConfig implements WebMvcConfigurer {
    /**
     * 默认的servlet来处理静态资源
     * @param configurer
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    /**
     * 配置对应视图控制器
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }

    /**
     * 配置文件上传解析器
     * @return
     */
    @Bean
    public CommonsMultipartResolver multipartResolver(){
        return new CommonsMultipartResolver();
    }
	/**
     * 配置拦截器
     * @return
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        FirstInterceptor firstInterceptor=new FirstInterceptor();
        registry.addInterceptor(firstInterceptor).addPathPatterns("/**").excludePathPatterns("/");
    }

    /**
     * 配置异常解析器
     * @param resolvers
     */
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        Properties prop=new Properties();
        prop.setProperty("java.lang.ArithmeticException","error");
        simpleMappingExceptionResolver.setExceptionMappings(prop);
        simpleMappingExceptionResolver.setExceptionAttribute("ex");
        resolvers.add(simpleMappingExceptionResolver);
    }
    //配置生成模板解析器
    @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执行流程

SpringMVC常用组件

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

    • 作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
  • HandlerMapping:处理器映射器,不需要工程师开发,由框架提供

    • 作用:根据请求的url、method等信息查找Handler,即控制器方法 我们的requestMapping中的value属性,method属性,params等都是这个作用
    • 通过扩展处理器适配器支持更多类型的处理器
  • Handler:处理器,需要工程师开发

    • 作用:在DispatcherServlet的控制下Handler对具体的用户请求进行处理 就是在我们的Controller层编写的代码
  • HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供

    • 作用:通过HandlerAdapter对处理器(控制器方法)进行执行,我们的HandlerMapping只是找到了对应的方法,而方法的执行还是得靠HandlerAdapter来进行调用
  • ViewResolver:视图解析器,不需要工程师开发,由框架提供

    • 作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView
    • 也就是将逻辑视图名解析为真正的视图
  • View:视图

    • 作用:将模型数据通过页面展示给用户 比如我们的前端HTML页面

DispatcherServlet初始化过程

首先DispatcherServlet本质上是一个Servlet,所以天然还是遵循我们的Servlet的生命周期,所以在宏观上还是在Servlet的生命周期进行调度

对于一个Servlet默认情况下:

  • 第一次接收请求时,这个Servlet会进行实例化(调用构造方法)、初始化(调用init())、然后服务(调用service())

  • 从第二次请求开始,每一次都是服务

  • 当容器关闭时,其中的所有的servlet实例会被销毁,调用销毁方法 destroy()

  • 从调用构造方法可以看出 Servlet实例tomcat只会创建一个,所有的请求都是这个实例去响应。

  • 一般情况下,第一次请求时,Servlet才会去实例化,初始化,然后再服务.这样的好处是什么? 提高系统的启动速度 。 这样的缺点是什么? 第一次请求时,耗时较长。

    • 因此得出结论: 如果需要提高系统的启动速度,当前默认情况就是这样。如果需要提高响应速度,我们应该设置Servlet的初始化时机。
<!--
       作为框架的核心组件,在启动过程中有大量的初始化操作要做
       而这些操作放在第一次请求时才执行会严重影响访问速度
       因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
-->
<load-on-startup>1</load-on-startup>

因此我们的看DispatcherServlet初始化必须关注它的init方法

DispatcherServlet的继承关系

image-20230204121802218

GenericServlet的init方法

//这是重写Servlet接口的void init(ServletConfig var1) throws ServletException
public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }
//这是重载init方法
 public void init() throws ServletException {
}
  • 这里的意义就是在执行void init(ServletConfig config),也会调用了 void init() ,而且给成员对象 config赋值

HttpServlet的init方法

  • 没有重写或者重载init方法,所以完全继承GenericServlet的init方法

HttpServletBean的init方法

//这是重写了GenericServlet的 public void init() throws ServletException方法
public final void init() throws ServletException {
        PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
                this.initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            } catch (BeansException var4) {
                if (this.logger.isErrorEnabled()) {
                    this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
                }

                throw var4;
            }
        }

        this.initServletBean();
    }
  • this.initServletBean()这里调用了initServletBean()方法,把初始化的功能交给了其它的方法,也就是我们的initServletBean

HttpServletBean的initServletBean方法

protected void initServletBean() throws ServletException {
}
  • HttpServletBean中这个方法是空的,而且还是protected,明显是让子类重写去实现

FrameworkServlet的init方法

没有重写或者重载init方法,所以完全继承HttpServletBean的init方法

FrameworkServlet的initServletBean方法

  protected final void initServletBean() throws ServletException {
        this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
        }

        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var4) {
            this.logger.error("Context initialization failed", var4);
            throw var4;
        }

        if (this.logger.isDebugEnabled()) {
            String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data";
            this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
        }

        if (this.logger.isInfoEnabled()) {
            this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }

    }
  • this.webApplicationContext = this.initWebApplicationContext()进行实现初始化WebApplicationContext(初始化Web Ioc容器)
    • 这里也可以证SpringMVC配置文件是在DispatcherServlet初始化的时候完成的
  • this.initFrameworkServlet() 是空的,DispatcherServlet中没有重写,所以就不关注

初始化WebApplicationContext

org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
        WebApplicationContext wac = null;
    	  //先进行判断Web Ioc容器是否为null
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }

                    this.configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
			
        if (wac == null) {
            //如果Web Ioc容器为null,先进行寻找操作
            wac = this.findWebApplicationContext();
        }

        if (wac == null) {
            //如果没有找到,就创建一个新的Ioc容器
            wac = this.createWebApplicationContext(rootContext);
        }
			
        if (!this.refreshEventReceived) {
            synchronized(this.onRefreshMonitor) {
                this.onRefresh(wac);//进行刷新容器
            }
        }

        if (this.publishContext) {
            String attrName = this.getServletContextAttributeName();
            //将SpringMVCIoc容器共享到我们Servlet的上下文,也就是application(应用)域对象中
            this.getServletContext().setAttribute(attrName, wac);
        }

        return wac;
    }

创建WebApplicationContext

org.springframework.web.servlet.FrameworkServlet

 protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        Class<?> contextClass = this.getContextClass();
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() 
             + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
        } else {
            // 通过反射创建 IOC 容器对象
            ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
            wac.setEnvironment(this.getEnvironment());
            // 设置父容器 Spring 的Ioc容器是SpringMVC Ioc容器的父容器
            wac.setParent(parent);
            String configLocation = this.getContextConfigLocation();
            if (configLocation != null) {
                wac.setConfigLocation(configLocation);
            }

            this.configureAndRefreshWebApplicationContext(wac);
            return wac;
        }

DispatcherServlet初始化策

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

 protected void onRefresh(ApplicationContext context) {
        this.initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
        this.initMultipartResolver(context);//初始化文件上传解析器
        this.initLocaleResolver(context);//初始化本地解析器
        this.initThemeResolver(context);
        this.initHandlerMappings(context);//初始化处理器映射器
        this.initHandlerAdapters(context);//初始化处理器适配器
        this.initHandlerExceptionResolvers(context);//初始化异常解析器
        this.initRequestToViewNameTranslator(context);
        this.initViewResolvers(context);//初始化视图解析器
        this.initFlashMapManager(context);
    }

DispatcherServlet调用组件处理请求

我们知道Servlet处理请求的通过调用Service方法,所以我们需要关注Service方法

GenericServlet的service方法

public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
  • 我们GenreicServlet中将service方法设置为了抽象方法

HttpServlet的service方法

//这是重写了GenericServlet的service方法
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        this.service(request, response);
    } else {
        throw new ServletException("non-HTTP request or response");
    }
}
//这是重载了servlet方法,对应的是我们Http的请求和响应
 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals("GET")) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader("If-Modified-Since");
                if (ifModifiedSince < lastModified) {
                    this.maybeSetLastModified(resp, lastModified);
                    this.doGet(req, resp);
                } else {
                    resp.setStatus(304);
                }
            }
        } else if (method.equals("HEAD")) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals("POST")) {
            this.doPost(req, resp);
        } else if (method.equals("PUT")) {
            this.doPut(req, resp);
        } else if (method.equals("DELETE")) {
            this.doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            this.doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

    }
  • String method = req.getMethod(); 这个方法是获取请求的方式

    • 然后通过后面的if和else if的结构来通过什么类型的方法去执行不同的逻辑
      • 决定调用的是哪个do开头的方法
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_post_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

}

在HttpServlet这个抽象类中,do方法都差不多

  • 那么在HttpServlet中这些do方法默认都是405的实现风格-要我们子类去实现对应的方法,否则默认会报405错误

HttpServletBean的service方法

没有进行重写,所以完全继承HttpServlet

FrameworkServlet的service方法

 protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());//获取请求方法的类型
        if (httpMethod != HttpMethod.PATCH && httpMethod != null) {
            super.service(request, response);
        } else {
            this.processRequest(request, response);
        }

    }
  • 有一个if else结果,如果满足(httpMethod != HttpMethod.PATCH && httpMethod != null)
    • 如果满足则调用父类的service,也就是HttpServlet的service方法
    • 不满足,则调用processRequest()方法
  • 对应调用父类service方法,但是我们FrameworkServlet重写了doXXX()方法,所以service中调用的还是我们FrameworkServlet重写的doxxx方法
  protected final void doGet(HttpServletRequest request, HttpServletResponse response) {
        this.processRequest(request, response);
    }

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

processRequest方法

 protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = this.buildLocaleContext(request);
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
        this.initContextHolders(request, localeContext, requestAttributes);

        try {
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
            failureCause = var17;
            throw new NestedServletException("Request processing failed", var17);
        } finally {
            this.resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }

            this.logResult(request, response, (Throwable)failureCause, asyncManager);
            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
        }

    }
  • this.doService(request, response);我们的processRequest中通过调用doService方法处理请求
    • 在FrameworkServlet中的doService方法是抽象方法,由子类进行重写

DispatcherServlet的doService方法和doDispatch

 protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.logRequest(request);
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap();
            Enumeration attrNames = request.getAttributeNames();

            label120:
            while(true) {
                String attrName;
                do {
                    if (!attrNames.hasMoreElements()) {
                        break label120;
                    }

                    attrName = (String)attrNames.nextElement();
                } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));

                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }

        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.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 {
            this.doDispatch(request, response);
        } finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
                this.restoreAttributesAfterInclude(request, attributesSnapshot);
            }

            if (requestPath != null) {
                ServletRequestPathUtils.clearParsedRequestPath(request);
            }

        }

    }
  • this.doDispatch(request, response);处理由我们doDispatch进行处理

doDispatch

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

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

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                     /*
							mappedHandler:调用链包含handler、interceptorList、interceptorIndex
							handler:浏览器发送的请求所匹配的控制器方法
							interceptorList:处理控制器方法的所有拦截器集合
							interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行
						 */
                    mappedHandler = this.getHandler(processedRequest);//创建对应的HandlerMapping对象
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }
						// 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());//创建了HandlerAdapter对象
                    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;
                    }
						// 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }

                    this.applyDefaultViewName(processedRequest, mv);
                    //然后调用调用拦截器PostHandle方法
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } catch (Exception var20) {
                    dispatchException = var20;
                } catch (Throwable var21) {
                    dispatchException = new NestedServletException("Handler dispatch failed", var21);
                }
					//后续处理:处理模型数据和渲染视图也就是我们的ModeAndView 
                this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
                //在这个方法中,执行完渲染,就会调用拦截器的afterCompletion方法
            } catch (Exception var22) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
            } catch (Throwable var23) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
            }

        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            } else if (multipartRequestParsed) {
                this.cleanupMultipart(processedRequest);
            }

        }
    }

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) {
                this.logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException)exception).getModelAndView();
            } else {
                Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
                mv = this.processHandlerException(request, response, handler, exception);
                errorView = mv != null;
            }
        }

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

        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                // 调用拦截器的afterCompletion()
                mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
            }

        }
    }

SpringMVC的执行流程

img

  1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获。(通过HTTP服务器和Servelt容器的功劳)

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

    • 不存在 则会报404错误
    • 对应的资源存在 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。
  3. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter用来调用对应的Handler方法。

    • 这里的Handler也就是代指我们的HandlerExecutionChain
    • 为什么需要适配器,因为我们的handler有很多种实现方式,所以使用适配器模式,为了选择最合适的形参和对应调用方法(by反射做到的)
  4. 通过适配器去调用HandlerExecutionChain执行链对象——视为调用到我们自己写的方法上

    1. 此时将开始执行拦截器的preHandler(…)方法【正向】

    2. 提取Request中的模型数据,填充Handler入参(也就是将request域对象中的数据填充到我们调用的方法的参数中),开始执行Handler(Controller)方法,处理请求。

      • 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

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

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

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

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

  5. 如果我们的方法没有通过@ResponseBody修饰,Controller方法执行完成后,向DispatcherServlet 返回一个ModelAndView对象。

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

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

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

  9. 将渲染结果返回给客户端。将相应内容填充到HttpServletResponse对象(Servlet API的要求,自然变成了HTTP响应返回给浏览器)

对于URI不存在的情况

  • 判断是否配置了mvc:default-servlet-handler ,也就是我们的TomCat默认配置的Servlet
  • 如果没配置,则控制台报映射查找不到,客户端展示404错误
  • 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库里不会投三分

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值