“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第一篇文章,点击检查活动概况”。

导言

关于SpringMVC信任诸位并不生疏,这是Java开发进程中运用最频繁的结构,在你的项目中或许不一定用MyBatis,但肯定会运用SpringMVC,因为操作数据库还有Hibernate、JPA等其他ORM结构挑选,但SpringMVC这个结构在其领域中,可谓是独领风骚,因而在面试中也会常常问到一些与之相关的面试题,其间最为经典的则是那道:

SpringMVC在发动后是怎么作业的?(作业原理)

关于这题的答案,信任咱们在“Java面试八股文”中肯定背过,但之前大多数小伙伴应该也仅仅死记,并未真实的了解其间心原理,那本篇的意图就在于让诸位真实的掌握SpringMVC原理。当然,为了更好的了解,咱们也不会以之前分析底层时的那种源码办法,对其进行长篇概述,本次则运用一种新的办法来对其进行原理讲解。

那新的办法是什么呢?那便是自己手写结构,真实的了解便是自己能够把轮子重新造一次,这本来源码的办法愈加形象,也能够愈加让咱们对其原理印象深入。

在之后有或许会写的《源码分析》专题中,会再次详细分析一些常用开源结构的源码完结,一起为了加深对每个技能栈的了解,在分析清楚源码完结后,也会以本文这种形式,对结构进行迷你版的手写实战,因而本文也算是一个新的尝试。

一、SpringMVC结构的概述与回想

SpringMVCSpring宗族中的元老之一,它是一个依据MVC三层架构形式的Web运用结构,它的出现也一统了JavaWEB运用开发的项目结构,然后防止将悉数事务代码都糅合在同一个包下的杂乱情况。在该结构中经过把Model、View、Controller分离,如下:

  • M/Model模型:由service、dao、entityJavaBean构成,首要担任事务逻辑处理。
  • V/View视图:担任向用户进行界面的展现,由jsp、html、ftl....等组成。
  • C/Controller操控器:首要担任接纳恳求、调用事务服务、依据成果派发页面。

SpringMVC贯彻落实了MVC思想,以分层作业的形式,把整个较为杂乱的web运用拆分成逻辑清晰的几部分,从很大程度上也简化了开发作业,削减了团队协作开发时的出错几率。

回想开端的servlet开发,或者说开端咱们学习Java时,如冲弱般的操作,当时也不会区分模块、区分包,悉数代码一股脑的全都放在少量的几个包下。但不知从何时起,渐渐的,每逢有一个新的项目需求出现时,咱们都会先对其区分模块,再区分层次,SpringMVC这个结构现已让每位Java开发彻底将MVC思想刻入到了DNA中,无论是开端的单体开发,亦或是现在主流的分布式、微服务开发,信任咱们都现已遵守着这个思想。

SpringMVC结构的规划,是以恳求为驱动,环绕Servlet规划的,将恳求发给操控器,然后经过模型方针,分配器来展现恳求成果的视图。SpringMVC的中心类是DispatcherServlet,它是一个Servlet子类,顶层是完结的Servlet接口。

当然,此刻暂时避开其原理不谈,先回想开端的SpringMVC是怎么运用的呢?一起来看看。

1.1、SpringMVC的运用办法

关于SpringMVC结构的原生运用办法,估量大部分小伙伴都现已忘了,尤其是近些年SpringBoot结构的盛行,因为其简化装备的特性,让咱们简直无需再关注开端那些繁杂的XML装备。

说到这块就引起了我早些年那些苦楚的回想,在SpringBoot还未那么盛行之前,简直悉数的装备都是依据XML来弄的,而且每逢引进一个新的技能栈,都需求装备一大堆文件,比方Spring、SpringMVC、MyBatis、Shiro、Quartz、EhCache....,这个整合进程无疑是苦楚的。

但随着后续的SpringBoot盛行,这些问题则无需开发者再关注,不过成也SpringBoot,败也SpringBoot,尤其是近几年新入行的Java程序员,正是因为未曾有过之前那种繁重的XML装备阅历,因而关于application.yml中很多技能栈的装备项也并不是特别了解,项目开发中需求引进一个新的技能栈时,简直靠在网上copy别人的装备信息,也就成了“知其然而不知其所以然”,这对后续想要深入研究底层也成了一道新的屏障。

