【2021软件创新实验室暑假集训】SpringMVC框架(设计原理、简单使用、源码探究)

系列文章目录

前言

写本文的原因主要是为了实验室暑假集训备课所写,但是本着要么就不写,要写就写好的原则,同时也是为了让我复习技术,我就从开始的模式背景开始讲起,到SpringMVC的使用,再到SpringMVC的源码原理探究。也算自己对于SpringMVC框架的小总结吧。

一、MVC模式的发展

1.Model1

对于浏览器上丰富多彩的网页,我们是如何获取到的呢?直观的去想,我们发送给服务器一个http请求,服务器收到后返回相应的网页,可是我们希望网页上的数据根据我们的请求而变化,但是原先静态网页无法满足这个需求,因为它已经把前端代码和数据固定死了,而我们希望里面的数据可以根据请求的不同而不同。解决方案也不难想,我们只需把前端代码部分里的数据由Java程序提供即可,而这便是Model1的解决方案。

在早期 Java Web 的开发中,统一把显示层、控制层、数据层的操作全部交给 JSP 或者 JavaBean 来进行处理,我们称之为 Model1

在这里插入图片描述

2.Model2

但是model1存在一个很大的问题——代码高度耦合。JSP肩负着控制,视图渲染,数据查询处理等多项职责,同时html代码也和Java代码严重耦合在一起,这为日后项目的维护和拓展带来了极大的困难。

我们希望有一种模式可以摆脱这种困扰,于是有人提出了MVC模式,所谓MVC,其实是一种利用分层来解耦合的思想,这种利用分层/分模块解决耦合问题的方法在计算机领域颇为普遍。

那什么是MVC呢,其实它就是将Model1模式中担负了数据渲染,控制,数据分散开来,形成三层——控制层(Controller)、数据层(Model)、视图层(View)
在这里插入图片描述

  • V即View视图,是指用户看到并与之交互的界面。比如由html元素组成的网页界面,或者软件的客户端界面。MVC的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操作的方式。
  • M即model模型是指模型表示业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。
  • C即controller控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。

具体到JavaEE项目中就是下图所示。
在这里插入图片描述

3.SpringMVC中MVC

和MVC模式类似,不过SpringMVC将model层进行了细分,分为Service层(业务层)和Dao层(数据库层,又称Mapper层)。这样细分也是为了解耦合的需要,在日常开发中我们有必要将业务层的代码抽离出来。
在这里插入图片描述

4.更多设计模式——DDD领域驱动设计思想 ?

其实MVC发展远没有结束,随着业务的日渐庞大,像传统的MVC模式已经不能满足业务的需要,单方面的纵向切分在日渐庞大的业务中显得力不从心。DDD领域驱动设计便是一种解决这个问题的设计思想。

从DDD思想延伸出很多实地解决方案,比如当下热门的分布式微服务思想。

其实不管是什么设计思想,

如果你能深入理解职责、封装。并随着业务的迭代,不断的重构你的代码,那么你不需要什么DDD,或者其他方法论。

该内容与本文无关,只做简单的延伸,具体可以自行百度搜索相关内容,
阅读材料推荐:可落地的DDD的(2)-为什么说MVC工程架构已经过时

二、SpringMVC的简单使用

理解了什么是MVC后,我们开始开发一个简单Demo项目

注:下面的demo应用摘自第一个Spring MVC程序,我根据示例做出了点修改

1.利用配置开发第一个SpringMVC项目

①创建 Web 应用并引入 JAR 包

在idea中创建Java EE项目
在这里插入图片描述

在这里插入图片描述
命名可以随意。

创建完成后在pom.xml写入配置,导入相关jar包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dreamchaser</groupId>
    <artifactId>demo-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>demo-springmvc</name>
    <packaging>war</packaging>

    <dependencies>
        <!--测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!--日志-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
        <!--J2EE-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <!--mysql驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.35</version>
        </dependency>
        <!--springframework-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.github.stefanbirkner</groupId>
            <artifactId>system-rules</artifactId>
            <version>1.16.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.9</version>
        </dependency>
        <!--其他需要的包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>
    </dependencies>

</project>

②Spring MVC 配置

在 web.xml 中配置 Servlet,创建 Spring MVC 的配置文件。

Spring MVC 是基于 Servlet 的,DispatcherServlet 是整个 Spring MVC 框架的核心,主要负责截获请求并将其分派给相应的处理器处理。所以配置 Spring MVC,首先要定义 DispatcherServlet。跟所有 Servlet 一样,用户必须在 web.xml 中进行配置。

1)定义DispatcherServlet

在开发 Spring MVC 应用时需要在 web.xml 中部署 DispatcherServlet,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>springMVC</display-name>
    <!-- 部署 DispatcherServlet -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 表示容器再启动时立即加载servlet -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <!-- 处理所有URL -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Spring MVC 初始化时将在应用程序的 WEB-INF 目录下查找配置文件,该配置文件的命名规则是“servletName-servlet.xml”,例如 springmvc-servlet.xml。

也可以将 Spring MVC 的配置文件存放在应用程序目录中的任何地方,但需要使用 servlet 的 init-param 元素加载配置文件,通过 contextConfigLocation 参数来指定 Spring MVC 配置文件的位置,示例代码如下。

<!-- 部署 DispatcherServlet -->
<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc-servlet.xml</param-value>
    </init-param>
    <!-- 表示容器再启动时立即加载servlet -->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

此处使用 Spring 资源路径的方式进行指定,即 classpath:springmvc-servlet.xml。

