Spring系列之手寫一個SpringMVC源碼分析

關於MVC/SpringMVC

springMvc是一個基於mvc模式的web框架,SpringMVC框架是一種提供了MVC(模型 - 視圖 - 控制器)架構和用於開發靈活和鬆散耦合的Web應用程序的組件。

MVC模式使得應用程序的不同部分分離,同時提供這些元素之間的鬆散耦合。

  • 模型(Model)封裝了應用程序數據,通常是指普通的bean。
  • 視圖(View)負責渲染模型數據,一般來說它生成客戶端瀏覽器可以解釋HTML輸出。
  • 控制器(Controller)負責處理用戶請求並獲取請求結果,將其傳遞給視圖進行渲染。

SpringMVC

SpringMVC處理請求流程

首先我們來了解一下SpringMVC在處理http請求的整個流程中都在做些什麼事。

Spring系列之手寫一個SpringMVC源碼分析

從上圖中我們可以總結springmvc的處理流程:

  1. 客戶端發送請求,被web容器(tomcat等)攔截到,web容器將請求交給DispatcherServlet。
  2. DispatcherServlet收到請求後將請求交給HandlerMapping(處理器映射器)查找該請求對應的Handler(處理器)。實際上這個過程在圖上是分為兩個的,這是因為一個請求url可能會有多個請求處理器,比如GET請求,POST請求等就是不同的處理器對象來處理的,所以需要一個HandlerAdapter(處理器適配器)來根據不同的請求參數來獲取對應的處理器對象。
  3. 獲取到請求的處理器對象後,執行處理器的請求處理流程。這裡的處理流程一般是值我們在開發中定義的業務流程。
  4. 處理流程執行完畢後將返回的結果包裝為一個ModelAndView對象返回給DispatcherServlet。
  5. DispatcherServlet通過ViewResolver(視圖解析器)將ModelAndView解析為View。
  6. 通過View渲染頁面,響應給用戶。

上面就是一個http請求從開始帶完成響應中由SpringMVC完成的流程,我們的SpringMVC沒有實際的那麼複雜,不過相應的功能都會進行實現。

SpringMVC分析

我們知道SpringMVC是實現了MVC模式的一個web框架,所以肯定有ModelViewController三種角色。從上圖中我們還可以看到一個十分重要的類:DispatcherServlet。接下來我們單獨來分析。

Controller

控制器(Controller)負責處理用戶請求並獲取請求結果,將其傳遞給視圖進行渲染。

在SpringMVC中Controller負責來處理由DispatcherServlet分發來的請求,並將進過業務處理後的請求結果包裝成一個Model提供給View使用。在SpringMVC中將請求映射到對應的Controller上是給我們提供了兩種不同的方法:

  1. 實例級別的映射,每一個請求都有一個對應的類實例來處理,類似於Struts2。這種方法實際很少使用。要實現這種類型需要實現一個Controller接口。
  2. 方法級別的映射,請求映射到bean的方法上,這樣每一個Controller可以對應多個請求,同時也能更易於保證併發請求的線程安全。

實例級別的映射

考慮如何實現實例級別的映射?

在實例級別映射中每一個請求對應一個不同的類,即一個URL<==>一個Class,這樣我們可以將beanName和請求地址進行對應。

同時我們框架需要提供一個請求處理的入口供使用者實現業務代碼。這裡我們定義一個Controller接口,接口中提供一個處理方法。

Spring系列之手寫一個SpringMVC源碼分析

接口中包含一個handlerRequest的處理方法,所有實例級別的Controller都需要Controller接口。在handlerRequest方法中完成業務邏輯。因為目前我們還無法判斷業務邏輯完成後需要返回那種類型的值,所以用Object代替。

這裡要確定方法應該返回什麼,我們首先得明白返回值用來幹嘛的?這個返回的值包含了業務處理的結果。並且返回給頁面用於頁面渲染。所以肯定是持有一個結果值和需要返回的具體的頁面。考慮到返回的值不僅僅是業務處理的結果,可能用戶需要設置一些其他的值給頁面,我們定義一個map類型來接收。

Spring系列之手寫一個SpringMVC源碼分析

添加hasView()方法是因為我們返回的並不是必須要有頁面的信息,比如返回json值。

