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

引言

對于SpringMVC相信諸位并不陌生,這是Java開發(fā)過程中使用最頻繁的框架,在你的項(xiàng)目中可能不一定用MyBatis,但絕對會(huì)使用SpringMVC,因?yàn)椴僮鲾?shù)據(jù)庫還有Hibernate、JPA等其他ORM框架選擇,但SpringMVC這個(gè)框架在其領(lǐng)域中,可謂是獨(dú)領(lǐng)風(fēng)騷,因此在面試中也會(huì)常常問到一些與之相關(guān)的面試題,其中最為經(jīng)典的則是那道:

SpringMVC在啟動(dòng)后是如何工作的?(工作原理)

對于這題的答案,相信大家在“Java面試八股文”中絕對背過,但之前大多數(shù)小伙伴應(yīng)該也只是死記,并未真正的理解其核心原理,那本篇的目的就在于讓諸位真正的掌握SpringMVC原理。當(dāng)然,為了更好的理解,咱們也不會(huì)以之前分析底層時(shí)的那種源碼方式,對其進(jìn)行長篇概述,本次則使用一種新的方式來對其進(jìn)行原理講解。

那新的方式是什么呢?那就是自己手寫框架,真正的理解就是自己能夠把輪子重新造一次,這原本源碼的方式更加形象,也能夠更加讓我們對其原理印象深刻。

在之后有可能會(huì)寫的《源碼分析》專題中,會(huì)再次詳細(xì)剖析一些常用開源框架的源碼實(shí)現(xiàn),同時(shí)為了加深對每個(gè)技術(shù)棧的理解,在剖析清楚源碼實(shí)現(xiàn)后,也會(huì)以本文這種形式,對框架進(jìn)行迷你版的手寫實(shí)戰(zhàn),因此本文也算是一個(gè)新的嘗試。

一、SpringMVC框架的概述與回憶

SpringMVC是Spring家族中的元老之一,它是一個(gè)基于MVC三層架構(gòu)模式的Web應(yīng)用框架,它的出現(xiàn)也一統(tǒng)了JavaWEB應(yīng)用開發(fā)的項(xiàng)目結(jié)構(gòu),從而避免將所有業(yè)務(wù)代碼都糅合在同一個(gè)包下的復(fù)雜情況。在該框架中通過把Model、View、Controller分離,如下:

M/Model模型:由service、dao、entity等JavaBean構(gòu)成,主要負(fù)責(zé)業(yè)務(wù)邏輯處理。

V/View視圖:負(fù)責(zé)向用戶進(jìn)行界面的展示,由jsp、html、ftl....等組成。

C/Controller控制器:主要負(fù)責(zé)接收請求、調(diào)用業(yè)務(wù)服務(wù)、根據(jù)結(jié)果派發(fā)頁面。

SpringMVC貫徹落實(shí)了MVC思想,以分層工作的模式,把整個(gè)較為復(fù)雜的web應(yīng)用拆分成邏輯清晰的幾部分,從很大程度上也簡化了開發(fā)工作,減少了團(tuán)隊(duì)協(xié)作開發(fā)時(shí)的出錯(cuò)幾率。

回想最初的servlet開發(fā),或者說最初我們學(xué)習(xí)Java時(shí),如稚子般的操作,當(dāng)時(shí)也不會(huì)劃分模塊、劃分包,所有代碼一股腦的全都放在少數(shù)的幾個(gè)包下。但不知從何時(shí)起,慢慢的,每當(dāng)有一個(gè)新的項(xiàng)目需求出現(xiàn)時(shí),我們都會(huì)先對其劃分模塊,再劃分層次,SpringMVC這個(gè)框架已經(jīng)讓每位Java開發(fā)徹底將MVC思想刻入到了DNA中,無論是最初的單體開發(fā),亦或是如今主流的分布式、微服務(wù)開發(fā),相信大家都已經(jīng)遵守著這個(gè)思想。

SpringMVC框架的設(shè)計(jì),是以請求為驅(qū)動(dòng),圍繞Servlet設(shè)計(jì)的,將請求發(fā)給控制器,然后通過模型對象,分派器來展示請求結(jié)果的視圖。SpringMVC的核心類是DispatcherServlet,它是一個(gè)Servlet子類,頂層是實(shí)現(xiàn)的Servlet接口。