上述代码配置了一个名为“springmvc”的 Servlet。该 Servlet 是 DispatcherServlet 类型,它就是 Spring MVC 的入口,并通过 1 配置标记容器在启动时就加载此 DispatcherServlet,即自动启动。然后通过 servlet-mapping 映射到“/”,即 DispatcherServlet 需要截获并处理该项目的所有 URL 请求。

2)创建Spring MVC配置文件

在 WEB-INF 目录下创建 springmvc-servlet.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"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- LoginController控制器类,映射到"/login" -->
    <bean name="/login"
          class="net.biancheng.controller.LoginController"/>
    <!-- LoginController控制器类,映射到"/register" -->
    <bean name="/register"
          class="net.biancheng.controller.RegisterController"/>
</beans>

③创建 Controller(处理请求的控制器)

在 项目目录下创建controller 包,并在该包中创建 RegisterController 和 LoginController 两个传统风格的控制器类(实现 Controller 接口),分别处理首页中“注册”和“登录”超链接的请求。

LoginController 的具体代码如下:

public class LoginController implements Controller {
    
    
    @Override
    public ModelAndView handleRequest(HttpServletRequest arg0,
                                      HttpServletResponse arg1) throws Exception {
    
    
        return new ModelAndView("/WEB-INF/jsp/register.jsp");
    }
}

RegisterController 的具体代码如下:

public class RegisterController implements Controller {
    
    
    @Override
    public ModelAndView handleRequest(HttpServletRequest arg0,
                                      HttpServletResponse arg1) throws Exception {
    
    
        return new ModelAndView("/WEB-INF/jsp/login.jsp");
    }
}

④创建 View

这里采用 JSP 作为视图的模板引擎

index.jsp 代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
    未注册的用户,请
    <a href="${pageContext.request.contextPath }/register"> 注册</a><br /> 已注册的用户,去
    <a href="${pageContext.request.contextPath }/login"> 登录</a></body>
</html>

在 WEB-INF 下创建 jsp 文件夹,将 login.jsp 和 register.jsp 放到 jsp 文件夹下。login.jsp 代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
    登录页面!
</body>
</html>

register.jsp 代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
<body>
    注册页面!
</body>
</html>
</head>

⑤部署运行

配置tomcat
在这里插入图片描述
点击运行
在这里插入图片描述

部署成功!
在这里插入图片描述
在这里插入图片描述

以上便完成了最基本的SpringMVC项目搭建

这里我们可以看到,springmvc-servlet.xml的作用就是写各种配置以及告诉SpringMVC我们业务类的位置,在上面的项目中我们用配置的方式告诉了springmvc我们把Controller放在了哪里以及对应的请求映射。

但是如果每次都这么配,不免有些麻烦。所以一般情况我们都会用注解的方式去实现。

2.利用注解开发

根据我自己的经验,注解的使用是不难的,一般看看示例代码就会了。但是很多时候我们只是看注解概念,这时候你大概率会云里雾里的,一用起来还是不会。

所以我这里以代码的形式去讲解这些常用注解的实际使用。相信大家很快就能上手使用,如果不理解的,大家可以边看边打代码,然后去动手测试一下,相信你很快就能掌握的。

①一般的Controller


/**
 * 一般controller的演示类
 * @author Dreamchaser
 */
//标识其为一个Controller类
@Controller
//在controller类前加@RequestMapping注解,可以为所有方法映射路径加个前缀,比如/getRequest01,它实际的访问路径为/prefix/getRequest01
@RequestMapping("/prefix")
public class TestController {
    
    
    //@RequestMapping请求映射,value值是映射路径,method标识请求的方式,如get,post,delete,put等(这里是一个枚举类型)。
    // springmvc会根据value中的路径匹配,如果访问路径和value一致则会执行该方法,
    @RequestMapping(value = "/getRequest01",method = RequestMethod.GET )
    //返回类型可以有以下几种,Object,String,ModelAndView。
    //1.返回类型为Object,需要有相应的转换器才可以解析成对应字符串,可以使用fastjson,或者其他解析的工具,同时要加上@ResponseBody注解
    //2.返回值类型为String,该返回值视为view路径,springmvc会去寻找相应路径的视图返回。
    // 如果不想自动解析,需要直接返回字符串,则加上注解@ResponseBody 注解即可
    //3.返回类型为ModelAndView,该对象需要设置数据和视图路径,返回该对象,SpringMVC会去数据渲染到相应视图后返回(渲染的模板引擎可以自己设置)
    //@RequestParam用于匹配传如参数和方法参数不相等
    public @ResponseBody Author getRequest01(@RequestParam(name = "uname",required = false) String username){
    
    
        Author author=new Author(username,18);
        return author;
    }
    //@GetMapping(value = "/getRequest02")效果和@RequestMapping(value = "/getRequest02",method = RequestMethod.GET )一致,是一种简写
    //同时@GetMapping("/getRequest02")这样也是可以的
    @GetMapping(value = "/getRequest02")
    public String  getRequest02(String option){
    
    
        if ("视图转发".equals(option)){
    
    
            //会解析成视图,其实这就是一种转发,不过转发的是视图而已
            return "/WEB-INF/jsp/login.jsp";
        }else if ("路径转发".equals(option)){
    
    
            //转发到/getRequest03/{id}这个路径,是服务器内部的转发,这个操作客户是感受不到的
            return "/getRequest03/1";
        }else if ("重定向".equals(option)){
    
    
            //重定向到/getRequest03/{id}这个路径,告知浏览器去访问这个路径,这个操作客户是可以感受到的
            return "redirect:/getRequest03/1";
        }else {
    
    
            return "/WEB-INF/jsp/register.jsp";
        }
    }
    //该路径写法可以配合@PathVariable注解将路径上的数据注入到参数当中
    @GetMapping(value = "/getRequest03/{id}")
    //将路径中{id}的部分注入到@PathVariable(name = "id")注解的参数上,如路径为/getRequest03/1,那么参数i的值就为1
    public @ResponseBody String getRequest03(@PathVariable(name = "id") Integer i){
    
    
        System.out.println(i);
        return "这是数据,不是路径";
    }
    @GetMapping("/getRequest04" )
    //用map类型也可以接受参数,SpringMVC会把参数都put进入map中,不过得在其前面加上@RequestParam注解
    public ModelAndView getRequest04(@RequestParam Map<String,Object> map){
    
    
        ModelAndView mv=new ModelAndView();
        //设置视图的路径
        mv.setViewName("/WEB-INF/jsp/register.jsp");
        //增加数据
        mv.addObject("name","Dreamchaser追梦");
        //添加对象也是可以的,不过要有get和set方法
        mv.addObject("author",new Author("Dreamchaser追梦",21));
        return mv;
    }
    @GetMapping("/getRequest05" )
    //用map类型也可以接受参数,SpringMVC会把参数都put进入map中,不过得在其前面加上@RequestParam注解
    public ModelAndView getRequest05(@RequestParam Map<String,Object> map){
    
    
        ModelAndView mv=new ModelAndView();
        //设置视图的路径
        mv.setViewName("/WEB-INF/jsp/register.jsp");
        //增加数据
        mv.addObject("name","Dreamchaser追梦");
        //添加对象也是可以的,不过要有get和set方法
        mv.addObject("author",new Author("Dreamchaser追梦",21));
        return mv;
    }

