目錄
引言
在前面的幾個章節中我們已經簡單的完成了一個簡易版的spring,已經包括容器,依賴注入,AOP和配置文件解析等功能。這一節我們來實現一個自己的springMvc。
關於MVC/SpringMVC
springMvc是一個基於mvc模式的web框架,SpringMVC框架是一種提供了MVC(模型 - 視圖 - 控制器)架構和用於開發靈活和松散耦合的Web應用程序的組件。
MVC模式使得應用程序的不同部分分離,同時提供這些元素之間的松散耦合。
- 模型(Model)封裝了應用程序數據,通常是指普通的bean。
- 視圖(View)負責渲染模型數據,一般來說它生成客戶端瀏覽器可以解釋HTML輸出。
- 控制器(Controller)負責處理用戶請求並獲取請求結果,將其傳遞給視圖進行渲染。
SpringMVC
SpringMVC處理請求流程
首先我們來了解一下SpringMVC在處理http請求的整個流程中都在做些什么事。
//圖片來源於網絡
從上圖中我們可以總結springmvc的處理流程:
- 客戶端發送請求,被web容器(tomcat等)攔截到,web容器將請求交給DispatcherServlet。
- DispatcherServlet收到請求后將請求交給HandlerMapping(處理器映射器)查找該請求對應的Handler(處理器)。實際上這個過程在圖上是分為兩個的,這是因為一個請求url可能會有多個請求處理器,比如GET請求,POST請求等就是不同的處理器對象來處理的,所以需要一個HandlerAdapter(處理器適配器)來根據不同的請求參數來獲取對應的處理器對象。
- 獲取到請求的處理器對象后,執行處理器的請求處理流程。這里的處理流程一般是值我們在開發中定義的業務流程。
- 處理流程執行完畢后將返回的結果包裝為一個ModelAndView對象返回給DispatcherServlet。
- DispatcherServlet通過ViewResolver(視圖解析器)將ModelAndView解析為View。
- 通過View渲染頁面,響應給用戶。
上面就是一個http請求從開始帶完成響應中由SpringMVC完成的流程,我們的SpringMVC沒有實際的那么復雜,不過相應的功能都會進行實現。
SpringMVC分析
我們知道SpringMVC是實現了MVC模式的一個web框架,所以肯定有Model,View,Controller三種角色。從上圖中我們還可以看到一個十分重要的類:DispatcherServlet。接下來我們單獨來分析。
Controller
控制器(Controller)負責處理用戶請求並獲取請求結果,將其傳遞給視圖進行渲染。
在SpringMVC中Controller負責來處理由DispatcherServlet分發來的請求,並將進過業務處理后的請求結果包裝成一個Model提供給View使用。在SpringMVC中將請求映射到對應的Controller上是給我們提供了兩種不同的方法:
- 實例級別的映射,每一個請求都有一個對應的類實例來處理,類似於Struts2。這種方法實際很少使用。要實現這種類型需要實現一個Controller接口。
- 方法級別的映射,請求映射到bean的方法上,這樣每一個Controller可以對應多個請求,同時也能更易於保證並發請求的線程安全。
實例級別的映射
考慮如何實現實例級別的映射?
在實例級別映射中每一個請求對應一個不同的類,即一個URL<==>一個Class
,這樣我們可以將beanName和請求地址進行對應。
同時我們框架需要提供一個請求處理的入口供使用者實現業務代碼。這里我們定義一個Controller接口,接口中提供一個處理方法。
接口中包含一個handlerRequest的處理方法,所有實例級別的Controller都需要Controller接口。在handlerRequest方法中完成業務邏輯。因為目前我們還無法判斷業務邏輯完成后需要返回那種類型的值,所以用Object代替。
這里要確定方法應該返回什么,我們首先得明白返回值用來干嘛的?這個返回的值包含了業務處理的結果。並且返回給頁面用於頁面渲染。所以肯定是持有一個結果值和需要返回的具體的頁面。考慮到返回的值不僅僅是業務處理的結果,可能用戶需要設置一些其他的值給頁面,我們定義一個map類型來接收。
添加hasView()方法是因為我們返回的並不是必須要有頁面的信息,比如返回json值。
view的工作非常簡單,就是講我們返回的值響應給瀏覽器。所以view的接口是這樣的:
方法級別的映射
用過SpringMVC的都很清楚,上面那種實例級別映射的方式基本上都不會使用。實例級別的映射一旦項目中的請求多了將會導致項目中的類特別的多。我們平時用的多的是方法級別的映射。
我們這里方法級別的映射不需要實現Controller接口,這里我們仿造SpringMVC使用@Controller
來表示一個控制器,使用@RequestMapping
來表示不同的請求。
RequestMethod是指http請求類型,包括GEt,POST,PUT
等類型,一個枚舉類型。
方法映射的處理除了映射到方法上外其他和實例映射類似。
請求分發
客戶端發送請求,后端接收到請求后,需要將請求分發到對應的處理器上,從宏觀角度說就是請求交給DispatcherServlet,然后由DispatcherServlet分發給不同的處理器。我們很明顯需要知道請求是如何分發到處理器上的。
不同類型的映射對應的請求處理器肯定是不一樣的,比如對於實例映射是通過實現Controller接口,處理也是關於接口的,而方法映射是通過注解實現。即不同的方式,映射方式不同,請求處理器也不一樣。
簡單的方法就是分別定義處理不同類型的處理器,然后在DispatcherServlet中通過判斷確定具體的處理器。這樣處理思路很簡單,但是問題在於假設我們要再添加一種處理的方式,那么就需要改變原有的代碼,很明顯的違反了開閉原則,也會給代碼維護帶來麻煩。所以我們希望有一種DispatcherServlet能避開這種改變的方式。
我們這里需要一個能夠根據傳遞進來的不同的請求來調用不同的處理器,而且能夠簡單的進行擴展而不改動原代碼。這里肯定就需要用到設計模式了,那么使用什么設計模式呢? 策略模式。
HandlerMapping
我們定義一個用於請求處理器映射的接口,該接口的作用就是獲取一個請求具體的請求處理器,不同方式的處理方式分別實現該接口。
BeanNameUrlHandlerMapping就是實例映射的處理器映射器,RequestMappingHandlerMapping就是方法映射的處理器。而如何將請求和HandlerMapping對應起來呢?我們能想到的就是url了,這里我們定義一個urlMaps用來存儲url和處理器映射器的對應關系。
HandlerAdapter
現在我們有了HandlerMapping后就可以獲取到某一種類型的處理方式的處理對象。但是實際上我們還是沒有獲取到實際的處理器,所以我們還需要根據請求來獲取到實際的處理器,這里我們定義一個HandlerAdapter來獲取實際的處理器。
handler(...)就是具體的處理方法,實際上就是執行控制器,而support主要是用於判斷是否是一個處理器對象。
這里很明顯對於實例映射來說我們只需要執行方法中的handlerRequest(...)方法即可,但是對於方法映射就不是那么簡單了,不同的方法根據@RequestMapping
表示不同的請求。所以我們還需要一個類來表示不同的方法信息,便於請求傳遞過來后直接取用。
類的定義中classRequestMapping
表示作用在類上的@RequestMapping
,methodRequestMapping
表示作用在方法上的@RequestMapping
,method
表示作用在類上的方法信息。match(...)
方法是用來檢測當前請求與這個RequestMappingInfo是否相匹配。
掃描注冊
基本上我們的准備工作完成了,現在我們需要考慮如何來識別我們的控制器和生成RequestMappingInfo
的信息。
首先創建的時機肯定是在項目啟動的時候就講這些信息初始化好,因為請求過來后會立刻使用到這些信息。而初始化這些信息的行為在哪里發生呢?我的第一反應是交給DispatcherServlet
,因為直觀來講是它來使用,實際上真正的使用這些信息的事HandlerMapping的實現類,通過請求和RequestMappingInfo等信息來獲取實際的處理器,所以初始化的信息應該交給HandlerMapping的實現類。
我們要明白的事提取帶有Controller注解的bean或者是實現類Controller接口的類肯定是在bean初始化之后進行的,所以我們需要提供一個在初始化后獲取控制器類型的接口。同時獲取已經初始化好的類那么肯定會使用到ApplicationContext
。我們現在對RequestMapping
接口修改下。
在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
需要完成的任務吧:
- 創建ApplicationContext容器對象
- 從容器中獲取
HandlerMapping
,HandlerAdapter
對象。 - 分發請求
- view轉發
熟悉Servlet的都應該知道Servlet提供了一系列生命周期的API,上面的這些事情都需要在Servlet生命周期的不同階段來完成。
- 容器對象的初始化和獲取
HandlerMapping
,HandlerAdapter
對象在init(...)完成。 - 請求分發由
service(HttpServletRequest req, HttpServletResponse res)
完成。 destroy()
完成關閉后的處理。
View
在之前我們定義控制器的時候有說到由控制器來返回一個ModelAndView對象,該對象確定具體返回哪一個頁面和處理結果的數據。這樣不僅需要提供ModelAndView對象,同時還需要提供一個View的對象,現在我們希望這個過程能夠盡量的簡單,使用者可以僅僅提供一個視圖的名稱,然后框架就可以自動的找到對應的頁面然后進行渲染。
我們現在需要重新定義ModelAndViewView類。
這樣用戶可以傳遞一個名稱過來,然后由HandlerAdapter根據傳遞的handler來生成ModelAndView,同時也可以自定義ModelAndView對象。
ViewResolver
當我們前面的准備工作都做好了並不代表就已經可以完成了,因為對於不同的視圖可能會有不同的操作,比如直接轉發給一個URL,可能還會重定向到另一個URL,或者直接就是返回json串的。所以我們還需要定義不同的視圖解析器來將ModelAndView解析成相應的View。
這里定義了一個解析JSP視圖的解析器,同理也可以定義其他的處理器。
定義好了視圖解析器后我們還需要定義幾個用於處理不同情況的視圖類。
這里的View類型還可以根據不同的需求添加其他類型的處理器,比如freemarker、JSTL等。對於json處理我們還需要像SpringMVC那樣來定義一個@ResponseBody
的注解。當使用了該注解的時候我們就將返回值轉換為JSON串然后直接通過response返回給客戶端即可。
小結
SpringMVC到這里就基本結束了,總得來說這一篇的內容稍微比較麻煩。主要是涉及到的內容較多,再加上這段時間比較忙,平時就下班后抽時間整理,目前也只是將思路基本捋完。代碼也只是整理了一個框架,內容還沒有進行填充。所以文章中可能會有一些錯誤的地方,大家如果發現了可以指出來。后面有時間會將代碼實現的。代碼都在這里:Spring。
總結
Spring的手寫框架差不多就是這些了,寫這一系列文章是為了鞏固我的Spring學習的成果,當然如果能幫助大家學習Spring當然是更好了。Spring的內容十分的繁雜,涉及的內容多,這一系列文章只能幫助大家對Spring的原理有一個最初的了解,在看Spring源碼的過程中不至於完全就是一頭霧水。因為技術水平的原因,文章中可能還存在着一些錯誤,歡迎大家指出。