Spring MVC系列之Spring MVC的前端控制器(DispatcherServlet)工作流程及原理

Posted 一宿君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC系列之Spring MVC的前端控制器(DispatcherServlet)工作流程及原理相关的知识,希望对你有一定的参考价值。

五、Spring MVC的工作流程执行图

5.1 DispatcherServlet流程中每一步的含义

  1. 用户通过浏览器向服务器发送请求。请求会被Spring MVC的DispatcherServlet(前端控制器)所拦截
  2. DispatcherServlet拦截到请求后,会通过HandlerMapping处理器映射器
  3. 处理器映射器根据请求URL找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
  4. DispatcherServlet会通过返回信息选择合适的HandlerAdapter(处理器适配器)
  5. HandlerAdpater会调用并执行Handler(处理器)这里的处理器指的就是程序中Controller类,也被称为后端控制器;
  6. Controller执行后会返回一个ModelAndView对象,该对象中会包含视图名或包含模型视图名
  7. HandlerAdapter将ModelAndView对象返回给DispatcherServlet;
  8. DispatcherServlet会根据ModelAndView对象选择一个合适ViewResolver(视图解析器)
  9. ViewResolver解析后,会向DispatcherServlet中返回一个具体的View(视图)
  10. DispatcherServlet对View进行渲染(即将模型数据填充至视图中);
  11. 视图渲染结果会返回给客户端浏览器显示。

5.2 DispatcherServlet前端控制器的内部工作原理机制

我们要清楚DispatcherServlet是怎样拦截到我们在浏览器地址栏中发出的请求,以及通过我们的请求找到控制器类中对应的请求方法,并获取到方法的返回值,通过视图解析器找到对应的视图,最终通过请求转发跳转到我们想要请求的页面中。

  • web.xml
    <?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_3_1.xsd"
             version="3.1">
    
      <display-name>Archetype Created Web Application</display-name>
    
      <!--配置servlet调度程序-->
      <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--启动tomcat服务器时,默认加载applicationContext-springmvc.xml配置文件-->
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath*:applicationContext-springmvc.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>
    
    </web-app>
    

我们知道在web.xml文件中配置前端控制器,但是内部原理是怎么实现的,下面可以通过一个简单的例子来演示下原理,我们不使用系统提供的前端控制器类,我们自己
新建一个DispatcherServlet类。

使用maven创建一个springmvc-DispatcherServlet-principle的Web项目,只需导入javax.servlet.api的依赖文件:

  • pom.xml

    <!--servlet-api-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>compile</scope>
    </dependency>
    
  • DispatcherServlet类(要继承HttpServlet类)

    package com.servlet;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-25 15:04:05
     */
    public class DispatcherServlet extends HttpServlet {
    
    
        public DispatcherServlet() {
            System.out.println("构造方法");
        }
    
        @Override
        public void init() throws ServletException {
            System.out.println("初始化方法---作用是为了存储通过注解扫描controller类下所有@RequestMapping注解标注的value值(请求名)");
        }
    
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
           doPost(req,resp);
        }
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doPost(req,resp);
        }
    
    }
    
  • springmvc-config.xml(此文件可以为空,目前我们暂不进行注解配置的操作

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:bean="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           https://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/context
           https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
           http://www.springframework.org/schema/mvc
           https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
    
    </beans>
    
  • web.xml

    <?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_3_1.xsd"
             version="3.1">
    
      <display-name>Archetype Created Web Application</display-name>
    
      <!--配置servlet调度程序-->
      <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>com.servlet.DispatcherServlet</servlet-class>
        <!--启动tomcat服务器时,默认加载springmvc-config.xml配置文件-->
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath*:springmvc-config.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>
    
    </web-app>
    

  • 创建UserControler控制器类

    package com.controller;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-25 15:11:26
     */
    public class UserController {
    
        //请求到达登录界面
        public String toLogin(){
            return "login";
        }
    
        //请求到达注册界面
        public String toRegister(){
            return "register";
        }
    
        //请求到达删除界面
        public String toDelete(){
            return "delete";
        }
    }
    

在webapp目录下建三个页面:

  • login.jsp
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <h1>这是login.jsp页面</h1>
    </body>
    </html>
    
  • register.jsp页面
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <h1>这是register.jsp页面</h1>
    </body>
    </html>
    
  • delete.jsp
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
    <h1>这是delete.jsp页面</h1>
    </body>
    </html>
    
    到这一步我们先将项目部署到tomcat服务器上,先保证运行正常!

    没问题,进行下一步操作!

5.3 Spring MVC注解驱动原理

  • springmvc.xml

    <!--开启springmvc注解驱动-->
    <mvc:annotation-driven/>
    <!--指定springmvc扫描哪个包-->
    <context:component-scan base-package="com.controller"/>
    
    <!--内部资源视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--扫描以.jsp为后缀的jsp页面-->
        <property name="suffix" value=".jsp"/>
        <!--扫描/WEB-INF下的所有jsp-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
    </bean>
    


    开启注解驱动,我们就无需关注前端控制器是如何帮我们找到相应的视图以及解析,但是原理我们还是要清楚的,如下重点:

  • DispatcherServlet类中init()初始化方法:

    /**
     * 用于存储请求访问名字
     */
    Map<String,String> mappings = new HashMap<>();
    
    @Override
        public void init() throws ServletException {
            System.out.println("初始化方法---作用是为了存储通过注解扫描controller类下所有@RequestMapping注解标注的value值(请求名)");
            mappings.put("toLogin","com.controller.UserController.toLogin");
            mappings.put("toRegister","com.controller.UserController.toRegister");
            mappings.put("toDelete","com.controller.UserController.toDelete");
        }
    

  • DispatcherServlet类中的doGet方法

     @Override
     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //1 获取请求URI
            String requestURI = req.getRequestURI();
            System.out.println(requestURI);
            //2 获取请求URI名称(从最后一个/处,加1截取)
            String requestURIName = requestURI.substring(requestURI.lastIndexOf("/") + 1);
            System.out.println(requestURIName);
    }
    

  • 到这步我们运行下项目,在地址栏输入toLogin,看控制台:

当我们得到请求地址名后,再去map集合中进行对比,查看我们事先初始化时(此处是模拟springmvc通过开启注解扫描将@RequestMapping注解标注的方法存入集合中)存入集合中的方法,是否有有我们现在所发出的请求地址名,如果有则进行下一步视图解析操作,如果无,则说明该请求无效。

  • DispatcherServlet类中的doGet方法:
    	@Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //1 获取请求URI
            String requestURI = req.getRequestURI();
            System.out.println(requestURI);
            //2 获取请求URI名称(从最后一个/处,加1截取)
            String requestURIName = requestURI.substring(requestURI.lastIndexOf("/") + 1);
            System.out.println(requestURIName);
    
            /**
             * 获取到请求名后,与我们实现存在map集合中的键值对比,是否存在
             */
            if(mappings.containsKey(requestURIName )){
                //获取请求URI名称---在控制类中的所对应的方法的路径
                String classPath = mappings.get(requestURIName);
                System.out.println(classPath);
                //获取方法所在类的路径
                String className = classPath.substring(0,classPath.lastIndexOf("." ));
                System.out.println(className);
                //获取所对应的方法名(从最后一个“.”加1截取)
                String methodName = classPath.substring(classPath.lastIndexOf(".") + 1);
                System.out.println(methodName);
                try {
                    //通过反射加载到类
                    Class clz = Class.forName(className);
                    //创建加载类对象
                    Object obj = clz.getDeclaredConstructor().newInstance();
                    //获取对象中的指定请求方法
                    Method method = clz.getDeclaredMethod(methodName,null);
                    //执行反射中对应的请求方法,并获取返回值(也即是页面名称)
                    Object page = method.invoke(obj,null);
                    System.out.println(page);
                    req.getRequestDispatcher(page + ".jsp").forward(req,resp);
    
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
            }else {
                throw new ServletException("请求的URI不存在!");
            }
        }
    

注意上述请求转发request.getRequestDispatcher(page + “.jsp”).forward(req,resp);
一定要交.jsp 后缀名,因为我们在web.xml配置文件中,配置的是< url-pattern>/< /url-pattern>,不会拦截.jsp文件,其余请求以及静态资源都会被当做请求拦截。

我们重启项目,在地址栏输入toLogin

查看控制台:

同理再地址栏输入toRegister和toDelete:

我觉类通过上述简单的详解,多少有点懂了吧,Spring MVC强不强,你我说了都不算,就像苏大强差不多,没有蔡根花小宝贝,苏大强是真的强!!!妥了~

以上是关于Spring MVC系列之Spring MVC的前端控制器(DispatcherServlet)工作流程及原理的主要内容,如果未能解决你的问题,请参考以下文章

[Interview]Java 面试宝典系列之 Spring MVC

Shiro系列之Shiro+Spring MVC整合(Integration)

Shiro系列之Shiro+Spring MVC整合(Integration)

Shiro系列之Shiro+Spring MVC整合(Integration)

Shiro系列之Shiro+Spring MVC整合(Integration)

Spring系列之手写一个SpringMVC