    @PostMapping("/PostTest01")
    //但是大多数情况非get请求的参数都不会放在路径上都是放在请求体中,如果要获取请求体中的参数,只需在参数前面加上@RequestBody
    public @ResponseBody String DeleteRequest01(@RequestBody Map<String,Object> map){
    
    
        return "post类型的请求";
    }
    //Post方式的Mapping
    @PostMapping("/PostTest02")
    //如果你需要HttpServletRequest或者HttpServletResponse,只需再参数中加入即可,SpringMVC会自动检查类型,
    //将此次请求的HttpServletRequest和HttpServletResponse注入其中
    public @ResponseBody String PostRequest02(int i, HttpServletRequest request, HttpServletResponse reponse){
    
    
        return "post类型的请求";
    }

②Restful风格的controller

/**
 * @author Dreamchaser
 */
//嫌实现Restful风格的api太麻烦了?直接用@RestController注解,轻松支持restful风格的书写
@RestController
//在controller类前加@RequestMapping注解,可以为所有方法映射路径加个前缀,比如/author,它实际的访问路径为/restful/author
@RequestMapping("/restful")
public class TestRestController {
    
    
    @GetMapping("/author")
    public Author getAuthor(String name,Integer age){
    
    
        return new Author(name,age);
    }
    @PostMapping("/author")
    public boolean addAuthor(String name){
    
    
        return true;
    }
    @DeleteMapping("/author")
    public boolean deleteAuthor(String name){
    
    
        return true;
    }
    @PutMapping("/author")
    public boolean deleteAuthor(String name,Integer age){
    
    
        return true;
    }
}

其实原理也很简单,一看便知。
在这里插入图片描述

注:如果不知道restful风格接口的同学,请自行百度,本文就不再赘述了

③真实项目中controller写法

下面这个Controller类是我的大作业仓库管理系统的UserController,用于用户信息的相关操作,如注册,登录等等。

/**
 * 用户的相关接口
 * @author Dreamchaser追梦
 */
@RestController
public class UserController {
    
    
    @Autowired
    private LoginRealms loginRealms;
    @Autowired
    private UserService userService;
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Autowired
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String fromEmail;

    /**
     * 注册用户(通常为手机或者邮箱注册)
     * @param map 参数列表,包括账号(手机注册就是phone,邮箱就是email)、密码
     * @return 成功则返回凭证,否则返回验证失败
     */
    @PostMapping("/register")
    public RestResponse register(@RequestBody Map<String,Object>map){
    
    
        String principal;
        Object password=map.get("pwd");
        Object code=map.get("code");
        UserToken userToken;
        //判断必要参数是否满足
        if (password==null||code==null){
    
    
            return CrudUtil.ID_MISS_RESPONSE;
        }

        //从map中获取对应参数
        if (map.get("email")!=null){
    
    
            principal=String.valueOf(map.get("email"));
            userToken=new UserToken(LoginType.EMAIl_PASSWORD,principal,String.valueOf(password));
        }else {
    
    
            return CrudUtil.ID_MISS_RESPONSE;
        }
        //验证码正确且成功插入数据
        if (checkCode(principal,String.valueOf(code))){
    
    
            //对密码进行加密然后存储用户信息
            map.put("pwd",Md5.crypt(String.valueOf(map.get("pwd"))));
            //如果用户记录插入成功
            if (userService.insertUser(map)==1){
    
    
                String token= Md5.crypt(userToken.getPrincipal()+userToken.getInstant());
                //返回凭证
                return new RestResponse().setData(token);
            }
        }else {
    
    
            //验证码错误
            return CrudUtil.CODE_ERROR;
        }
        return new RestResponse().setStatus(450).setStatusInfo(new StatusInfo("注册失败,系统繁忙,请稍后再试!","注册失败"));
    }