就此打住,感慨也不多说了,咱们先来回想回想开端SpringMVC的运用办法:依据最一般的maven-web工程构建。

在运用SpringMVC结构时,一般会首先装备它的中心文件: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"
       xmlns:context="http://www.springframework.org/schema/context"
       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/context
		http://www.springframework.org/schema/context/spring-context-4.3.xsd 
		http://www.springframework.org/schema/mvc
		http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 经过context:component-scan元素扫描指定包下的操控器-->
    <!-- 扫描com.xxx.xxx及后代包下的操控器(扫描规模过大,耗时)-->
    <context:component-scan base-package="com.xxx.controller"/>
    <!-- ViewResolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- viewClass需求在pom中引进两个包:standard.jar and jstl.jar -->
        <property name="viewClass"
                  value="org.springframework.web.servlet.view.JstlView"></property>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    <!-- 省略其他装备...... -->
</beans>

springmvc-servlet.xml这个中心装备文件中,最重要的其实是装备Controller类地点的途径,即包扫描的途径,以及装备一个视图解析器,首要用于解析恳求成功之后的视图数据。

OK~,装备好了springmvc-servlet.xml文件后,紧接着咱们会再修正maven-web项目中心文件web.xml中的装备项:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <!-- 再这儿会增加一个SpringMVC的servlet装备项 -->
  <servlet>
  <!-- 首先指定SpringMVC中心操控器地点的方位 -->
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- DispatcherServlet发动时,从哪个文件中加载组件的初始化信息 -->
    <!--此参数能够不装备,默认值为:/WEB-INF/springmvc-servlet.xml-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/springmvc-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <!--web.xml 3.0的新特性,是否支撑异步-->
    <!--<async-supported>true</async-supported>-->
  </servlet>
  <!-- 装备路由匹配规矩,/ 代表匹配悉数,类似于nginx的location规矩 -->
  <servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

修正web.xml中的装备时,首要就干了一件事情,也便是为SpringMVC增加了一对servlet的装备项,首要指定了几个值:

  • ①指定了SpringMVCDispatcherServlet类的全途径。
  • ②指定DispatcherServlet初始化组件时,从哪个文件中加载组件的装备信息。
  • ③装备了一条值为/的路由匹配规矩,/代表悉数恳求途径都匹配。

经过上述装备后,服务器发动后,悉数的恳求都会依据装备好的路由规矩,先去到DispatcherServlet中处理。

至此,大概的装备就弄好了,紧接着是在前面装备的com.xxx.controller包中编写对应的Controller类,如下:

package com.xxx.controller;
@Controller("/user")
public class UserController{
    // 省略......
}

悉数安排妥当后,一般都会将WEB运用打成war包,然后放入到Tomcat中运转,而当Tomcat发动时,首先会找到对应的WEB程序,紧接着会去加载web.xml,加载web.xml时,因为前面在其间装备了DispatcherServlet,所以此刻会先去加载DispatcherServlet,而加载这个类时,又会触发它的初始化办法,会调用initStrategies()办法对组件进行初始化,如下:

// DispatcherServlet类 → initStrategies()办法
protected void initStrategies(ApplicationContext context) {
        // 在这儿边初始化SpringMVC作业时,需求用到的各大组件
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }

那初始化组件时,肯定需求一些加载一些对应的组件装备,这些装备信息从哪儿来呢?也便是依据咱们指定的<init-param></init-param>装备项,读取之前的中心文件:springmvc-servlet.xml中所装备的信息,对各大组件进行初始化。

所以,当Tomcat发动成功后,SpringMVC的各大组件也会初始化完结。

当然,DispatcherServlet除开是SpringMVC的初始化构建器外,仍是SpringMVC的组件调用器,因为前面在web.xml还装备了一条路由规矩,悉数的恳求都会先进入DispatcherServlet中处理,那既然悉数的恳求都进入了这个类,此刻究竟该怎么分发恳求,就能够任由SpringMVC调度了。

SpringMVC内部究竟是怎么调用各大组件对恳求进行处理的,这就涉及到了本文开头抛出的面试题了,也便是SpringMVC的作业原理,接下来咱们简略聊一聊。