view的工作非常簡單,就是講我們返回的值響應給瀏覽器。所以view的接口是這樣的:

Spring系列之手寫一個SpringMVC源碼分析

方法級別的映射

用過SpringMVC的都很清楚,上面那種實例級別映射的方式基本上都不會使用。實例級別的映射一旦項目中的請求多了將會導致項目中的類特別的多。我們平時用的多的是方法級別的映射。

我們這裡方法級別的映射不需要實現Controller接口,這裡我們仿造SpringMVC使用@Controller來表示一個控制器,使用@RequestMapping來表示不同的請求。

Spring系列之手寫一個SpringMVC源碼分析

RequestMethod是指http請求類型,包括GEt,POST,PUT等類型,一個枚舉類型。

方法映射的處理除了映射到方法上外其他和實例映射類似。

請求分發

客戶端發送請求,後端接收到請求後,需要將請求分發到對應的處理器上,從宏觀角度說就是請求交給DispatcherServlet,然後由DispatcherServlet分發給不同的處理器。我們很明顯需要知道請求是如何分發到處理器上的。

Spring系列之手寫一個SpringMVC源碼分析

不同類型的映射對應的請求處理器肯定是不一樣的,比如對於實例映射是通過實現Controller接口,處理也是關於接口的,而方法映射是通過註解實現。即不同的方式,映射方式不同,請求處理器也不一樣。

簡單的方法就是分別定義處理不同類型的處理器,然後在DispatcherServlet中通過判斷確定具體的處理器。這樣處理思路很簡單,但是問題在於假設我們要再添加一種處理的方式,那麼就需要改變原有的代碼,很明顯的違反了開閉原則,也會給代碼維護帶來麻煩。所以我們希望有一種DispatcherServlet能避開這種改變的方式。

我們這裡需要一個能夠根據傳遞進來的不同的請求來調用不同的處理器,而且能夠簡單的進行擴展而不改動原代碼。這裡肯定就需要用到設計模式了,那麼使用什麼設計模式呢? 策略模式。

HandlerMapping

我們定義一個用於請求處理器映射的接口,該接口的作用就是獲取一個請求具體的請求處理器,不同方式的處理方式分別實現該接口。

Spring系列之手寫一個SpringMVC源碼分析

BeanNameUrlHandlerMapping就是實例映射的處理器映射器,RequestMappingHandlerMapping就是方法映射的處理器。而如何將請求和HandlerMapping對應起來呢?我們能想到的就是url了,這裡我們定義一個urlMaps用來存儲url和處理器映射器的對應關係。

HandlerAdapter

現在我們有了HandlerMapping後就可以獲取到某一種類型的處理方式的處理對象。但是實際上我們還是沒有獲取到實際的處理器,所以我們還需要根據請求來獲取到實際的處理器,這裡我們定義一個HandlerAdapter來獲取實際的處理器。

Spring系列之手寫一個SpringMVC源碼分析

handler(...)就是具體的處理方法,實際上就是執行控制器,而support主要是用於判斷是否是一個處理器對象。

這裡很明顯對於實例映射來說我們只需要執行方法中的handlerRequest(...)方法即可,但是對於方法映射就不是那麼簡單了,不同的方法根據@RequestMapping表示不同的請求。所以我們還需要一個類來表示不同的方法信息,便於請求傳遞過來後直接取用。

Spring系列之手寫一個SpringMVC源碼分析

類的定義中classRequestMapping表示作用在類上的@RequestMapping,methodRequestMapping表示作用在方法上的@RequestMapping,method表示作用在類上的方法信息。match(...)方法是用來檢測當前請求與這個RequestMappingInfo是否相匹配。

掃描註冊

基本上我們的準備工作完成了,現在我們需要考慮如何來識別我們的控制器和生成RequestMappingInfo的信息。

首先創建的時機肯定是在項目啟動的時候就講這些信息初始化好,因為請求過來後會立刻使用到這些信息。而初始化這些信息的行為在哪裡發生呢?我的第一反應是交給DispatcherServlet,因為直觀來講是它來使用,實際上真正的使用這些信息的事HandlerMapping的實現類,通過請求和RequestMappingInfo等信息來獲取實際的處理器,所以初始化的信息應該交給HandlerMapping的實現類。