    /**
     * 验证是否有此账号,然后发送验证码
     * @param map 主要认证主体,如账号,邮箱,qq的openID,wechat的code等
     * @return restResponse,附带凭证token
     */
    @PostMapping("/sendCode")
    public RestResponse sendCode(@RequestBody Map<String,Object> map){
    
    
        if (userService.findUserByCondition(map)==null){
    
    
            String principal;
            if (map.containsKey("phone")){
    
    
                principal=String.valueOf(map.get("phone"));

            }else if (map.containsKey("email")){
    
    
                principal=String.valueOf(map.get("email"));
            }else {
    
    
                return CrudUtil.ID_MISS_RESPONSE;
            }
            //创建一个验证码
            VerificationCode v=new VerificationCode();
            //将验证码存入验证码等待池
            VerificationCodePool.addCode(principal,v);
            //发送邮箱验证码
            sendEmail(principal,v.getCode());
            return new RestResponse();
        }
        return new RestResponse("",304,new StatusInfo("发送验证码失败,该账户已存在!","发送验证码失败,该账户已存在!"));
    }

    /**
     * 登录接口
     * @param map 登录信息
     *  loginType 登录方式,目前支持的有email,qq,wechat
     *  principal 主要认证主体,如账号,邮箱,qq的openID,wechat的code等
     *  credentials 类似于密码,如果是qq,wechat则不需要传改参数
     *  restResponse,附带凭证token
     */
    @PostMapping("/login")
    public RestResponse login(@RequestBody Map<String,String> map) {
    
    
        UserToken userToken=new UserToken(LoginType.getType(map.get("loginType"))
                ,map.get("principal"),map.get("credentials"));
        return login(userToken);
    }

    /**
     * 退出登录,删除令牌的操作依据在拦截器中完成
     * @return RESPONSE200
     */
    @GetMapping("/logout")
    public RestResponse logout() {
    
    
        return CrudUtil.RESPONSE200;
    }

    @GetMapping("/sys/users")
    public RestResponse findUsers(@RequestParam Map<String,Object> map) {
    
    
        return new RestResponse(userService.findUserPsByCondition(map),userService.findCount(),200);
    }
    @PostMapping("/sys/user")
    public RestResponse addUser(@RequestBody Map<String,Object> map) {
    
    
        //对密码进行加密
        map.put("pwd",Md5.crypt(String.valueOf(map.get("pwd"))));
        return CrudUtil.postHandle(userService.insertUser(map),1);
    }
    @PutMapping("/sys/user")
    public RestResponse updateUser(@RequestBody Map<String,Object> map) {
    
    
        if (map.containsKey("pwd")&&map.get("pwd")!=""){
    
    
            //对密码进行加密
            map.put("pwd",Md5.crypt(String.valueOf(map.get("pwd"))));
        }
        return CrudUtil.postHandle(userService.updateUser(map),1);
    }
    @DeleteMapping("/sys/user")
    public RestResponse deleteUser(@RequestBody Map<String,Object> map) {
    
    
        return deleteHandle(userService.deleteUserById(ObjectFormatUtil.toInteger(map.get("id"))),1);
    }
    @DeleteMapping("/sys/users")
    public RestResponse deleteUsers(@RequestBody Map<String,Object> map) {
    
    
        if (map.containsKey("ids")){
    
    
            List<Integer> ids=(List<Integer>) map.get("ids");
            return CrudUtil.deleteHandle(userService.deleteUserByIds(ids),ids.size());
        }else {
    
    
            return CrudUtil.ID_MISS_RESPONSE;
        }
    }

    @PutMapping("/user")
    public RestResponse updateSelf(@RequestBody Map<String,Object> map, HttpServletRequest request) {
    
    
        UserToken userToken= (UserToken) request.getAttribute("userToken");
        map.put("id",userToken.getUser().getId());

        return CrudUtil.postHandle(userService.updateUserNoSensitive(map),1);
    }
    @PutMapping("/checkOldEmail")
    public RestResponse checkOldEmail(@RequestBody Map<String,Object> map, HttpServletRequest request) {
    
    
        if (!map.containsKey("oldCode")){
    
    
            return CrudUtil.ID_MISS_RESPONSE;
        }
        UserToken userToken= (UserToken) request.getAttribute("userToken");
        if (checkCode(userToken.getPrincipal(),  map.get("oldCode").toString())){
    
    
            return new RestResponse("验证成功!");
        }else {
    
    
            //验证码错误
            return CrudUtil.CODE_ERROR;
        }
    }
    @PutMapping("/updateEmail")
    public RestResponse updateEmail(@RequestBody Map<String,Object> map, HttpServletRequest request) {
    
    
        //参数检测
        if (!map.containsKey("email")||!map.containsKey("oldCode")||!map.containsKey("newCode")){
    
    
            return CrudUtil.ID_MISS_RESPONSE;
        }
        UserToken userToken= (UserToken) request.getAttribute("userToken");
        //必须同时检测,否则会出现漏洞
        if (checkCode(userToken.getPrincipal(),map.get("oldCode").toString())
                &&checkCode(map.get("email").toString(),  map.get("code").toString())){
    
    
            map.put("id",userToken.getUser().getId());
            return CrudUtil.putHandle(userService.updateUser(map),1);
        }else {
    
    
            //验证码错误
            return CrudUtil.CODE_ERROR;
        }

    }


    /**
     * 将生成的令牌拿去认证,如果认证成功则返回带有token凭证响应,否则返回用户密码错误的响应
     * @param userToken 未认证的令牌
     * @return restResponse 如果认证成功则返回带有token凭证响应,否则返回用户密码错误的响应
     */
    private RestResponse login(UserToken userToken) {
    
    
        String token=loginRealms.authenticate(userToken);
        if (token!=null){
    
    
            return new RestResponse(token);
        }else {
    
    
            return CrudUtil.NOT_EXIST_USER_OR_ERROR_PWD_RESPONSE;
        }
    }

    /**
     * 用于注册用户的方法,主要为号码验证和邮箱验证提供验证码核对的服务
     * @param principal 认证主体
     * @param code 验证码
     * @return 是否验证通过
     */
    private boolean checkCode(String principal,String code){
    
    
        if (code!=null){
    
    
            VerificationCode verificationCode=VerificationCodePool.getCode(principal);
            if (verificationCode!=null){
    
    
                return code.equals(verificationCode.getCode());
            }
        }
        return false;
    }

    /**
     * 发送带有验证码的邮件信息
     */
    private void sendEmail(String email,String code){
    
    
        //发送验证邮件
        try {
    
    
            SimpleMailMessage mailMessage = new SimpleMailMessage();

            //主题
            mailMessage.setSubject("仓库管理系统的验证码邮件");

            //内容
            mailMessage.setText("欢迎使用仓库管理系统,您正在注册此账户。" +
                    "\n您收到的验证码是: "+code+" ,请不要将此验证码透露给别人。");

            //发送的邮箱地址
            mailMessage.setTo(email);
            //默认发送邮箱邮箱
            mailMessage.setFrom(fromEmail);

            //发送
            mailSender.send(mailMessage);
        }catch (Exception e){
    
    
            throw new MyException(e.toString());
        }
    }
}

因为实际项目中,Controller层是要引用Service层的类的,所以都会用Spring的@Autowired注解注入进来。

④注解开发的配置

使用注解开发需要进行一些配置。具体配置步骤如下:

1)注解扫描

在之前创建的Springmvc-servlet.xml配置文件中的beans标签下面添加一句

<context:component-scan base-package="com.dreamchaser.demo_springmvc.controller" />

上面的意思是开启注解扫描,指定扫描包为com.dreamchaser.demo_springmvc.controller,这样SpringMVC会在启动时扫描该包下的注解,并将相应的类注册到SpringMVC里。

2)对象转换

需要注意的是,如果返回值为对象,需要有相应的转化器,这里我们可以用阿里的fastjson,然后将fastjson的转化器注册进去。
步骤如下:

1.在pom.xml中添加依赖

<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.72</version>
</dependency>

2.将FastJsonHttpMessageConverter注册进SpringMVC

有两种方式:

①配置文件配置

<mvc:annotation-driven>
        <mvc:message-converters register-defaults="false">
            <bean id="fastJsonHttpMessageConverter"
                  class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <!-- 加入支持的媒体类型:返回contentType -->
                <property name="supportedMediaTypes">
                    <list>
                        <!-- 这里顺序不能反,一定先写text/html,不然ie下会出现下载提示 -->
                        <value>text/html;charset=UTF-8</value>
                        <value>application/json;charset=UTF-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

②代码配置
创建SpringMVC的配置类

/**
 * @author Dreamchaser
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    
    
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        //自定义fastjson配置
        FastJsonConfig config = new FastJsonConfig();
        config.setSerializerFeatures(
                // 是否输出值为null的字段,默认为false,我们将它打开
                SerializerFeature.WriteMapNullValue,
                // 将Collection类型字段的字段空值输出为[]
                SerializerFeature.WriteNullListAsEmpty,
                // 将字符串类型字段的空值输出为空字符串
                SerializerFeature.WriteNullStringAsEmpty,
                // 将数值类型字段的空值输出为0
                SerializerFeature.WriteNullNumberAsZero,
                SerializerFeature.WriteDateUseDateFormat,
                // 禁用循环引用
                SerializerFeature.DisableCircularReferenceDetect
        );
        fastJsonHttpMessageConverter.setFastJsonConfig(config);

        // 添加支持的MediaTypes;不添加时默认为*/*,也就是默认支持全部
        // 但是MappingJackson2HttpMessageConverter里面支持的MediaTypes为application/json
        // 参考它的做法, fastjson也只添加application/json的MediaType
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
        converters.add(fastJsonHttpMessageConverter);
    }
}

3.Interceptor拦截器

如果我想在每个controller执行前去执行一些特定的操作,这该怎么办呢?
比如我想在controller方法执行前判断一下该用户是否有资格去执行,这个鉴权需求在很多系统都很常见,那么我该如何实现呢?

别急,SpringMVC提供了Interceptor拦截器机制,你只需继承相应的类,覆写相应的方法,并将其配置到SpringMVC的配置中即可。

就比如下面这样:
1.编写Interceptor


/**
 * 认证拦截器,如果请求头中有相应凭证则放行,否则拦截返回认证失效错误
 * @author Dreamchaser追梦
 */
@Slf4j
//spring中的注解,主要创建这个类的单例对象并注册进spring容器中
@Component
//一般只需继承HandlerInterceptorAdapter 即可,如果有其他需求可以继承其他拦截器类
public class UserInterceptor extends HandlerInterceptorAdapter {
    
    