二、SpringMVC作业原理详解

在了解SpringMVC的作业原理之前,首先认识一些常用组件:

DispatcherServlet前端操控器:接纳恳求,呼应成果,相当于转发器,是整个流程操控的中心,由它调用其它组件处理用户的恳求,因而也可称为中心处理器。有了它之后,能够很大程度上削减其它组件之间的耦合度。

HandlerMapping处理映射器:首要担任依据恳求途径查找Handler处理器,也便是依据用户的恳求途径找到详细的Java办法,详细是怎么找到的呢?是依据映射联系查找的,SpringMVC供给了不同的映射器完结不同的映射办法,例如:装备文件办法,完结接口办法,注解办法等。

HandlerAdapter处理适配器:便是一个用于履行Handler处理器的组件,会依据客户端不同的恳求办法(get/post/...),履行对应的Handler。说人话便是前面的组件定位到详细Java办法后,用来履行Java办法的组件。

Handler处理器:其实这也便是包括详细事务操作的Java办法,在SpringMVC中会被包装成一个Handler方针。

ViewResolver视图解析器::对事务代码履行完结之后的成果进行视图解析,依据逻辑视图名解析成真实的视图,比方controller办法履行完结之后,return的值是index,那么会对这个成果进行解析,将成果生成例如index.jsp这类的View视图。
ViewResolver作业时,会首先依据逻辑视图名解析成物理视图名,即详细的页面地址,然后再生成View视图方针,终究对视图进行烘托,将处理成果经过页面展现给用户。
SpringMVC供给了很多的View视图类型,如:jstlView、freemarkerView、pdfView等,前面咱们装备的JSP视图解析器则是JstlView,这儿也能够依据模板引擎的不同,挑选不同的解析器。

View视图ViewSpringMVC中是一个接口,完结类支撑不同的类型,例如jsp、freemarker、ftl...,不过现在一般都是前后端分离的项目,因而也很少再用到这块内容,视图一般都成了html页面,数据成果的烘托作业也交给了前端完结。

大致关于SpringMVC的中心组件有了了解之后,再上一张图:

深入理解SpringMVC工作原理,像大牛一样手写SpringMVC框架

关于这张图,信任咱们都多多少少有在“面试八股文”中看到过,这也是涵盖了SpringMVC内部调度时的完好流程图,恳求到来后都会经过这一系列步骤,如下:

  • ①用户发送恳求至会先进入DispatcherServlet操控器进行相应处理。
  • DispatcherServlet会调用HandlerMapping依据恳求途径查找Handler
  • ③处理器映射器找到详细的处理器后,生成Handler方针及Handler拦截器(假如有则生成),然后回来给DispatcherServlet
  • DispatcherServlet紧接着会调用HandlerAdapter,准备履行Handler
  • HandlerAdapter底层会利用反射机制,对前面生成的Handler方针进行履行。
  • ⑥履行完对应的Java办法后,HandlerAdapter会得到一个ModelAndView方针。
  • HandlerAdapterModelAndView再回来给DispatcherServlet操控器。
  • DisPatcherServlet再调用ViewReslover,并将ModelAndView传递给它。
  • ViewReslover视图解析器开端解析ModelAndView并回来解析出的View视图。
  • ⑩解析出View视图后,对视图进行数据烘托(行将模型数据填充至视图中)。
  • DispatcherServlet终究将烘托好的View视图呼应给用户浏览器。

其实调查如上流程,SpringMVC中的其他组件简直不存在太多的耦合联系,大部分的作业都是由DispatcherServlet来调度组件完结的,因而这也是它被称为“中心操控器”的原因,DispatcherServlet本质上并不会处理用户恳求,它仅仅是作为恳求统一的访问点,担任恳求处理时的大局流程操控。

当然,最开端因为咱们在springmvc-servlet.xml中装备了扫包途径,因而在项目发动时,就会去扫描对应目录下的悉数类,然后将带有对应注解的类与办法,与注解上指定的恳求途径生成映射联系,方便后续恳求到来时能够精准定位(稍后看完手写事例咱们就了解这点了)。