當(dāng)然,此刻暫且避開其原理不談,先回想最初的SpringMVC是如何使用的呢?一起來看看。

1.1、SpringMVC的使用方式

對于SpringMVC框架的原生使用方式,估計(jì)大部分小伙伴都已經(jīng)忘了,尤其是近些年SpringBoot框架的流行,由于其簡化配置的特性,讓我們幾乎無需再關(guān)注最初那些繁雜的XML配置。

說到這塊就引起了我早些年那些痛苦的回憶,在SpringBoot還未那么流行之前,幾乎所有的配置都是基于XML來弄的,而且每當(dāng)引入一個(gè)新的技術(shù)棧,都需要配置一大堆文件,比如Spring、SpringMVC、MyBatis、Shiro、Quartz、EhCache....,這個(gè)整合過程無疑是痛苦的。

但隨著后續(xù)的SpringBoot流行,這些問題則無需開發(fā)者再關(guān)注,不過成也SpringBoot,敗也SpringBoot,尤其是近幾年新入行的Java程序員,正是由于未曾有過之前那種繁重的XML配置經(jīng)歷,因此對于application.yml中很多技術(shù)棧的配置項(xiàng)也并不是特別理解,項(xiàng)目開發(fā)中需要引入一個(gè)新的技術(shù)棧時(shí),幾乎靠在網(wǎng)上copy他人的配置信息,也就成了“知其然而不知其所以然”,這對后續(xù)想要深入研究底層也成了一道新的屏障。

就此打住,感慨也不多說了,咱們先來回憶回憶最初SpringMVC的使用方式:基于最普通的maven-web工程構(gòu)建。

在使用SpringMVC框架時(shí),一般會(huì)首先配置它的核心文件: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及子孫包下的控制器(掃描范圍過大,耗時(shí))-->
    <context:component-scan base-package="com.xxx.controller"/>
    
    <!-- ViewResolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- viewClass需要在pom中引入兩個(gè)包: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這個(gè)核心配置文件中,最重要的其實(shí)是配置Controller類所在的路徑,即包掃描的路徑,以及配置一個(gè)視圖解析器,主要用于解析請求成功之后的視圖數(shù)據(jù)。
OK~,配置好了springmvc-servlet.xml文件后,緊接著我們會(huì)再修改maven-web項(xiàng)目核心文件web.xml中的配置項(xiàng):

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

  <!-- 再這里會(huì)添加一個(gè)SpringMVC的servlet配置項(xiàng) -->
  <servlet>
  <!-- 首先指定SpringMVC核心控制器所在的位置 -->
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- DispatcherServlet啟動(dòng)時(shí),從哪個(gè)文件中加載組件的初始化信息 -->
    <!--此參數(shù)可以不配置,默認(rèn)值為:/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>
  <!-- 配置路由匹配規(guī)則,/ 代表匹配所有,類似于nginx的location規(guī)則 -->
  <servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

修改web.xml中的配置時(shí),主要就干了一件事情,也就是為SpringMVC添加了一對servlet的配置項(xiàng),主要指定了幾個(gè)值:

①指定了SpringMVC中DispatcherServlet類的全路徑。
②指定DispatcherServlet初始化組件時(shí),從哪個(gè)文件中加載組件的配置信息。
③配置了一條值為/的路由匹配規(guī)則,/代表所有請求路徑都匹配。

經(jīng)過上述配置后,服務(wù)器啟動(dòng)后,所有的請求都會(huì)根據(jù)配置好的路由規(guī)則,先去到DispatcherServlet中處理。
至此,大概的配置就弄好了,緊接著是在前面配置的com.xxx.controller包中編寫對應(yīng)的Controller類,如下:

package com.xxx.controller;

@Controller("/user")
public class UserController{
    // 省略......
}

一切就緒后,一般都會(huì)將WEB應(yīng)用打成war包,然后放入到Tomcat中運(yùn)行,而當(dāng)Tomcat啟動(dòng)時(shí),首先會(huì)找到對應(yīng)的WEB程序,緊接著會(huì)去加載web.xml,加載web.xml時(shí),由于前面在其中配置了DispatcherServlet,所以此時(shí)會(huì)先去加載DispatcherServlet,而加載這個(gè)類時(shí),又會(huì)觸發(fā)它的初始化方法,會(huì)調(diào)用initStrategies()方法對組件進(jìn)行初始化,如下:

// DispatcherServlet類 → initStrategies()方法
protected void initStrategies(ApplicationContext context) {
        // 在這里面初始化SpringMVC工作時(shí),需要用到的各大組件
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }

那初始化組件時(shí),肯定需要一些加載一些對應(yīng)的組件配置,這些配置信息從哪兒來呢?也就是根據(jù)我們指定的<init-param></init-param>配置項(xiàng),讀取之前的核心文件:springmvc-servlet.xml中所配置的信息,對各大組件進(jìn)行初始化。

所以,當(dāng)Tomcat啟動(dòng)成功后,SpringMVC的各大組件也會(huì)初始化完成。

當(dāng)然,DispatcherServlet除開是SpringMVC的初始化構(gòu)建器外,還是SpringMVC的組件調(diào)用器,因?yàn)榍懊嬖?code>web.xml還配置了一條路由規(guī)則,所有的請求都會(huì)先進(jìn)入DispatcherServlet中處理,那既然所有的請求都進(jìn)入了這個(gè)類,此時(shí)究竟該如何分發(fā)請求,就可以任由SpringMVC調(diào)度了。

SpringMVC內(nèi)部究竟是如何調(diào)用各大組件對請求進(jìn)行處理的,這就涉及到了本文開頭拋出的面試題了,也就是SpringMVC的工作原理,接下來我們簡單聊一聊。

二、SpringMVC工作原理詳解

在了解SpringMVC的工作原理之前,首先認(rèn)識(shí)一些常用組件:

DispatcherServlet前端控制器:接收請求,響應(yīng)結(jié)果,相當(dāng)于轉(zhuǎn)發(fā)器,是整個(gè)流程控制的中心,由它調(diào)用其它組件處理用戶的請求,因此也可稱為中央處理器。有了它之后,可以很大程度上減少其它組件之間的耦合度。

HandlerMapping處理映射器:主要負(fù)責(zé)根據(jù)請求路徑查找Handler處理器,也就是根據(jù)用戶的請求路徑找到具體的Java方法,具體是如何找到的呢?是根據(jù)映射關(guān)系查找的,SpringMVC提供了不同的映射器實(shí)現(xiàn)不同的映射方式,例如:配置文件方式,實(shí)現(xiàn)接口方式,注解方式等。

HandlerAdapter處理適配器:就是一個(gè)用于執(zhí)行Handler處理器的組件,會(huì)根據(jù)客戶端不同的請求方式(get/post/...),執(zhí)行對應(yīng)的Handler。說人話就是前面的組件定位到具體Java方法后,用來執(zhí)行Java方法的組件。

Handler處理器:其實(shí)這也就是包含具體業(yè)務(wù)操作的Java方法,在SpringMVC中會(huì)被包裝成一個(gè)Handler對象。

ViewResolver視圖解析器::對業(yè)務(wù)代碼執(zhí)行完成之后的結(jié)果進(jìn)行視圖解析,根據(jù)邏輯視圖名解析成真正的視圖,比如controller方法執(zhí)行完成之后,return的值是index,那么會(huì)對這個(gè)結(jié)果進(jìn)行解析,將結(jié)果生成例如index.jsp這類的View視圖。
ViewResolver工作時(shí),會(huì)首先根據(jù)邏輯視圖名解析成物理視圖名,即具體的頁面地址,然后再生成View視圖對象,最后對視圖進(jìn)行渲染,將處理結(jié)果通過頁面展示給用戶。
SpringMVC提供了很多的View視圖類型,如:jstlView、freemarkerView、pdfView等,前面我們配置的JSP視圖解析器則是JstlView,這里也可以根據(jù)模板引擎的不同,選擇不同的解析器。

View視圖ViewSpringMVC中是一個(gè)接口,實(shí)現(xiàn)類支持不同的類型,例如jsp、freemarker、ftl...,不過現(xiàn)在一般都是前后端分離的項(xiàng)目,因此也很少再用到這塊內(nèi)容,視圖一般都成了html頁面,數(shù)據(jù)結(jié)果的渲染工作也交給了前端完成。

