Spring系列之手寫一個SpringMVC

寧願。發表於2019-01-14

目錄

引言

在前面的幾個章節中我們已經簡單的完成了一個簡易版的spring,已經包括容器,依賴注入,AOP和配置檔案解析等功能。這一節我們來實現一個自己的springMvc。

關於MVC/SpringMVC

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

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

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

SpringMVC

SpringMVC處理請求流程

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

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介面,介面中提供一個處理方法。

Controller

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

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

ModelAndView

新增hasView()方法是因為我們返回的並不是必須要有頁面的資訊,比如返回json值。

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

View

方法級別的對映

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

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

annotation

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

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

請求分發

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

請求分發

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

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

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

HandlerMapping

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

handlerMapping

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

HandlerAdapter

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

handlerAdapter

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

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

requestMappinginfo

類的定義中classRequestMapping表示作用在類上的@RequestMappingmethodRequestMapping表示作用在方法上的@RequestMappingmethod表示作用在類上的方法資訊。match(...)方法是用來檢測當前請求與這個RequestMappingInfo是否相匹配。

掃描註冊

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

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

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

image

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. 從容器中獲取HandlerMappingHandlerAdapter物件。
  3. 分發請求
  4. view轉發

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

  • 容器物件的初始化和獲取HandlerMappingHandlerAdapter物件在init(...)完成。
  • 請求分發由service(HttpServletRequest req, HttpServletResponse res)完成。
  • destroy()完成關閉後的處理。

DispatcherServlet

View

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

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

ModelAndView

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

ViewResolver

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

ViewResolver

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

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

View

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

小結

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

總結

Spring的手寫框架差不多就是這些了,寫這一系列文章是為了鞏固我的Spring學習的成果,當然如果能幫助大家學習Spring當然是更好了。Spring的內容十分的繁雜,涉及的內容多,這一系列文章只能幫助大家對Spring的原理有一個最初的瞭解,在看Spring原始碼的過程中不至於完全就是一頭霧水。因為技術水平的原因,文章中可能還存在著一些錯誤,歡迎大家指出。

相關文章