	//覆写方法,prehandle会在所有controller方法前执行,这里只覆写了prehandle方法,同时也是可以覆写其他方法
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws MyException {
    
    

        //拿到requset中的head
        String token =null;
        Cookie[] cookies=request.getCookies();
        if (cookies!=null){
    
    
            for (Cookie c:cookies){
    
    
                if (c.getName().equals("token")){
    
    
                    token=c.getValue();
                    break;
                }
            }
        }
        //如果是访问logout则删除对应的令牌
        if ("/logout".equals(request.getServletPath())){
    
    
            AuthenticationTokenPool.removeToken(token);
            return true;
        }
        if (token!=null&&AuthenticationTokenPool.getToken(token)!=null){
    
    
            request.setAttribute("userToken",AuthenticationTokenPool.getToken(token));
            return true;
        }else {
    
    
            try {
    
    
                response.sendRedirect("/login");
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
            return false;
        }
    }
}

2.编写配置类,将拦截器配置到SpringMVC中

/**
 * @author Dreamchaser
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    

	//增加一个拦截器,并配置拦截路径为/**,即拦截所有接口
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(new UserInterceptor())
                .addPathPatterns("/**");
    }
}

这样就实现了一个简单的拦截器。

以上演示代码我已经放到gitee上了,感兴趣的同学可以拉下来看看,gitee地址

三、SpringMVC进阶——SpringMVC原理探究

当你可以熟练使用SpringMVC的相关操作后,恭喜你,你已经成功了解SpringMVC了。接下来如果你想熟悉SpringMVC,对于SpringMVC的理解更近一步的话,那么你需要知道SpringMVC的原理。

1.SpringMVC机制是建立在servlet之上的

SpringMVC是建立在Servlet基础上的,这是因为SpringMVC核心DispatcherServlet类便是一个Servlet。它是一个前端控制器,用于控制整体的操作,包括分发请求,控制各个环节的执行等。
在这里插入图片描述

2.SpringMVC处理流程

SpringMVC 的执行流程如下:

  1. 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器);
  2. 由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。
  3. DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
  4. HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller);
  5. Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息);
  6. HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
  7. DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
  8. ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet;
  9. DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
  10. 视图负责将结果显示到浏览器(客户端)。

在这里插入图片描述
通过上图我们可以清楚了解到SpringMVC的流程机制,对于其运作有了一个总体的印象,但是对于一些细节还不甚了解,比如这里的Handle是什么?HandleMapping又是指什么?执行链又是什么东东?
你是不是有点懵?

别急,我们来看源码理解这些操作。

3.SpringMVC源码探究

注:以下源码探究,建议自己也打开源码跟着走一遍,这样效果最好。
博主的SpringMVC源码版本为5.2.3.RELEASE,不同版本可能会有些许区别

因为我们之前已经搭好了环境,现在用单步调试的方式来快速阅读源码。

先打好断点,然后点击调试。
在这里插入图片描述

访问之前写的接口localhost:9090/prefix/getRequest04
因为源码里细节很多,有些我也不是特别明白,所以下面讲解中我只挑重要的讲。

①将各种参数加入Request的attributes中

在这里插入图片描述

和一个servlet一样,请求到来时,会执行doService方法。
他这里一开始调用setAttribute方法将各种参数(如上下文对象,本地解析器等等)添加到该Request的attribute中。

我猜测这里是为了后续操作的方便,所以统一将各种参数配置到Request中。

②分派请求方法

确定了环境后,SpringMVC就会分派请求,调用doDispatch方法,该方法是SpringMVC的核心。
在这里插入图片描述
里面涉及非常多的操作,生成时序图发现这个调用非常复杂。

在这里插入图片描述

不过我们只需抓住核心即可,下面我们继续进入该方法。

③根据请求获取对应的执行链

进入doDispatch方法,这里主要专注于getHandler方法和getHandlerAdapter方法
在这里插入图片描述

在这里插入图片描述
这里调用了getHandler方法去获取对应的执行链。

如果不知道执行链是什么的,可以继续往下看,下面会讲。

可以先看看以下的时序图,对于之后的深入有个大致了解。

在这里插入图片描述
点开第一层调用getHandler方法,这是DispatcherServlet类的一个封装方法。

1)循环遍历寻找可以匹配的处理器映射器

在这里插入图片描述
我们看到这里去循环遍历全局变量handlerMappings,handlerMappings实际上是个list,里面存储的是这个是处理器映射器。
在这里插入图片描述
点开调试的变量
在这里插入图片描述

这里我们从名字上能大致猜到意思,handlerMappings存储了两个(目前这个环境时两个,可能还会有其他的)不同方式的处理映射器,分别是根据用户request信息中提供的Method来查找handler,根据用户请求信息中的URL查找handler。

从网上找了一张架构图,如下:

在这里插入图片描述

可以看到HandlerMapping家族有两个分支,分别是AbstractUrlHandlerMapping和AbstractHandlerMethodMapping,它们又统一继承于AbstractHandlerMapping。

AbstractUrlHandlerMapping中的getHandlerInternal方法会根据用户请求信息中的URL查找handler。
AbstractHandlerMethodMapping中的getHandlerInternal方法则会根据用户request信息中提供的Method来查找handler。

回到getHandler方法上来
在这里插入图片描述
这里循环查看处理器映射器,调用其getHandler方法,如果可以返回执行链,则说明该映射器可以找到,直接返回即可。也就是说SpringMVC提供了多种匹配controller方法的方式,它会根据顺序返回匹配的第一个。

2)根据映射器里的规则去获取执行链对象

2.1 获取handler对象(controller请求方法的封装对象)

进入处理器映射器的getHandler方法
在这里插入图片描述
这里会尝试根据自己的方法获取对应的Controller方法,点开运行下去
在这里插入图片描述
这里会调用RequestMappingHandlerMapping的getHandlerInternal方法,而此方法会调用父类AbstractHandlerMethodMapping的getHandlerInternal方法
在这里插入图片描述
进入lookupHandlerMethod方法,我们发现AbstractHandlerMethodMapping类中有个mappingRegistry全局变量。

点开类信息
在这里插入图片描述
然后对比调试变量,
在这里插入图片描述

不难发现这个mappingRegistry是个存储controller信息的地方,里面的urlLookup存储了路径和对应处理器对象。其中key是请求路径,value是一个HandlerMethod的List。

我们点开HandlerMethod类后,可以发现里面其实汇集了处理器方法的各种信息,包括反射获取的method对象和注解信息等。
在这里插入图片描述

回到lookupHandlerMethod方法,这样我们就不难理解了
在这里插入图片描述
这里有个很有意思的地方,他这里先去直接匹配路径,因为路径信息存放在map中,直接通过key去获取时间复杂度为O(1),然后再在匹配好的路径handler中寻找一个与映射信息中的所有条件与提供的请求匹配的映射信息(请求方式,参数类型个数等等)加入matches中。

但是这样并不能完成要求,因为有一些有通配符的路径,比如/*,所以只有在搜索不到的时候才会全局搜索,这时候就会去用字符串匹配的算法,这个算法在PatternsRequestCondition类中(具体就不继续下去了)。

这样做的目的就是为了尽可能提高效率,充分利用map的特性。

@Nullable
private String getMatchingPattern(String pattern, String lookupPath) {
    
    
		if (pattern.equals(lookupPath)) {
    
    
			return pattern;
		}
		if (this.useSuffixPatternMatch) {
    
    
			if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
    
    
				for (String extension : this.fileExtensions) {
    
    
					if (this.pathMatcher.match(pattern + extension, lookupPath)) {
    
    
						return pattern + extension;
					}
				}
			}
			else {
    
    
				boolean hasSuffix = pattern.indexOf('.') != -1;
				if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
    
    
					return pattern + ".*";
				}
			}
		}
		if (this.pathMatcher.match(pattern, lookupPath)) {
    
    
			return pattern;
		}
		if (this.useTrailingSlashMatch) {
    
    
			if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
    
    
				return pattern + "/";
			}
		}
		return null;
	}

获得到匹配的handler后,他比对该匹配后的数组是不是只有一个,如果没有就对外报错,这也是为什么会看到Ambiguous handler methods mapped forXXXX 这样的错误,这是因为找到的handler不唯一。
在这里插入图片描述

但是需要注意的是——因为之前采用“先匹配直接路径,匹配不到才全局匹配”的方式,所以如果有handler能直接匹配到请求路径(一模一样),那么就算还有其他handler比如/*能匹配到,它依旧是只执行那个直接匹配的handler,并不会报错。

2.2 封装执行链对象,添加对应的拦截器对象

好了我们继续回到先前的方法
在这里插入图片描述
在获取到对应的handler对象后,他会创建一个处理器执行链,然后把Interceptor拦截器对象加入其中。

这个过程封装在getHandlerExecutionChain方法中,进入看看
在这里插入图片描述
至于匹配算法,他调用的spring中util包下的AntPathMatcher的doMatch方法

在这里插入图片描述

具体算法贴在下面(因为最近项目中也要有字符解析的需求,然后我就特别留意这方面的写法),大家有时间也可以借鉴一下

protected boolean doMatch(String pattern, @Nullable String path, boolean fullMatch, @Nullable Map<String, String> uriTemplateVariables) {
    
    
        if (path != null && path.startsWith(this.pathSeparator) == pattern.startsWith(this.pathSeparator)) {
    
    
            String[] pattDirs = this.tokenizePattern(pattern);
            if (fullMatch && this.caseSensitive && !this.isPotentialMatch(path, pattDirs)) {
    
    
                return false;
            } else {
    
    
                String[] pathDirs = this.tokenizePath(path);
                int pattIdxStart = 0;
                int pattIdxEnd = pattDirs.length - 1;
                int pathIdxStart = 0;

                int pathIdxEnd;
                String pattDir;
                for(pathIdxEnd = pathDirs.length - 1; pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd; ++pathIdxStart) {
    
    
                    pattDir = pattDirs[pattIdxStart];
                    if ("**".equals(pattDir)) {
    
    
                        break;
                    }

                    if (!this.matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
    
    
                        return false;
                    }

                    ++pattIdxStart;
                }

                int patIdxTmp;
                if (pathIdxStart > pathIdxEnd) {
    
    
                    if (pattIdxStart > pattIdxEnd) {
    
    
                        return pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator);
                    } else if (!fullMatch) {
    
    
                        return true;
                    } else if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
    
    
                        return true;
                    } else {
    
    
                        for(patIdxTmp = pattIdxStart; patIdxTmp <= pattIdxEnd; ++patIdxTmp) {
    
    
                            if (!pattDirs[patIdxTmp].equals("**")) {
    
    
                                return false;
                            }
                        }

                        return true;
                    }
                } else if (pattIdxStart > pattIdxEnd) {
    
    
                    return false;
                } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
    
    
                    return true;
                } else {
    
    
                    while(pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
    
    
                        pattDir = pattDirs[pattIdxEnd];
                        if (pattDir.equals("**")) {
    
    
                            break;
                        }

                        if (!this.matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
    
    
                            return false;
                        }

                        --pattIdxEnd;
                        --pathIdxEnd;
                    }

                    if (pathIdxStart > pathIdxEnd) {
    
    
                        for(patIdxTmp = pattIdxStart; patIdxTmp <= pattIdxEnd; ++patIdxTmp) {
    
    
                            if (!pattDirs[patIdxTmp].equals("**")) {
    
    
                                return false;
                            }
                        }

                        return true;
                    } else {
    
    
                        while(pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
    
    
                            patIdxTmp = -1;

                            int patLength;
                            for(patLength = pattIdxStart + 1; patLength <= pattIdxEnd; ++patLength) {
    
    
                                if (pattDirs[patLength].equals("**")) {
    
    
                                    patIdxTmp = patLength;
                                    break;
                                }
                            }

                            if (patIdxTmp == pattIdxStart + 1) {
    
    
                                ++pattIdxStart;
                            } else {
    
    
                                patLength = patIdxTmp - pattIdxStart - 1;
                                int strLength = pathIdxEnd - pathIdxStart + 1;
                                int foundIdx = -1;
                                int i = 0;

                                label144:
                                while(i <= strLength - patLength) {
    
    
                                    for(int j = 0; j < patLength; ++j) {
    
    
                                        String subPat = pattDirs[pattIdxStart + j + 1];
                                        String subStr = pathDirs[pathIdxStart + i + j];
                                        if (!this.matchStrings(subPat, subStr, uriTemplateVariables)) {
    
    
                                            ++i;
                                            continue label144;
                                        }
                                    }

                                    foundIdx = pathIdxStart + i;
                                    break;
                                }

                                if (foundIdx == -1) {
    
    
                                    return false;
                                }

                                pattIdxStart = patIdxTmp;
                                pathIdxStart = foundIdx + patLength;
                            }
                        }

                        for(patIdxTmp = pattIdxStart; patIdxTmp <= pattIdxEnd; ++patIdxTmp) {
    
    
                            if (!pattDirs[patIdxTmp].equals("**")) {
    
    
                                return false;
                            }
                        }

                        return true;
                    }
                }
            }
        } else {
    
    
            return false;
        }
    }

好了目前为止我们根据请求request拿到了Handler执行链(包含handler处理方法以及对应的拦截器对象适应)

④获取对应的handler适配器

在这里插入图片描述
进入getHandlerAdapter方法

在这里插入图片描述
在这里插入图片描述
它会去判断该handler是否支持该适配器。然后他会返回一个支持这个handler的适配器,因为我访问的路径匹配的是一个方法,所以他返回了RequestMappingHandlerAdapter。

PS:为了让大家更好的理解,我这里提前先说明为什么要有适配器(这是我看到后面才明白的)。
你如果看到后面,会发现他会执行这个handler。而handler怎么执行?如果只有一种执行方式,那大可不必有适配器这种东西,问题就在于他有多种执行方式。

像本文开始时的Controller实际上是实现了Controller接口,而该接口只有一种方法handleRequest,如果执行这种handler只需调用这个方法即可。而我们后来用注解实现的Controller中,我们路径的匹配是方法级别的,而且并没有实现接口,也就说SpringMVC不能显示的调用某个固定的方法,面对这种handler的执行只能通过反射机制去完成。

面对handler的不同,执行方法的不同,我们该如何处理呢?

一种方式便是借鉴设计模式中的适配器模式,增加适配器来适应各种多变的情况。SpringMVC也是这么做的。

//RequestMappingHandlerAdapter的handleInternal方法
@Override
	protected ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    
    

		ModelAndView mav;
		checkRequest(request);

		// Execute invokeHandlerMethod in synchronized block if required.
		if (this.synchronizeOnSession) {
    
    
			HttpSession session = request.getSession(false);
			if (session != null) {
    
    
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
    
    
					mav = invokeHandlerMethod(request, response, handlerMethod);
				}
			}
			else {
    
    
				// No HttpSession available -> no mutex necessary
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
    
    
			// No synchronization on session demanded at all...
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}

		if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
    
    
			if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
    
    
				applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
			}
			else {
    
    
				prepareResponse(response);
			}
		}

		return mav;
	}
//SimpleControllerHandlerAdapter的handle方法
@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
    
    

		return ((Controller) handler).handleRequest(request, response);
	}

RequestMappingHandlerAdapter具体调用很深,很多细节我也不太理解,所以这里也不多展示了。

⑤执行拦截器的applyPreHandle方法

在这里插入图片描述

在这里执行了拦截器的applyPreHandle方法。

⑤利用获取的适配器去执行handler

为什么会有适配器,以及适配器的工作我在上面已经说了
在这里插入图片描述
这里执行完handler会获得一个ModelAndView对象。

⑥执行拦截器的applyPostHandle方法

在这里插入图片描述

⑦选择视图解析器渲染视图

这里我不多展示了,看了源码,大致就是会判断是否需要渲染,如果需要就选择一个视图解析器把数据渲染到视图上面。

不过视图解析器的包要实现View接口
在这里插入图片描述
然后调用render方法,渲染的结果是一个对象
在这里插入图片描述
然后会将结果写到response中。

⑧所有操作完成后执行拦截器的afterCompletion方法

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在映射的 HandlerInterceptors 上触发 afterCompletion 回调。将只为 preHandle 调用已成功完成并返回 true 的所有拦截器调用 afterCompletion。
从源码中可以知道不论这个过程中是否出现异常都会调用afterCompletion方法。

⑨异步请求调用afterConcurrentHandlingStarted方法

在这里插入图片描述

在AsyncHandlerInterceptor拦截器中官方是这么解释的:
在这里插入图片描述

到此SpringMVC的大致流程就结束了。

SpringMVC的源码大体如上,不过我忽略一些异步代码,以我目前的水平,对于一些细节地方(比如并发执行等)也不太明白。如果以后有新的理解我再回来补充。

总结

对于上述源码探究,我最深的一个感受就是调用方法颇多,设计的东西也很多,很多时候你在看这块的时候,看着看着就到另一个包去了,甚至跑出了SpringMVC的范围。

不过粗略的过了一遍源码后,我不禁感叹设计的魅力——在如此繁多的类中依然能有条理的将代码组织起来。正是这巧妙的算法策略、合理的设计模式以及面向对象思想的灵活使用,才铸就了这点!

如果大家有什么意见或者建议,都可以在博文下方评论。


愿我们以梦为马,不负人生韶华!
与君共勉。

猜你喜欢

转载自blog.csdn.net/qq_46101869/article/details/118674073
今日推荐