我們要明白的事提取帶有Controller註解的bean或者是實現類Controller接口的類肯定是在bean初始化之後進行的,所以我們需要提供一個在初始化後獲取控制器類型的接口。同時獲取已經初始化好的類那麼肯定會使用到ApplicationContext。我們現在對RequestMapping接口修改下。

Spring系列之手寫一個SpringMVC源碼分析

在afterPropertiesSet()方法中獲取控制器類型的bean。在我們之前完成的代碼中只提供了通過beanName獲取bean的方法,所以這裡我們還需要提供一種獲取所有的執行類型的方法。

public void afterPropertiesSet() {
String[] beanNameForType = applicationContext.getBeanNameForType(Object.class);
for(String beanName:beanNameForType){
Class type = applicationContext.getType(beanName);
//判斷是否是控制器類型
if (isHandler(type)) {
//註冊控制器的類型
detectHandlerMethod(type);
}
}
}

DispatcherServlet

好了,到現在對於控制器的準備已經差不多了,現在我們需要來實現DispatcherServlet了。

從DispatcherServlet名字來看就知道這是一個Servlet,我們的框架是基於Servlet來完成的,SpringMVC框架本身也是基於Servlet的。當然也可以根據其他技術來實現,比如基於Filter的Struts2。

我們先來捋一下DispatcherServlet需要完成的任務吧:

  1. 創建ApplicationContext容器對象
  2. 從容器中獲取HandlerMapping,HandlerAdapter對象。
  3. 分發請求
  4. view轉發

熟悉Servlet的都應該知道Servlet提供了一系列生命週期的API,上面的這些事情都需要在Servlet生命週期的不同階段來完成。

  • 容器對象的初始化和獲取HandlerMapping,HandlerAdapter對象在init(...)完成。
  • 請求分發由service(HttpServletRequest req, HttpServletResponse res)完成。
  • destroy()完成關閉後的處理。
Spring系列之手寫一個SpringMVC源碼分析

在之前我們定義控制器的時候有說到由控制器來返回一個ModelAndView對象,該對象確定具體返回哪一個頁面和處理結果的數據。這樣不僅需要提供ModelAndView對象,同時還需要提供一個View的對象,現在我們希望這個過程能夠儘量的簡單,使用者可以僅僅提供一個視圖的名稱,然後框架就可以自動的找到對應的頁面然後進行渲染。

我們現在需要重新定義ModelAndViewView類。

Spring系列之手寫一個SpringMVC源碼分析

這樣用戶可以傳遞一個名稱過來,然後由HandlerAdapter根據傳遞的handler來生成ModelAndView,同時也可以自定義ModelAndView對象。

ViewResolver

當我們前面的準備工作都做好了並不代表就已經可以完成了,因為對於不同的視圖可能會有不同的操作,比如直接轉發給一個URL,可能還會重定向到另一個URL,或者直接就是返回json串的。所以我們還需要定義不同的視圖解析器來將ModelAndView解析成相應的View。

Spring系列之手寫一個SpringMVC源碼分析

這裡定義了一個解析JSP視圖的解析器,同理也可以定義其他的處理器。

定義好了視圖解析器後我們還需要定義幾個用於處理不同情況的視圖類。

Spring系列之手寫一個SpringMVC源碼分析

這裡的View類型還可以根據不同的需求添加其他類型的處理器,比如freemarker、JSTL等。對於json處理我們還需要像SpringMVC那樣來定義一個@ResponseBody的註解。當使用了該註解的時候我們就將返回值轉換為JSON串然後直接通過response返回給客戶端即可。

小結

SpringMVC到這裡就基本結束了,總得來說這一篇的內容稍微比較麻煩。主要是涉及到的內容較多,再加上這段時間比較忙,平時就下班後抽時間整理,目前也只是將思路基本捋完。代碼也只是整理了一個框架,內容還沒有進行填充。所以文章中可能會有一些錯誤的地方,大家如果發現了可以指出來。後面有時間會將代碼實現的。

關注我:私信回覆“555”獲取往期Java高級架構資料、源碼、筆記、視頻Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術往期架構視頻截圖


分享到:


相關文章: