引言
對于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視圖:View在SpringMVC中是一個(gè)接口,實(shí)現(xiàn)類支持不同的類型,例如jsp、freemarker、ftl...,不過現(xiàn)在一般都是前后端分離的項(xiàng)目,因此也很少再用到這塊內(nèi)容,視圖一般都成了html頁面,數(shù)據(jù)結(jié)果的渲染工作也交給了前端完成。
大致對于SpringMVC的核心組件有了了解之后,再上一張圖:

對于這張圖,相信大家都多多少少有在“面試八股文”中看到過,這也是涵蓋了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對象。 - ⑦
HandlerAdapter將ModelAndView再返回給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/

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

輸入上述給出的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

效果依舊明顯,上述確實(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ì)更高。