新聞中心
本文轉(zhuǎn)載自IBM Developer Works廖雪峰的文章,原文標(biāo)題為《設(shè)計(jì) REST 風(fēng)格的 MVC 框架》(地址:http://www.ibm.com/developerworks/cn/java/j-lo-restmvc/?ca=drs-tp4608)。

創(chuàng)新互聯(lián)建站-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比會(huì)寧網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式會(huì)寧網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋會(huì)寧地區(qū)。費(fèi)用合理售后完善,十多年實(shí)體公司更值得信賴。
Java 開發(fā)者對(duì) MVC 框架一定不陌生,從 Struts 到 WebWork,Java MVC 框架層出不窮。我們已經(jīng)習(xí)慣了處理 *.do 或 *.action 風(fēng)格的 URL,為每一個(gè) URL 編寫一個(gè)控制器,并繼承一個(gè) Action 或者 Controller 接口。然而,流行的 Web 趨勢(shì)是使用更加簡(jiǎn)單,對(duì)用戶和搜索引擎更加友好的 REST 風(fēng)格的 URL。例如,來自豆瓣的一本書的鏈接是 http://www.douban.com/subject/2129650/,而非 http://www.douban.com/subject.do?id=2129650。
有經(jīng)驗(yàn)的 Java Web 開發(fā)人員會(huì)使用 URL 重寫的方式來實(shí)現(xiàn)類似的 URL,例如,為前端 Apache 服務(wù)器配置 mod_rewrite 模塊,并依次為每個(gè)需要實(shí)現(xiàn) URL 重寫的地址編寫負(fù)責(zé)轉(zhuǎn)換的正則表達(dá)式,或者,通過一個(gè)自定義的 RewriteFilter,使用 Java Web 服務(wù)器提供的 Filter 和請(qǐng)求轉(zhuǎn)發(fā)(Forward)功能實(shí)現(xiàn) URL 重寫,不過,仍需要為每個(gè)地址編寫正則表達(dá)式。
既然 URL 重寫如此繁瑣,為何不直接設(shè)計(jì)一個(gè)原生支持 REST 風(fēng)格的 MVC 框架呢?
要設(shè)計(jì)并實(shí)現(xiàn)這樣一個(gè) MVC 框架并不困難,下面,我們從零開始,仔細(xì)研究如何實(shí)現(xiàn) REST 風(fēng)格的 URL 映射,并與常見的 IoC 容器如 Spring 框架集成。這個(gè)全新的 MVC 框架暫命名為 WebWind。
術(shù)語
MVC:Model-View-Controller,是一種常見的 UI 架構(gòu)模式,通過分離 Model(模型)、View(視圖)和 Controller(控制器),可以更容易實(shí)現(xiàn)易于擴(kuò)展的 UI。在 Web 應(yīng)用程序中,Model 指后臺(tái)返回的數(shù)據(jù);View 指需要渲染的頁面,通常是 JSP 或者其他模板頁面,渲染后的結(jié)果通常是 HTML;Controller 指 Web 開發(fā)人員編寫的處理不同 URL 的控制器(在 Struts 中被稱之為 Action),而 MVC 框架本身還有一個(gè)前置控制器,用于接收所有的 URL 請(qǐng)求,并根據(jù) URL 地址分發(fā)到 Web 開發(fā)人員編寫的 Controller 中。
IoC:Invertion-of-Control,控制反轉(zhuǎn),是目前流行的管理所有組件生命周期和復(fù)雜依賴關(guān)系的容器,例如 Spring 容器。
Template:模板,通過渲染,模板中的變量將被 Model 的實(shí)際數(shù)據(jù)所替換,然后,生成的內(nèi)容即是用戶在瀏覽器中看到的 HTML。模板也能實(shí)現(xiàn)判斷、循環(huán)等簡(jiǎn)單邏輯。本質(zhì)上,JSP 頁面也是一種模板。此外,還有許多第三方模板引擎,如 Velocity,F(xiàn)reeMarker 等。
設(shè)計(jì)目標(biāo)
和傳統(tǒng)的 Struts 等 MVC 框架完全不同,為了支持 REST 風(fēng)格的 URL,我們并不把一個(gè) URL 映射到一個(gè) Controller 類(或者 Struts 的 Action),而是直接把一個(gè) URL 映射到一個(gè)方法,這樣,Web 開發(fā)人員就可以將多個(gè)功能類似的方法放到一個(gè) Controller 中,并且,Controller 沒有強(qiáng)制要求必須實(shí)現(xiàn)某個(gè)接口。一個(gè) Controller 通常擁有多個(gè)方法,每個(gè)方法負(fù)責(zé)處理一個(gè) URL。例如,一個(gè)管理 Blog 的 Controller 定義起來就像清單 1 所示。
清單 1. 管理 Blog 的 Controller 定義
@Mapping() 注解指示了這是一個(gè)處理 URL 映射的方法,URL 中的參數(shù) $1、$2 ……則將作為方法參數(shù)傳入。對(duì)于一個(gè)“/blog/1234/5678”的 URL,對(duì)應(yīng)的方法將自動(dòng)獲得參數(shù) userId=1234 和 postId=5678。同時(shí),也無需任何與 URL 映射相關(guān)的 XML 配置文件。
使用 $1、$2 ……來定義 URL 中的可變參數(shù)要比正則表達(dá)式更簡(jiǎn)單,我們需要在 MVC 框架內(nèi)部將其轉(zhuǎn)化為正則表達(dá)式,以便匹配 URL。
此外,對(duì)于方法返回值,也未作強(qiáng)制要求。
集成 IoC
當(dāng)接收到來自瀏覽器的請(qǐng)求,并匹配到合適的 URL 時(shí),應(yīng)該轉(zhuǎn)發(fā)給某個(gè) Controller 實(shí)例的某個(gè)標(biāo)記有 @Mapping 的方法,這需要持有所有 Controller 的實(shí)例。不過,讓一個(gè) MVC 框架去管理這些組件并不是一個(gè)好的設(shè)計(jì),這些組件可以很容易地被 IoC 容器管理,MVC 框架需要做的僅僅是向 IoC 容器請(qǐng)求并獲取這些組件的實(shí)例。
為了解耦一種特定的 IoC 容器,我們通過 ContainerFactory 來獲取所有 Controller 組件的實(shí)例,如清單 2 所示。
清單 2. 定義 ContainerFactory
其中,關(guān)鍵方法 findAllBeans() 返回 IoC 容器管理的所有 Bean,然后,掃描每一個(gè) Bean 的所有 public 方法,并引用那些標(biāo)記有 @Mapping 的方法實(shí)例。
我們?cè)O(shè)計(jì)目標(biāo)是支持 Spring 和 Guice 這兩種容器,對(duì)于 Spring 容器,可以通過 ApplicationContext 獲得所有的 Bean 引用,代碼見清單 3。
清單 3. 定義 SpringContainerFactory
對(duì)于 Guice 容器,通過 Injector 實(shí)例可以返回所有綁定對(duì)象的實(shí)例,代碼見清單 4。
清單 4. 定義 GuiceContainerFactory
類似的,通過擴(kuò)展 ContainerFactory,就可以支持更多的 IoC 容器,如 PicoContainer。
出于效率的考慮,我們緩存所有來自 IoC 的 Controller 實(shí)例,無論其在 IoC 中配置為 Singleton 還是 Prototype 類型。當(dāng)然,也可以修改代碼,每次都從 IoC 容器中重新請(qǐng)求實(shí)例。
設(shè)計(jì)請(qǐng)求轉(zhuǎn)發(fā)
和 Struts 等常見 MVC 框架一樣,我們也需要實(shí)現(xiàn)一個(gè)前置控制器,通常命名為 DispatcherServlet,用于接收所有的請(qǐng)求,并作出合適的轉(zhuǎn)發(fā)。在 Servlet 規(guī)范中,有以下幾種常見的 URL 匹配模式:
- /abc:精確匹配,通常用于映射自定義的 Servlet;
- *.do:后綴模式匹配,常見的 MVC 框架都采用這種模式;
- /app/*:前綴模式匹配,這要求 URL 必須以固定前綴開頭;
- /:匹配默認(rèn)的 Servlet,當(dāng)一個(gè) URL 沒有匹配到任何 Servlet 時(shí),就匹配默認(rèn)的 Servlet。一個(gè) Web 應(yīng)用程序如果沒有映射默認(rèn)的 Servlet,Web 服務(wù)器會(huì)自動(dòng)為 Web 應(yīng)用程序添加一個(gè)默認(rèn)的 Servlet。
REST 風(fēng)格的 URL 一般不含后綴,我們只能將 DispatcherServlet 映射到“/”,使之變?yōu)橐粋€(gè)默認(rèn)的 Servlet,這樣,就可以對(duì)任意的 URL 進(jìn)行處理。
由于無法像 Struts 等傳統(tǒng)的 MVC 框架根據(jù)后綴直接將一個(gè) URL 映射到一個(gè) Controller,我們必須依次匹配每個(gè)有能力處理 HTTP 請(qǐng)求的 @Mapping 方法。完整的 HTTP 請(qǐng)求處理流程如圖 1 所示。
圖 1. 請(qǐng)求處理流程
當(dāng)掃描到標(biāo)記有 @Mapping 注解的方法時(shí),需要首先檢查 URL 與方法參數(shù)是否匹配,UrlMatcher 用于將 @Mapping 中包含 $1、$2 ……的字符串變?yōu)檎齽t表達(dá)式,進(jìn)行預(yù)編譯,并檢查參數(shù)個(gè)數(shù)是否符合方法參數(shù),代碼見清單 5。
清單 5. 定義 UrlMatcher
將 @Mapping 中包含 $1、$2 ……的字符串變?yōu)檎齽t表達(dá)式的轉(zhuǎn)換規(guī)則是,依次將每個(gè) $n 替換為 ([^\\/]*),其余部分作精確匹配。例如,“/blog/$1/$2”變化后的正則表達(dá)式為:
請(qǐng)注意,Java 字符串需要兩個(gè)連續(xù)的“\\”表示正則表達(dá)式中的轉(zhuǎn)義字符“\”。將“/”排除在變量匹配之外可以避免很多歧義。
調(diào)用一個(gè)實(shí)例方法則由 Action 類表示,它持有類實(shí)例、方法引用和方法參數(shù)類型,代碼見清單 6。
清單 6. 定義 Action
負(fù)責(zé)請(qǐng)求轉(zhuǎn)發(fā)的 Dispatcher 通過關(guān)聯(lián) UrlMatcher 與 Action,就可以匹配到合適的 URL,并轉(zhuǎn)發(fā)給相應(yīng)的 Action,代碼見清單 7。
清單 7. 定義 Dispatcher
當(dāng) Dispatcher 接收到一個(gè) URL 請(qǐng)求時(shí),遍歷所有的 UrlMatcher,找到***個(gè)匹配 URL 的 UrlMatcher,并從 URL 中提取方法參數(shù),代碼見清單 8。
清單 8. 匹配并從 URL 中提取參數(shù)
根據(jù) URL 找到匹配的 Action 后,就可以構(gòu)造一個(gè) Execution 對(duì)象,并根據(jù)方法簽名將 URL 中的 String 轉(zhuǎn)換為合適的方法參數(shù)類型,準(zhǔn)備好全部參數(shù),代碼見清單 9。
清單 9. 構(gòu)造 Exectuion
調(diào)用 execute() 方法就可以執(zhí)行目標(biāo)方法,并返回一個(gè)結(jié)果。請(qǐng)注意,當(dāng)通過反射調(diào)用方法失敗時(shí),我們通過查找 InvocationTargetException 的根異常并將其拋出,這樣,客戶端就能捕獲正確的原始異常。
為了***限度地增加靈活性,我們并不強(qiáng)制要求 URL 的處理方法返回某一種類型。我們?cè)O(shè)計(jì)支持以下返回值:
- String:當(dāng)返回一個(gè) String 時(shí),自動(dòng)將其作為 HTML 寫入 HttpServletResponse;
- void:當(dāng)返回 void 時(shí),不做任何操作;
- Renderer:當(dāng)返回 Renderer 對(duì)象時(shí),將調(diào)用 Renderer 對(duì)象的 render 方法渲染 HTML 頁面。
***需要考慮的是,由于我們將 DispatcherServlet 映射為“/”,即默認(rèn)的 Servlet,則所有的未匹配成功的 URL 都將由 DispatcherServlet 處理,包括所有靜態(tài)文件,因此,當(dāng)未匹配到任何 Controller 的 @Mapping 方法后,DispatcherServlet 將試圖按 URL 查找對(duì)應(yīng)的靜態(tài)文件,我們用 StaticFileHandler 封裝,主要代碼見清單 10。
清單 10. 處理靜態(tài)文件
處理靜態(tài)文件時(shí)要過濾 /WEB-INF/ 目錄,否則將造成安全漏洞。
集成模板引擎
作為示例,返回一個(gè)“
Hello, world!
”作為 HTML 頁面非常容易。然而,實(shí)際應(yīng)用的頁面通常是極其復(fù)雜的,需要一個(gè)模板引擎來渲染出 HTML??梢园?JSP 看作是一種模板,只要不在 JSP 頁面中編寫復(fù)雜的 Java 代碼。我們的設(shè)計(jì)目標(biāo)是實(shí)現(xiàn)對(duì) JSP 和 Velocity 這兩種模板的支持。和集成 IoC 框架類似,我們需要解耦 MVC 與模板系統(tǒng),因此,TemplateFactory 用于初始化模板引擎,并返回 Template 模板對(duì)象。TemplateFactory 定義見清單 11。
清單 11. 定義 TemplateFactory
Template 接口則實(shí)現(xiàn)真正的渲染任務(wù)。定義見清單 12。
清單 12. 定義 Template
以 JSP 為例,實(shí)現(xiàn) JspTemplateFactory 非常容易。代碼見清單 13。
清單 13. 定義 JspTemplateFactory
JspTemplate 用于渲染頁面,只需要傳入 JSP 的路徑,將 Model 綁定到 HttpServletRequest,就可以調(diào)用 Servlet 規(guī)范的 forward 方法將請(qǐng)求轉(zhuǎn)發(fā)給指定的 JSP 頁面并渲染。代碼見清單 14。
清單 14. 定義 JspTemplate
另一種比 JSP 更加簡(jiǎn)單且靈活的模板引擎是 Velocity,它使用更簡(jiǎn)潔的語法來渲染頁面,對(duì)頁面設(shè)計(jì)人員更加友好,并且完全阻止了開發(fā)人員試圖在頁面中編寫 Java 代碼的可能性。使用 Velocity 編寫的頁面示例如清單 15 所示。
清單 15. Velocity 模板頁面
通過 VelocityTemplateFactory 和 VelocityTemplate 就可以實(shí)現(xiàn)對(duì) Velocity 的集成。不過,從 Web 開發(fā)人員看來,并不需要知道具體使用的模板,客戶端僅需要提供模板路徑和一個(gè)由 Map
清單 16. 定義 TemplateRenderer
TemplateRenderer 通過簡(jiǎn)單地調(diào)用 render 方法就實(shí)現(xiàn)了頁面渲染。為了指定 Jsp 或 Velocity,需要在 web.xml 中配置 DispatcherServlet 的初始參數(shù)。配置示例請(qǐng)參考清單 17。
清單 17. 配置 Velocity 作為模板引擎
如果沒有該缺省參數(shù),那就使用默認(rèn)的 Jsp。
類似的,通過擴(kuò)展 TemplateFactory 和 Template,就可以添加更多的模板支持,例如 FreeMarker。
設(shè)計(jì)攔截器
攔截器和 Servlet 規(guī)范中的 Filter 非常類似,不過 Filter 的作用范圍是整個(gè) HttpServletRequest 的處理過程,而攔截器僅作用于 Controller,不涉及到 View 的渲染,在大多數(shù)情況下,使用攔截器比 Filter 速度要快,尤其是綁定數(shù)據(jù)庫事務(wù)時(shí),攔截器能縮短數(shù)據(jù)庫事務(wù)開啟的時(shí)間。
攔截器接口 Interceptor 定義如清單 18 所示。
清單 18. 定義 Interceptor
和 Filter 類似,InterceptorChain 代表攔截器鏈。InterceptorChain 定義如清單 19 所示。
清單 19. 定義 InterceptorChain
實(shí)現(xiàn) InterceptorChain 要比實(shí)現(xiàn) FilterChain 簡(jiǎn)單,因?yàn)?Filter 需要處理 Request、Forward、Include 和 Error 這 4 種請(qǐng)求轉(zhuǎn)發(fā)的情況,而 Interceptor 僅攔截 Request。當(dāng) MVC 框架處理一個(gè)請(qǐng)求時(shí),先初始化一個(gè)攔截器鏈,然后,依次調(diào)用鏈上的每個(gè)攔截器。請(qǐng)參考清單 20 所示的代碼。
清單 20. 實(shí)現(xiàn) InterceptorChain 接口
成員變量 index 表示當(dāng)前鏈上的第 N 個(gè)攔截器,當(dāng)***一個(gè)攔截器被調(diào)用后,InterceptorChain 才真正調(diào)用 Execution 對(duì)象的 execute() 方法,并保存其返回結(jié)果,整個(gè)請(qǐng)求處理過程結(jié)束,進(jìn)入渲染階段。清單 21 演示了如何調(diào)用攔截器鏈的代碼。
清單 21. 調(diào)用攔截器鏈
當(dāng) Controller 方法被調(diào)用完畢后,handleResult() 方法用于處理執(zhí)行結(jié)果。
渲染
由于我們沒有強(qiáng)制 HTTP 處理方法的返回類型,因此,handleResult() 方法針對(duì)不同的返回值將做不同的處理。代碼如清單 22 所示。
清單 22. 處理返回值
如果返回 null,則認(rèn)為 HTTP 請(qǐng)求已處理完成,不做任何處理;如果返回 Renderer,則調(diào)用 Renderer 對(duì)象的 render() 方法渲染視圖;如果返回 String,則根據(jù)前綴是否有“redirect:”判斷是重定向還是作為 HTML 返回給瀏覽器。這樣,客戶端可以不必訪問 HttpServletResponse 對(duì)象就可以非常方便地實(shí)現(xiàn)重定向。代碼如清單 23 所示。
清單 23. 重定向
擴(kuò)展 Renderer 還可以處理更多的格式,例如,向?yàn)g覽器返回 JavaScript 代碼等。
擴(kuò)展
使用 Filter 轉(zhuǎn)發(fā)
對(duì)于請(qǐng)求轉(zhuǎn)發(fā),除了使用 DispatcherServlet 外,還可以使用 Filter 來攔截所有請(qǐng)求,并直接在 Filter 內(nèi)實(shí)現(xiàn)請(qǐng)求轉(zhuǎn)發(fā)和處理。使用 Filter 的一個(gè)好處是如果 URL 沒有被任何 Controller 的映射方法匹配到,則可以簡(jiǎn)單地調(diào)用 FilterChain.doFilter() 將 HTTP 請(qǐng)求傳遞給下一個(gè) Filter,這樣,我們就不必自己處理靜態(tài)文件,而由 Web 服務(wù)器提供的默認(rèn) Servlet 處理,效率更高。和 DispatcherServlet 類似,我們編寫一個(gè) DispatcherFilter 作為前置處理器,負(fù)責(zé)轉(zhuǎn)發(fā)請(qǐng)求,代碼見清單 24。
清單 24. 定義 DispatcherFilter
如果用 DispatcherFilter 代替 DispatcherServlet,則我們需要過濾“/*”,在 web.xml 中添加聲明如清單 25 所示。
清單 25. 聲明 DispatcherFilter
訪問 Request 和 Response 對(duì)象
如何在 @Mapping 方法中訪問 Servlet 對(duì)象?如 HttpServletRequest,HttpServletResponse,HttpSession 和 ServletContext。ThreadLocal 是一個(gè)最簡(jiǎn)單有效的解決方案。我們編寫一個(gè) ActionContext,通過 ThreadLocal 來封裝對(duì) Request 等對(duì)象的訪問。代碼見清單 26。
清單 26. 定義 ActionContext
在 Dispatcher 的 handleExecution() 方法中,初始化 ActionContext,并在 finally 中移除所有已綁定變量,代碼見清單 27。
清單 27. 初始化 ActionContext
這樣,在 @Mapping 方法內(nèi)部,可以隨時(shí)獲得需要的 Request、Response、 Session 和 ServletContext 對(duì)象。
處理文件上傳
Servlet API 本身并沒有提供對(duì)文件上傳的支持,要處理文件上傳,我們需要使用 Commons FileUpload 之類的第三方擴(kuò)展包??紤]到 Commons FileUpload 是使用最廣泛的文件上傳包,我們希望能集成 Commons FileUpload,但是,不要暴露 Commons FileUpload 的任何 API 給 MVC 的客戶端,客戶端應(yīng)該可以直接從一個(gè)普通的 HttpServletRequest 對(duì)象中獲取上傳文件。
要讓 MVC 客戶端直接使用 HttpServletRequest,我們可以用自定義的 MultipartHttpServletRequest 替換原始的 HttpServletRequest,這樣,客戶端代碼可以通過 instanceof 判斷是否是一個(gè) Multipart 格式的 Request,如果是,就強(qiáng)制轉(zhuǎn)型為 MultipartHttpServletRequest,然后,獲取上傳的文件流。
核心思想是從 HttpServletRequestWrapper 派生 MultipartHttpServletRequest,這樣,MultipartHttpServletRequest 具有 HttpServletRequest 接口。MultipartHttpServletRequest 的定義如清單 28 所示。
清單 28. 定義 MultipartHttpServletRequest
...解析Multipart ...
對(duì)于正常的 Field 參數(shù),保存在成員變量 Map
清單 29. 覆寫 getParameter
為了簡(jiǎn)化配置,在 Web 應(yīng)用程序啟動(dòng)的時(shí)候,自動(dòng)檢測(cè)當(dāng)前 ClassPath 下是否有 Commons FileUpload,如果存在,文件上傳功能就自動(dòng)開啟,如果不存在,文件上傳功能就不可用,這樣,客戶端只需要簡(jiǎn)單地把 Commons FileUpload 的 jar 包放入 /WEB-INF/lib/,不需任何配置就可以直接使用。核心代碼見清單 30。
清單 30. 檢測(cè) Commons FileUpload
小結(jié)
#t#要從頭設(shè)計(jì)并實(shí)現(xiàn)一個(gè) MVC 框架其實(shí)并不困難,設(shè)計(jì) WebWind 的目標(biāo)是改善 Web 應(yīng)用程序的 URL 結(jié)構(gòu),并通過自動(dòng)提取和映射 URL 中的參數(shù),簡(jiǎn)化控制器的編寫。WebWind 適合那些從頭構(gòu)造的新的互聯(lián)網(wǎng)應(yīng)用,以便天生支持 REST 風(fēng)格的 URL。但是,它不適合改造已有的企業(yè)應(yīng)用程序,企業(yè)應(yīng)用的頁面不需要搜索引擎的索引,其用戶對(duì) URL 地址的友好程度通常也并不關(guān)心。
當(dāng)前題目:設(shè)計(jì)REST風(fēng)格的Java MVC框架:WebWind
網(wǎng)頁網(wǎng)址:http://www.5511xx.com/article/cooehee.html


咨詢
建站咨詢