大致對于SpringMVC的核心組件有了了解之后,再上一張圖:

image.png

對于這張圖,相信大家都多多少少有在“面試八股文”中看到過,這也是涵蓋了SpringMVC內(nèi)部調(diào)度時(shí)的完整流程圖,請求到來后都會(huì)經(jīng)過這一系列步驟,如下:

  • ①用戶發(fā)送請求至?xí)冗M(jìn)入DispatcherServlet控制器進(jìn)行相應(yīng)處理。
  • DispatcherServlet會(huì)調(diào)用HandlerMapping根據(jù)請求路徑查找Handler
  • ③處理器映射器找到具體的處理器后,生成Handler對象及Handler攔截器(如果有則生成),然后返回給DispatcherServlet。
  • DispatcherServlet緊接著會(huì)調(diào)用HandlerAdapter,準(zhǔn)備執(zhí)行Handler。
  • HandlerAdapter底層會(huì)利用反射機(jī)制,對前面生成的Handler對象進(jìn)行執(zhí)行。
  • ⑥執(zhí)行完對應(yīng)的Java方法后,HandlerAdapter會(huì)得到一個(gè)ModelAndView對象。
  • HandlerAdapterModelAndView再返回給DispatcherServlet控制器。
  • DisPatcherServlet再調(diào)用ViewReslover,并將ModelAndView傳遞給它。
  • ViewReslover視圖解析器開始解析ModelAndView并返回解析出的View視圖。
  • ⑩解析出View視圖后,對視圖進(jìn)行數(shù)據(jù)渲染(即將模型數(shù)據(jù)填充至視圖中)。
  • ?DispatcherServlet最終將渲染好的View視圖響應(yīng)給用戶瀏覽器。

其實(shí)觀察如上流程,SpringMVC中的其他組件幾乎不存在太多的耦合關(guān)系,大部分的工作都是由DispatcherServlet來調(diào)度組件完成的,因此這也是它被稱為“中央控制器”的原因,DispatcherServlet本質(zhì)上并不會(huì)處理用戶請求,它僅僅是作為請求統(tǒng)一的訪問點(diǎn),負(fù)責(zé)請求處理時(shí)的全局流程控制。

當(dāng)然,最開始由于我們在springmvc-servlet.xml中配置了掃包路徑,因此在項(xiàng)目啟動(dòng)時(shí),就會(huì)去掃描對應(yīng)目錄下的所有類,然后將帶有對應(yīng)注解的類與方法,與注解上指定的請求路徑生成映射關(guān)系,方便后續(xù)請求到來時(shí)能夠精準(zhǔn)定位(稍后看完手寫案例大家就理解這點(diǎn)了)。

經(jīng)過上述一系列分析后會(huì)發(fā)現(xiàn),SpringMVC的核心就是DispatcherServlet,由它去調(diào)用各類組件完成工作。而DispatcherServlet其實(shí)本質(zhì)上就是一個(gè)Servlet子類,一般WEB層框架本質(zhì)上都離不開Servlet,就好比ORM框架離不開JDBC,比如Zuul、GateWay等框架,本質(zhì)上也是依賴于Servlet技術(shù)作為底層的。

三、手寫Mini版SpringMVC框架

到目前為止,相對來說已經(jīng)將SpringMVC的工作原理做了簡單概述,接下來就來到本文的核心:自己手寫一個(gè)Mini版的SpringMVC框架。步驟主要分為五步:

  • ①自定義相關(guān)注解。
  • ②實(shí)現(xiàn)核心組件。
  • ③實(shí)現(xiàn)DispatcherServlet。
  • ④編寫相關(guān)的視圖(jsp網(wǎng)頁)。
  • ⑤編寫測試用例。

不過在手寫之前,咱們得先創(chuàng)建一個(gè)普通的Maven-Web工程。

3.1、自定義相關(guān)注解

SpringMVC中的注解實(shí)際上并不少,所以在這里不會(huì)全部實(shí)現(xiàn),重點(diǎn)就自定義@Controller、@RequestMapping、@ResponseBody這幾個(gè)常用的核心注解。

3.1.1、@Controller注解的定義