经过上述一系列分析后会发现,SpringMVC的中心便是DispatcherServlet,由它去调用各类组件完结作业。而DispatcherServlet其实本质上便是一个Servlet子类,一般WEB层结构本质上都离不开Servlet,就好比ORM结构离不开JDBC,比方Zuul、GateWay等结构,本质上也是依赖于Servlet技能作为底层的。

三、手写Mini版SpringMVC结构

到目前为止,相对来说现已将SpringMVC的作业原理做了简略概述,接下来就来到本文的中心:自己手写一个Mini版的SpringMVC结构。步骤首要分为五步:

  • ①自界说相关注解。
  • ②完结中心组件。
  • ③完结DispatcherServlet
  • ④编写相关的视图(jsp网页)。
  • ⑤编写测验用例。

不过在手写之前,咱们得先创立一个一般的Maven-Web工程。

3.1、自界说相关注解

SpringMVC中的注解实际上并不少,所以在这儿不会悉数完结,重点就自界说@Controller、@RequestMapping、@ResponseBody这几个常用的中心注解。

3.1.1、@Controller注解的界说

// 声明注解的生命周期:RUNTIME表明运转时期有用
@Retention(RetentionPolicy.RUNTIME)
// 注解的收效规模:只能收效于类上面
@Target(ElementType.TYPE)
public @interface Controller {
    //@interface是元注解:JDK封装的专门用来完结自界说注解的注解
}

这个注解稍后会加载咱们要扫描的Controller类上,首要是为了标示出扫描时的方针类。

3.1.2、@RequestMapping注解的界说

// 声明注解的生命周期:RUNTIME表明运转时期有用
@Retention(RetentionPolicy.RUNTIME)
// 注解的收效规模:可运用在类上面、办法上面
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface RequestMapping {
    // 允许该注解能够填String类型的参数,默以为空
    String value() default "";
}

这个注解能够加在类或办法上,首要是用来给类或办法映射恳求途径。

3.1.3、@ResponseBody注解的界说

// 声明注解的生命周期:RUNTIME表明运转时期有用
@Retention(RetentionPolicy.RUNTIME)
// 注解的收效规模:只能运用在办法上面
@Target(ElementType.METHOD)
public @interface ResponseBody {
}

这个注解的作用是在于操控回来时的呼应办法,不加该注解的办法,默认会跳转页面,也加了该注解的办法,则会直接呼应数据。

OK~,在上面界说了三个注解,其间运用到了两个JDK供给的元注解:@Retention、@Target,前者用于操控注解的生命周期,表明自界说的注解在何时收效。后者则操控了注解的收效规模,能够操控自界说注解在类、办法、特点上收效。

不过在这儿并未对这些注解进行处理,仅仅简略的界说,假如想要注解收效,一般有两种办法:①运用AOP切面对注解进行处理。②运用反射机制对注解进行处理。

稍后咱们会选用上述的第二种办法对自界说的注解进行处理。

3.2、完结中心组件

自界说注解的作业完结后,紧接着再来完结一些运转时需求用到的中心组件。当然,这儿也不会将之前SpringMVC拥有的悉数组件悉数完结,仅完结几个中心的组件,能够到达作用即可。(在完结之后,咱们有爱好可自行完善)。

3.2.1、InvocationHandler组件

InvocationHandler这个组件,首要是为了待会儿合作扫描包运用的,能够简略了解成Java办法的封装方针,如下:

public class InvocationHandler {
    // 这儿会寄存办法对应的方针实例
    private Object object;
    // 这儿会寄存对应的Java办法
    private Method method;
    // 结构办法:无参和全参结构
    public InvocationHandler(){}
    public InvocationHandler(Object object, Method method) {
        this.object = object;
        this.method = method;
    }
    // Get and Set办法
    public Object getObject() {
        return object;
    }
    public void setObject(Object object) {
        this.object = object;
    }
    public Method getMethod() {
        return method;
    }
    public void setMethod(Method method) {
        this.method = method;
    }
    // 这儿重写了toString()办法
    @Override
    public String toString() {
        return "InvocationHandler{" +
                "object=" + object +
                ", method=" + method +
                '}';
    }
}

这个组件很简略,信任咱们也能直接看明白,这也对应着之前SpringMVC中的Handler组件。

3.2.2、HandlerMapping组件

这个组件首要担任扫描包,在项目发动时,将指定的包目录下,悉数的恳求途径与Java办法形成映射联系。

public class HandlerMapping {
    public Map<String,InvocationHandler> urlMapping(Set<Class<?>> classSet){
        // 初始化一个 Map 调集,用于寄存映射联系
        HashMap<String, InvocationHandler> HandlerHashMap = new HashMap<>();
        // 遍历 Controller 调集(也便是悉数带@Controller注解的类)
        for (Class<?> aClass : classSet) {
            //获取类上@RequestMapping注解的值
            String classReqPath = AnnotationUtil.
                    getAnnotationValue(aClass, RequestMapping.class);
            System.out.println("类的恳求途径:" + classReqPath);
            // 获取这个 class 类中的悉数办法
            Method[] methods = aClass.getDeclaredMethods();
            System.out.println("类中办法数量为:" + methods.length);
            // 假如这个类中办法数量不为空
            if (methods.length != 0) {
                // 开端遍历这个类中的悉数办法
                for (Method method : methods) {
                    // 判别每个办法上是否带有@RequestMapping注解
                    boolean flag = method.isAnnotationPresent(RequestMapping.class);
                    // 假如当时办法上带有这个注解
                    if (flag){
                        // 获取办法上@RequestMapping注解的值
                        String methodReqPath = AnnotationUtil.
                                getAnnotationValue(method, RequestMapping.class);
                        // 判别得到的值是否为空,不为空则获取对应的值
                        String reqPath = methodReqPath == null ||
                                methodReqPath.equals("") ? "" : methodReqPath;
                        System.out.println("办法上的恳求途径:" + reqPath);
                        // 将得到的值封装成 InvocationHandler 方针
                        try {
                            // 放入一个当时类的实例方针,用于履行后面的类办法
                            InvocationHandler invocationHandler = new 
                                    InvocationHandler(aClass.newInstance(), method);
                            // 运用 类的恳求途径 + 办法的恳求途径 作为Key
                            HandlerHashMap.put(classReqPath + reqPath,
                                    invocationHandler);
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        // 将寄存映射联系的Map调集回来
        return HandlerHashMap;
    }
}

在这个类中,首要界说了一个urlMapping()办法,这个办法做的首要作业便是:关于悉数存在@Controller注解的类做扫描,关于这些类中的办法进行判别,将悉数带@RequestMapping注解的办法,悉数封装成InvocationHandler方针作为Value,然后再以类的恳求途径 + 办法的恳求途径作为Key,放入到一个Map调集中保存。

3.3、完结DispatcherServlet中心操控器

自界说注解和组件的作业完结后,接下来再开端编写最中心的DispatcherServlet类,同样,在界说时记得继承HttpServlet

public class DispacherServlet extends HttpServlet {
    // 界说一个 Map 容器,存储映射联系
    private static Map<String, InvocationHandler> HandlerMap;
    @Override
    public void init() throws ServletException {
        System.out.println("项目发动了.....");
        // 指定要扫描的包途径(本来是从xml文件中读取的)
        String packagePath = "com.xxx.controller";
        // 在指定的包途径下扫描带有@Controller注解的类
        Set<Class<?>> classSet = ClassUtil.
                scanPackageByAnnotation(packagePath, Controller.class);
        System.out.println("扫描到类的数量为:" + classSet.size());
        // 创立一个HandlerMapping并调用urlMapping()办法
        HandlerMapping handlerMapping = new HandlerMapping();
        HandlerMap = handlerMapping.urlMapping(classSet);
        // 终究获取到一个带有悉数映射联系的 Map 调集
        System.out.println("HandlerMap的长度:" + HandlerMap.size());
    }
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doPost(req,resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 获取客户端的恳求途径
        StringBuffer requestURL = req.getRequestURL();
        System.out.println("客户端恳求途径:" + requestURL);
        // 判别恳求途径中是否包括项目名,包括的话运用空字符替换掉
        String path = new String(requestURL).replace("http://" +
                req.getServerName() + ":" + req.getServerPort(), "");
        System.out.println("处理后的客户端恳求途径:" + path);
        // 依据处理好的 path 作为条件去map中查找对应的办法
        InvocationHandler handler = HandlerMap.get(path);
        // 获取到对应的类实例方针和Java办法
        Object object = handler.getObject();
        Method method = handler.getMethod();
        // 判别该办法上是否增加了@ResponseBody注解:
        //      true:直接回来数据  false:跳转页面
        boolean f = method.isAnnotationPresent(ResponseBody.class);
        System.out.println("是否增加了@ResponseBody注解:" + f);
        // 假如办法上存在@ResponseBody注解
        if (f){
            try {
                // 经过反射的办法调用办法并履行
                Object invoke = method.invoke(object);
                // 将成果经过Response直接写回给客户端
                resp.getWriter().print(invoke.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else{
            // 获取客户端的恳求途径作为回来时的前途径
            String URL = "http://" + req.getServerName() + ":" +
                    req.getServerPort() + "/" + req.getContextPath();
            System.out.println("URL:" + URL);
            // 自界说的前后缀(本来也是在xml中读取)
            String prefix = "";
            String suffix = ".jsp";
            try {
                // 经过反射机制,履行对应的Java办法
                Object invoke = method.invoke(object);
                if(invoke instanceof ModelAndView){
                    // 假如是回来的ModelAndView方针,这儿做额定处理....
                } else{
                    // 获取Java办法履行之后的回来成果
                    String str = (String)invoke;
                    // 假如指定了跳转办法为 forward: 转发
                    if(str.contains("forward:")){
                        System.out.println("以转发的办法跳转页面...");
                        req.getRequestDispatcher("index.jsp").forward(req,resp);
                    }
                    // 假如指定了跳转办法为 redirect: 重定向
                    if(str.contains("redirect:")){
                        System.out.println("以重定向的办法跳转页面...");
                        resp.sendRedirect(URL + prefix +
                            str.replace("redirect:","") + suffix);
                    }
                    // 假如没有指定,则默认运用转发的办法跳转页面
                    if(!str.contains("forward:") && !str.contains("redirect:")){
                        resp.sendRedirect(URL + prefix + str + suffix);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

因为DispacherServlet完结了HttpServlet笼统类,因而也重写了它的三个办法:init()、doGet()、doPost(),其间init()办法会在项目发动时履行,而doGet()、doPost()则会在客户端恳求时被触发。

总结一下上述DispacherServlet所做的作业:

  • ①初始化悉数恳求途径与Java办法之间的映射联系。
  • ②依据客户端的恳求途径,查找对应的Java办法并履行。
  • ③判别办法上是否增加了@ResponseBody注解:
    • 增加了:直接向客户端回来数据。
    • 未增加:跳转对应的页面。
  • ④以重定向或转发的办法跳转对应的页面。

OK~,终究也不要忘了在web.xml装备一下咱们自己的DispacherServlet

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>dispacherServlet</servlet-name>
    <!-- 这儿装备的DispacherServlet是咱们自己的 -->
    <servlet-class>com.xxx.DispacherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispacherServlet</servlet-name>
    <!-- 匹配规矩仍旧是悉数恳求途径都会匹配 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

3.4、编写View视图

当然,不寻求外观了,简略编写两个视图页面:index.jsp、edit.jsp

<!-- index.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>主页</title>
    <link href="favicon.ico" rel="shortcut icon">
  </head>
  <body>
        <h1>欢迎来到熊猫高档会所,我是竹子一号!</h1>
  </body>
</html>
<!-- edit.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>修正</title>
    <link href="favicon.ico" rel="shortcut icon">
  </head>
  <body>
        <h1>修正页面</h1>
        <a href="#">跳转</a>
  </body>
</html>

3.5、编写测验用例

为了方便测验,先写一个实体类User.java,如下:

public class User {
    private Integer id;
    private String name;
    private String sex;
    private Integer age;
    public User(){}
    public User(Integer id, String name, String sex, Integer age) {
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

这个实体类首要方便为了待会儿测验@ResponseBody注解的功用,接下来写两个Controller类:

/* ------ UserController类 ------- */
@Controller
@RequestMapping("/user")
public class UserController {
    // 测验@ResponseBody的成效
    @RequestMapping("/get")
    @ResponseBody
    public User get(){
        return new User(1,"竹子爱熊猫","男",18);
    }
    // 跳转主页的办法
    @RequestMapping("/")
    public String test(){
        return "index";
    }
    // 测验重定向的成效
    @RequestMapping("/edit")
    public String toEdit(){
        return "redirect:edit";
    }
    public String TEST(){
        return null;
    }
}
/* ------OrderController类------- */ 
public class OrderController {
}

在上述测验事例中,编写了UserController、OrderController两个类,其间仅有UserController加了@Controller注解,下面来测验,首先将这个Maven工程打成war包布置在Tomcat中,然后发动,日志如下:

项目发动了.....
扫描到类的数量为:1
类的恳求途径:/user
类中办法数量为:4
办法上的恳求途径:/get
办法上的恳求途径:/test
办法上的恳求途径:/edit
HandlerMap的长度:3

从上述日志输出中,很显着能够看出,未增加@Controller注解的OrderController类并未被扫描,一起,UserController类中未增加@RequestMapping注解的TEST()办法,也没有被加入到HandlerMap调集中,该调集中仅寄存了有映射联系的Java办法。

OK~,接下来运用浏览器测验咱们手写的SpringMVC是否能够做到本来的作用:

测验主页跳转作用:http://localhost:8080/

深入理解SpringMVC工作原理,像大牛一样手写SpringMVC框架

作用很显着,主页的跳转正常,再来试试重定向的作用,如下:

http://localhost:8080/user/edit/

深入理解SpringMVC工作原理,像大牛一样手写SpringMVC框架

输入上述给出的url后,能够很完美的重定向到edit.jsp页面,日志输出如下:

客户端恳求途径:http://localhost:8080/user/edit
处理后的客户端恳求途径:/user/edit
是否增加了@ResponseBody注解:false
URL:http://localhost:8080/
以重定向的办法跳转页面...

紧接着终究来试试@ResponseBody注解的作用,在浏览器输入如下网址:

http://localhost:8080/user/get

深入理解SpringMVC工作原理,像大牛一样手写SpringMVC框架

作用仍旧显着,上述确实是咱们想要的作用,不会产生页面跳转,仅回来了对应的数据,再看看操控台:

客户端恳求途径:http://localhost:8080/user/get
处理后的客户端恳求途径:/user/get
是否增加了@ResponseBody注解:true

因为咱们在UserController.get()办法上增加了@ResponseBody注解的原因,因而确实未产生页面跳转。

OK~,至此一个简略的MiniSpringMVC结构就完结了,完结很简略,但作用却很明显。不过也存在很多缺陷未完善,咱们有爱好的能够在这个项意图结构上进一步拓展与笼统,将SpringMVC真实的各大组件抽出来,一起也关于其他一些功用进行拓展完结。

四、手写SpringMVC结构总结

终究结合手写SpringMVC的进程,再谈谈SpringMVC作业流程的了解,其实在咱们把一个JavaWeb程序打成war包丢入Tomcat后,当发动Tomcat时,它就会先去加载web.xml文件,而加载web.xml文件时,会碰到DispacherServlet需求被加载,所以又会去加载它,当加载DispacherServlet时,其实本质上会把SpringMVC的组件初始化,然后将悉数ControllerURL资源都映射到一个容器中存储。

当后续客户端产生恳求时,首先会依据装备好的路由规矩,悉数恳求会先进入DispacherServletDispacherServlet会先解析客户端的恳求途径,然后依据途径去容器中找到该Url对应的Java办法,找到之后再调用组件去履行详细的Controller办法,当履行完之后,又会将成果回来给DispacherServlet,此刻又会去调用相关组件处理履行后的成果,终究才将烘托后的成果呼应。

终究,假如在面试中遇到了面试官问你SpringMVC的作业原理(流程),最好能够结合自己的了解去回答,比方上述给出的这套总结一样,因而假如依照八股文中的死流程去讲述,并不能给面试官带来眼前一亮的感觉,因为背死的流程很简单给人带来“靠暂时记忆来面试”的感觉,所以想要更好的收割offer,更多的仍是要看自己关于技能的了解程度,还有你的思想逻辑。

你面试时,假如回答能比别人更有深度以及你自己的考虑,天然你就比其他候选者的机会更大,毕竟当下内卷越来越严重,一个能让面试官眼前一亮的候选者,天然也会给面试官带来不同的体会,因而你收到Offer的几率也会更高。