// 聲明注解的生命周期:RUNTIME表示運(yùn)行時(shí)期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范圍:只能生效于類上面
@Target(ElementType.TYPE)
public @interface Controller {
    //@interface是元注解:JDK封裝的專門用來實(shí)現(xiàn)自定義注解的注解
}

這個(gè)注解稍后會(huì)加載咱們要掃描的Controller類上,主要是為了標(biāo)注出掃描時(shí)的目標(biāo)類。

3.1.2、@RequestMapping注解的定義
// 聲明注解的生命周期:RUNTIME表示運(yùn)行時(shí)期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范圍:可應(yīng)用在類上面、方法上面
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface RequestMapping {
    // 允許該注解可以填String類型的參數(shù),默認(rèn)為空
    String value() default "";
}

這個(gè)注解可以加在類或方法上,主要是用來給類或方法映射請求路徑。

3.1.3、@ResponseBody注解的定義

// 聲明注解的生命周期:RUNTIME表示運(yùn)行時(shí)期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范圍:只能應(yīng)用在方法上面
@Target(ElementType.METHOD)
public @interface ResponseBody {
}

這個(gè)注解的作用是在于控制返回時(shí)的響應(yīng)方式,不加該注解的方法,默認(rèn)會(huì)跳轉(zhuǎn)頁面,也加了該注解的方法,則會(huì)直接響應(yīng)數(shù)據(jù)。
OK~,在上面定義了三個(gè)注解,其中使用到了兩個(gè)JDK提供的元注解:@Retention、@Target,前者用于控制注解的生命周期,表示自定義的注解在何時(shí)生效。后者則控制了注解的生效范圍,可以控制自定義注解在類、方法、屬性上生效。

不過在這里并未對這些注解進(jìn)行處理,只是簡單的定義,如果想要注解生效,一般有兩種方式:①使用AOP切面對注解進(jìn)行處理。②使用反射機(jī)制對注解進(jìn)行處理。

稍后我們會(huì)采用上述的第二種方式對自定義的注解進(jìn)行處理。

3.2、實(shí)現(xiàn)核心組件

自定義注解的工作完成后,緊接著再來實(shí)現(xiàn)一些運(yùn)行時(shí)需要用到的核心組件。當(dāng)然,這里也不會(huì)將之前SpringMVC擁有的所有組件全部實(shí)現(xiàn),僅實(shí)現(xiàn)幾個(gè)核心的組件,能夠達(dá)到效果即可。(在完成之后,大家有興趣可自行完善)。

3.2.1、InvocationHandler組件

InvocationHandler這個(gè)組件,主要是為了待會(huì)兒配合掃描包使用的,可以簡單理解成Java方法的封裝對象,如下:

public class InvocationHandler {
    // 這里會(huì)存放方法對應(yīng)的對象實(shí)例
    private Object object;
    // 這里會(huì)存放對應(yīng)的Java方法
    private Method method;
    
    // 構(gòu)造方法:無參和全參構(gòu)造
    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 +
                '}';
    }
}

這個(gè)組件很簡單,相信大家也能直接看明白,這也對應(yīng)著之前SpringMVC中的Handler組件。

3.2.2、HandlerMapping組件

這個(gè)組件主要負(fù)責(zé)掃描包,在項(xiàng)目啟動(dòng)時(shí),將指定的包目錄下,所有的請求路徑與Java方法形成映射關(guān)系。

public class HandlerMapping {
    public Map<String,InvocationHandler> urlMapping(Set<Class<?>> classSet){
        // 初始化一個(gè) Map 集合,用于存放映射關(guān)系
        HashMap<String, InvocationHandler> HandlerHashMap = new HashMap<>();
        // 遍歷 Controller 集合(也就是所有帶@Controller注解的類)
        for (Class<?> aClass : classSet) {
            //獲取類上@RequestMapping注解的值
            String classReqPath = AnnotationUtil.
                    getAnnotationValue(aClass, RequestMapping.class);
            System.out.println("類的請求路徑:" + classReqPath);

            // 獲取這個(gè) class 類中的所有方法
            Method[] methods = aClass.getDeclaredMethods();
            System.out.println("類中方法數(shù)量為:" + methods.length);

            // 如果這個(gè)類中方法數(shù)量不為空
            if (methods.length != 0) {
                // 開始遍歷這個(gè)類中的所有方法
                for (Method method : methods) {
                    // 判斷每個(gè)方法上是否帶有@RequestMapping注解
                    boolean flag = method.isAnnotationPresent(RequestMapping.class);
                    // 如果當(dāng)前方法上帶有這個(gè)注解
                    if (flag){
                        // 獲取方法上@RequestMapping注解的值
                        String methodReqPath = AnnotationUtil.
                                getAnnotationValue(method, RequestMapping.class);
                        // 判斷得到的值是否為空,不為空則獲取對應(yīng)的值
                        String reqPath = methodReqPath == null ||
                                methodReqPath.equals("") ? "" : methodReqPath;
                        System.out.println("方法上的請求路徑:" + reqPath);
                        // 將得到的值封裝成 InvocationHandler 對象
                        try {
                            // 放入一個(gè)當(dāng)前類的實(shí)例對象,用于執(zhí)行后面的類方法
                            InvocationHandler invocationHandler = new 
                                    InvocationHandler(aClass.newInstance(), method);
                            // 使用 類的請求路徑 + 方法的請求路徑 作為Key
                            HandlerHashMap.put(classReqPath + reqPath,
                                    invocationHandler);
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        // 將存放映射關(guān)系的Map集合返回
        return HandlerHashMap;
    }
}

在這個(gè)類中,主要定義了一個(gè)urlMapping()方法,這個(gè)方法做的主要工作就是:對于所有存在@Controller注解的類做掃描,對于這些類中的方法進(jìn)行判斷,將所有帶@RequestMapping注解的方法,全部封裝成InvocationHandler對象作為Value,然后再以類的請求路徑 + 方法的請求路徑作為Key,放入到一個(gè)Map集合中保存。

3.3、實(shí)現(xiàn)DispatcherServlet中央控制器

自定義注解和組件的工作完成后,接下來再開始編寫最核心的DispatcherServlet類,同樣,在定義時(shí)記得繼承HttpServlet:

public class DispacherServlet extends HttpServlet {

    // 定義一個(gè) Map 容器,存儲(chǔ)映射關(guān)系
    private static Map<String, InvocationHandler> HandlerMap;

    @Override
    public void init() throws ServletException {
        System.out.println("項(xiàng)目啟動(dòng)了.....");
        // 指定要掃描的包路徑(原本是從xml文件中讀取的)
        String packagePath = "com.xxx.controller";
        // 在指定的包路徑下掃描帶有@Controller注解的類
        Set<Class<?>> classSet = ClassUtil.
                scanPackageByAnnotation(packagePath, Controller.class);
        System.out.println("掃描到類的數(shù)量為:" + classSet.size());
        // 創(chuàng)建一個(gè)HandlerMapping并調(diào)用urlMapping()方法
        HandlerMapping handlerMapping = new HandlerMapping();
        HandlerMap = handlerMapping.urlMapping(classSet);
        // 最終獲取到一個(gè)帶有所有映射關(guān)系的 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);
        // 判斷請求路徑中是否包含項(xiàng)目名,包含的話使用空字符替換掉
        String path = new String(requestURL).replace("http://" +
                req.getServerName() + ":" + req.getServerPort(), "");
        System.out.println("處理后的客戶端請求路徑:" + path);
        // 根據(jù)處理好的 path 作為條件去map中查找對應(yīng)的方法
        InvocationHandler handler = HandlerMap.get(path);
        // 獲取到對應(yīng)的類實(shí)例對象和Java方法
        Object object = handler.getObject();
        Method method = handler.getMethod();

        // 判斷該方法上是否添加了@ResponseBody注解:
        //      true:直接返回?cái)?shù)據(jù)  false:跳轉(zhuǎn)頁面
        boolean f = method.isAnnotationPresent(ResponseBody.class);
        System.out.println("是否添加了@ResponseBody注解:" + f);
        // 如果方法上存在@ResponseBody注解
        if (f){
            try {
                // 通過反射的方式調(diào)用方法并執(zhí)行
                Object invoke = method.invoke(object);
                // 將結(jié)果通過Response直接寫回給客戶端
                resp.getWriter().print(invoke.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else{
            // 獲取客戶端的請求路徑作為返回時(shí)的前路徑
            String URL = "http://" + req.getServerName() + ":" +
                    req.getServerPort() + "/" + req.getContextPath();
            System.out.println("URL:" + URL);
            // 自定義的前后綴(原本也是在xml中讀取)
            String prefix = "";
            String suffix = ".jsp";
            try {
                // 通過反射機(jī)制,執(zhí)行對應(yīng)的Java方法
                Object invoke = method.invoke(object);
                if(invoke instanceof ModelAndView){
                    // 如果是返回的ModelAndView對象,這里做額外處理....
                } else{
                    // 獲取Java方法執(zhí)行之后的返回結(jié)果
                    String str = (String)invoke;
                    // 如果指定了跳轉(zhuǎn)方法為 forward: 轉(zhuǎn)發(fā)
                    if(str.contains("forward:")){
                        System.out.println("以轉(zhuǎn)發(fā)的方式跳轉(zhuǎn)頁面...");
                        req.getRequestDispatcher("index.jsp").forward(req,resp);
                    }
                    // 如果指定了跳轉(zhuǎn)方法為 redirect: 重定向
                    if(str.contains("redirect:")){
                        System.out.println("以重定向的方式跳轉(zhuǎn)頁面...");
                        resp.sendRedirect(URL + prefix +
                            str.replace("redirect:","") + suffix);
                    }
                    // 如果沒有指定,則默認(rèn)使用轉(zhuǎn)發(fā)的方式跳轉(zhuǎn)頁面
                    if(!str.contains("forward:") && !str.contains("redirect:")){
                        resp.sendRedirect(URL + prefix + str + suffix);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

由于DispacherServlet實(shí)現(xiàn)了HttpServlet抽象類,因此也重寫了它的三個(gè)方法:init()、doGet()、doPost(),其中init()方法會(huì)在項(xiàng)目啟動(dòng)時(shí)執(zhí)行,而doGet()、doPost()則會(huì)在客戶端請求時(shí)被觸發(fā)。
總結(jié)一下上述DispacherServlet所做的工作:

①初始化所有請求路徑與Java方法之間的映射關(guān)系。
②根據(jù)客戶端的請求路徑,查找對應(yīng)的Java方法并執(zhí)行。
③判斷方法上是否添加了@ResponseBody注解:

添加了:直接向客戶端返回?cái)?shù)據(jù)。
未添加:跳轉(zhuǎn)對應(yīng)的頁面。

④以重定向或轉(zhuǎn)發(fā)的方式跳轉(zhuǎn)對應(yīng)的頁面。

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>
    <!-- 匹配規(guī)則依舊是所有請求路徑都會(huì)匹配 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

3.4、編寫View視圖
當(dāng)然,不追求外觀了,簡單編寫兩個(gè)視圖頁面: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>歡迎來到熊貓高級(jí)會(huì)所,我是竹子一號(hào)!</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="#">跳轉(zhuǎn)</a>
  </body>
</html>
3.5、編寫測試用例

為了方便測試,先寫一個(gè)實(shí)體類: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 +
                '}';
    }
}

這個(gè)實(shí)體類主要方便為了待會(huì)兒測試@ResponseBody注解的功能,接下來寫兩個(gè)Controller類:

/* ------ UserController類 ------- */ 
@Controller
@RequestMapping("/user")
public class UserController {
    
    // 測試@ResponseBody的功效
    @RequestMapping("/get")
    @ResponseBody
    public User get(){
        return new User(1,"竹子愛熊貓","男",18);
    }
    
    // 跳轉(zhuǎn)首頁的方法
    @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兩個(gè)類,其中僅有UserController加了@Controller注解,下面來測試,首先將這個(gè)Maven工程打成war包部署在Tomcat中,然后啟動(dòng),日志如下:

項(xiàng)目啟動(dòng)了.....
掃描到類的數(shù)量為:1
類的請求路徑:/user
類中方法數(shù)量為:4
方法上的請求路徑:/get
方法上的請求路徑:/test
方法上的請求路徑:/edit
HandlerMap的長度:3

從上述日志輸出中,很明顯可以看出,未添加@Controller注解的OrderController類并未被掃描,同時(shí),UserController類中未添加@RequestMapping注解的TEST()方法,也沒有被加入到HandlerMap集合中,該集合中僅存放了有映射關(guān)系的Java方法。
OK~,接下來使用瀏覽器測試我們手寫的SpringMVC是否可以做到原本的效果:

測試首頁跳轉(zhuǎn)效果:http://localhost:8080/

image.png

效果很明顯,首頁的跳轉(zhuǎn)正常,再來試試重定向的效果,如下:

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

image.png

輸入上述給出的url后,能夠很完美的重定向到edit.jsp頁面,日志輸出如下:

客戶端請求路徑:http://localhost:8080/user/edit
處理后的客戶端請求路徑:/user/edit
是否添加了@ResponseBody注解:false
URL:http://localhost:8080/
以重定向的方式跳轉(zhuǎn)頁面...

緊接著最后來試試@ResponseBody注解的效果,在瀏覽器輸入如下網(wǎng)址:

http://localhost:8080/user/get

image.png

效果依舊明顯,上述確實(shí)是我們想要的效果,不會(huì)發(fā)生頁面跳轉(zhuǎn),僅返回了對應(yīng)的數(shù)據(jù),再看看控制臺(tái):

客戶端請求路徑:http://localhost:8080/user/get
處理后的客戶端請求路徑:/user/get
是否添加了@ResponseBody注解:true

由于我們在UserController.get()方法上添加了@ResponseBody注解的原因,因此確實(shí)未發(fā)生頁面跳轉(zhuǎn)。

OK~,至此一個(gè)簡單的Mini版SpringMVC框架就完成了,實(shí)現(xiàn)很簡單,但效果卻很顯然。不過也存在很多缺陷未完善,大家有興趣的可以在這個(gè)項(xiàng)目的結(jié)構(gòu)上進(jìn)一步拓展與抽象,將SpringMVC真正的各大組件抽出來,同時(shí)也對于其他一些功能進(jìn)行拓展實(shí)現(xiàn)。

四、手寫SpringMVC框架總結(jié)

最后結(jié)合手寫SpringMVC的過程,再談?wù)凷pringMVC工作流程的理解,其實(shí)在咱們把一個(gè)JavaWeb程序打成war包丟入Tomcat后,當(dāng)啟動(dòng)Tomcat時(shí),它就會(huì)先去加載web.xml文件,而加載web.xml文件時(shí),會(huì)碰到DispacherServlet需要被加載,所以又會(huì)去加載它,當(dāng)加載DispacherServlet時(shí),其實(shí)本質(zhì)上會(huì)把SpringMVC的組件初始化,然后將所有Controller的URL資源都映射到一個(gè)容器中存儲(chǔ)。
當(dāng)后續(xù)客戶端發(fā)生請求時(shí),首先會(huì)根據(jù)配置好的路由規(guī)則,所有請求會(huì)先進(jìn)入DispacherServlet,DispacherServlet會(huì)先解析客戶端的請求路徑,然后根據(jù)路徑去容器中找到該Url對應(yīng)的Java方法,找到之后再調(diào)用組件去執(zhí)行具體的Controller方法,當(dāng)執(zhí)行完之后,又會(huì)將結(jié)果返回給DispacherServlet,此時(shí)又會(huì)去調(diào)用相關(guān)組件處理執(zhí)行后的結(jié)果,最后才將渲染后的結(jié)果響應(yīng)。

最后,如果在面試中遇到了面試官問你SpringMVC的工作原理(流程),最好可以結(jié)合自己的理解去回答,比如上述給出的這套總結(jié)一樣,因此如果按照八股文中的死流程去述說,并不能給面試官帶來眼前一亮的感覺,因?yàn)楸乘赖牧鞒毯苋菀捉o人帶來“靠臨時(shí)記憶來面試”的感覺,所以想要更好的收割offer,更多的還是要看自己對于技術(shù)的理解程度,還有你的思維邏輯。

你面試時(shí),如果回答能比他人更有深度以及你自己的思考,自然你就比其他候選者的機(jī)會(huì)更大,畢竟當(dāng)下內(nèi)卷越來越嚴(yán)重,一個(gè)能讓面試官眼前一亮的候選者,自然也會(huì)給面試官帶來不同的體驗(yàn),因此你收到Offer的幾率也會(huì)更高。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容