設計 REST 風格的 MVC 框架

Java 開發者對 MVC 框架必定不陌生,從 Struts 到 WebWork,Java MVC 框架層出不窮。咱們已經習慣了處理 *.do 或 *.action 風格的 URL,爲每個 URL 編寫一個控制器,並繼承一個 Action 或者 Controller 接口。然而,流行的 Web 趨勢是使用更加簡單,對用戶和搜索引擎更加友好的 REST 風格的 URL。例如,來自豆瓣的一本書的連接是 http://www.douban.com/subject/2129650/,而非 http://www.douban.com/subject.do?id=2129650html

有經驗的 Java Web 開發人員會使用 URL 重寫的方式來實現相似的 URL,例如,爲前端 Apache 服務器配置 mod_rewrite 模塊,並依次爲每一個須要實現 URL 重寫的地址編寫負責轉換的正則表達式,或者,經過一個自定義的 RewriteFilter,使用 Java Web 服務器提供的 Filter 和請求轉發(Forward)功能實現 URL 重寫,不過,仍須要爲每一個地址編寫正則表達式。前端

既然 URL 重寫如此繁瑣,爲什麼不直接設計一個原生支持 REST 風格的 MVC 框架呢?java

要設計並實現這樣一個 MVC 框架並不困難,下面,咱們從零開始,仔細研究如何實現 REST 風格的 URL 映射,並與常見的 IoC 容器如 Spring 框架集成。這個全新的 MVC 框架暫命名爲 WebWind。web

術語

MVC:Model-View-Controller,是一種常見的 UI 架構模式,經過分離 Model(模型)、View(視圖)和 Controller(控制器),能夠更容易實現易於擴展的 UI。在 Web 應用程序中,Model 指後臺返回的數據;View 指須要渲染的頁面,一般是 JSP 或者其餘模板頁面,渲染後的結果一般是 HTML;Controller 指 Web 開發人員編寫的處理不一樣 URL 的控制器(在 Struts 中被稱之爲 Action),而 MVC 框架自己還有一個前置控制器,用於接收全部的 URL 請求,並根據 URL 地址分發到 Web 開發人員編寫的 Controller 中。正則表達式

IoC:Invertion-of-Control,控制反轉,是目前流行的管理全部組件生命週期和複雜依賴關係的容器,例如 Spring 容器。數據庫

Template:模板,經過渲染,模板中的變量將被 Model 的實際數據所替換,而後,生成的內容便是用戶在瀏覽器中看到的 HTML。模板也能實現判斷、循環等簡單邏輯。本質上,JSP 頁面也是一種模板。此外,還有許多第三方模板引擎,如 Velocity,FreeMarker 等。express

回頁首apache

設計目標

和傳統的 Struts 等 MVC 框架徹底不一樣,爲了支持 REST 風格的 URL,咱們並不把一個 URL 映射到一個 Controller 類(或者 Struts 的 Action),而是直接把一個 URL 映射到一個方法,這樣,Web 開發人員就能夠將多個功能相似的方法放到一個 Controller 中,而且,Controller 沒有強制要求必須實現某個接口。一個 Controller 一般擁有多個方法,每一個方法負責處理一個 URL。例如,一個管理 Blog 的 Controller 定義起來就像清單 1 所示。瀏覽器

清單 1. 管理 Blog 的 Controller 定義
public class Blog { 
    @Mapping("/create/$1") 
    Public void create(int userId) { ... } 

    @Mapping("/display/$1/$2") 
    Public void display(int userId, int postId) { ... } 

    @Mapping("/edit/$1/$2") 
    Public void edit(int userId, int postId) { ... } 

    @Mapping("/delete/$1/$2") 
    Public String delete(int userId, int postId) { ... } 
}

@Mapping() 註解指示了這是一個處理 URL 映射的方法,URL 中的參數 $一、$2 ……則將做爲方法參數傳入。對於一個「/blog/1234/5678」的 URL,對應的方法將自動得到參數 userId=1234 和 postId=5678。同時,也無需任何與 URL 映射相關的 XML 配置文件。緩存

使用 $一、$2 ……來定義 URL 中的可變參數要比正則表達式更簡單,咱們須要在 MVC 框架內部將其轉化爲正則表達式,以便匹配 URL。

此外,對於方法返回值,也未做強制要求。

回頁首

集成 IoC

當接收到來自瀏覽器的請求,並匹配到合適的 URL 時,應該轉發給某個 Controller 實例的某個標記有 @Mapping 的方法,這須要持有全部 Controller 的實例。不過,讓一個 MVC 框架去管理這些組件並非一個好的設計,這些組件能夠很容易地被 IoC 容器管理,MVC 框架須要作的僅僅是向 IoC 容器請求並獲取這些組件的實例。

爲了解耦一種特定的 IoC 容器,咱們經過 ContainerFactory 來獲取全部 Controller 組件的實例,如清單 2 所示。

清單 2. 定義 ContainerFactory
public interface ContainerFactory { 

    void init(Config config); 

    List<Object> findAllBeans(); 

    void destroy(); 
}

其中,關鍵方法 findAllBeans() 返回 IoC 容器管理的全部 Bean,而後,掃描每個 Bean 的全部 public 方法,並引用那些標記有 @Mapping 的方法實例。

咱們設計目標是支持 Spring 和 Guice 這兩種容器,對於 Spring 容器,能夠經過 ApplicationContext 得到全部的 Bean 引用,代碼見清單 3。

清單 3. 定義 SpringContainerFactory
public class SpringContainerFactory implements ContainerFactory { 
    private ApplicationContext appContext; 

    public List<Object> findAllBeans() { 
        String[] beanNames = appContext.getBeanDefinitionNames(); 
        List<Object> beans = new ArrayList<Object>(beanNames.length); 
        for (int i=0; i<beanNames.length; i++) { 
            beans.add(appContext.getBean(beanNames[i])); 
        } 
        return beans; 
    } 
    ... 
}

對於 Guice 容器,經過 Injector 實例能夠返回全部綁定對象的實例,代碼見清單 4。

清單 4. 定義 GuiceContainerFactory
public class GuiceContainerFactory implements ContainerFactory { 
    private Injector injector; 

    public List<Object> findAllBeans() { 
        Map<Key<?>, Binding<?>> map = injector.getBindings(); 
        Set<Key<?>> keys = map.keySet(); 
        List<Object> list = new ArrayList<Object>(keys.size()); 
        for (Key<?> key : keys) { 
            Object bean = injector.getInstance(key); 
            list.add(bean); 
        } 
        return list; 
    } 
    ... 
}

相似的,經過擴展 ContainerFactory,就能夠支持更多的 IoC 容器,如 PicoContainer。

出於效率的考慮,咱們緩存全部來自 IoC 的 Controller 實例,不管其在 IoC 中配置爲 Singleton 仍是 Prototype 類型。固然,也能夠修改代碼,每次都從 IoC 容器中從新請求實例。

回頁首

設計請求轉發

和 Struts 等常見 MVC 框架同樣,咱們也須要實現一個前置控制器,一般命名爲 DispatcherServlet,用於接收全部的請求,並做出合適的轉發。在 Servlet 規範中,有如下幾種常見的 URL 匹配模式:

  • /abc:精確匹配,一般用於映射自定義的 Servlet;

  • *.do:後綴模式匹配,常見的 MVC 框架都採用這種模式;

  • /app/*:前綴模式匹配,這要求 URL 必須以固定前綴開頭;

  • /:匹配默認的 Servlet,當一個 URL 沒有匹配到任何 Servlet 時,就匹配默認的 Servlet。一個 Web 應用程序若是沒有映射默認的 Servlet,Web 服務器會自動爲 Web 應用程序添加一個默認的 Servlet。

REST 風格的 URL 通常不含後綴,咱們只能將 DispatcherServlet 映射到「/」,使之變爲一個默認的 Servlet,這樣,就能夠對任意的 URL 進行處理。

因爲沒法像 Struts 等傳統的 MVC 框架根據後綴直接將一個 URL 映射到一個 Controller,咱們必須依次匹配每一個有能力處理 HTTP 請求的 @Mapping 方法。完整的 HTTP 請求處理流程如圖 1 所示。

圖 1. 請求處理流程

圖 1. 請求處理流程

當掃描到標記有 @Mapping 註解的方法時,須要首先檢查 URL 與方法參數是否匹配,UrlMatcher 用於將 @Mapping 中包含 $一、$2 ……的字符串變爲正則表達式,進行預編譯,並檢查參數個數是否符合方法參數,代碼見清單 5。

清單 5. 定義 UrlMatcher
final class UrlMatcher { 
    final String url; 
    int[] orders; 
    Pattern pattern; 

    public UrlMatcher(String url) { 
        ... 
    } 
}

@Mapping 中包含 $一、$2 ……的字符串變爲正則表達式的轉換規則是,依次將每一個 $n 替換爲 ([^\\/]*),其他部分做精確匹配。例如,「/blog/$1/$2」變化後的正則表達式爲:

 ^\\/blog\\/([^\\/]*)\\/([^\\/]*)$

請注意,Java 字符串須要兩個連續的「\\」表示正則表達式中的轉義字符「\」。將「/」排除在變量匹配以外能夠避免不少歧義。

調用一個實例方法則由 Action 類表示,它持有類實例、方法引用和方法參數類型,代碼見清單 6。

清單 6. 定義 Action
class Action { 
    public final Object instance; 
    public final Method method; 
    public final Class<?>[] arguments; 

    public Action(Object instance, Method method) { 
        this.instance = instance; 
        this.method = method; 
        this.arguments = method.getParameterTypes(); 
    } 
}

負責請求轉發的 Dispatcher 經過關聯 UrlMatcher 與 Action,就能夠匹配到合適的 URL,並轉發給相應的 Action,代碼見清單 7。

清單 7. 定義 Dispatcher
class Dispatcher  { 
    private UrlMatcher[] urlMatchers; 
    private Map<UrlMatcher, Action> urlMap = new HashMap<UrlMatcher, Action>(); 
    .... 
}

當 Dispatcher 接收到一個 URL 請求時,遍歷全部的 UrlMatcher,找到第一個匹配 URL 的 UrlMatcher,並從 URL 中提取方法參數,代碼見清單 8。

清單 8. 匹配並從 URL 中提取參數
final class UrlMatcher { 
    ... 

    /** 
     * 根據正則表達式匹配 URL,若匹配成功,返回從 URL 中提取的參數,
     * 若匹配失敗,返回 null 
     */ 
    public String[] getMatchedParameters(String url) { 
        Matcher m = pattern.matcher(url); 
        if (!m.matches()) 
            return null; 
        if (orders.length==0) 
            return EMPTY_STRINGS; 
        String[] params = new String[orders.length]; 
        for (int i=0; i<orders.length; i++) { 
            params[orders[i]] = m.group(i+1); 
        } 
        return params; 
    } 
}

根據 URL 找到匹配的 Action 後,就能夠構造一個 Execution 對象,並根據方法簽名將 URL 中的 String 轉換爲合適的方法參數類型,準備好所有參數,代碼見清單 9。

清單 9. 構造 Exectuion
class Execution { 
    public final HttpServletRequest request; 
    public final HttpServletResponse response; 
    private final Action action; 
    private final Object[] args; 
    ... 

    public Object execute() throws Exception { 
        try { 
            return action.method.invoke(action.instance, args); 
        } 
        catch (InvocationTargetException e) { 
            Throwable t = e.getCause(); 
            if (t!=null && t instanceof Exception) 
                throw (Exception) t; 
            throw e; 
        } 
    } 
}

調用 execute() 方法就能夠執行目標方法,並返回一個結果。請注意,當經過反射調用方法失敗時,咱們經過查找 InvocationTargetException 的根異常並將其拋出,這樣,客戶端就能捕獲正確的原始異常。

爲了最大限度地增長靈活性,咱們並不強制要求 URL 的處理方法返回某一種類型。咱們設計支持如下返回值:

  • String:當返回一個 String 時,自動將其做爲 HTML 寫入 HttpServletResponse;

  • void:當返回 void 時,不作任何操做;

  • Renderer:當返回 Renderer 對象時,將調用 Renderer 對象的 render 方法渲染 HTML 頁面。

最後須要考慮的是,因爲咱們將 DispatcherServlet 映射爲「/」,即默認的 Servlet,則全部的未匹配成功的 URL 都將由 DispatcherServlet 處理,包括全部靜態文件,所以,當未匹配到任何 Controller 的 @Mapping 方法後,DispatcherServlet 將試圖按 URL 查找對應的靜態文件,咱們用 StaticFileHandler 封裝,主要代碼見清單 10。

清單 10. 處理靜態文件
class StaticFileHandler { 
    ... 
    public void handle(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException { 
        String url = request.getRequestURI(); 
        String path = request.getServletPath(); 
        url = url.substring(path.length()); 
        if (url.toUpperCase().startsWith("/WEB-INF/")) { 
            response.sendError(HttpServletResponse.SC_NOT_FOUND); 
            return; 
        } 
        int n = url.indexOf('?'); 
        if (n!=(-1)) 
            url = url.substring(0, n); 
        n = url.indexOf('#'); 
        if (n!=(-1)) 
            url = url.substring(0, n); 
        File f = new File(servletContext.getRealPath(url)); 
        if (! f.isFile()) { 
            response.sendError(HttpServletResponse.SC_NOT_FOUND); 
            return; 
        } 
        long ifModifiedSince = request.getDateHeader("If-Modified-Since"); 
        long lastModified = f.lastModified(); 
        if (ifModifiedSince!=(-1) && ifModifiedSince>=lastModified) { 
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 
            return; 
        } 
        response.setDateHeader("Last-Modified", lastModified); 
        response.setContentLength((int)f.length()); 
        response.setContentType(getMimeType(f)); 
        sendFile(f, response.getOutputStream()); 
    } 
}

處理靜態文件時要過濾 /WEB-INF/ 目錄,不然將形成安全漏洞。

回頁首

集成模板引擎

做爲示例,返回一個「<h1>Hello, world!</h1>」做爲 HTML 頁面很是容易。然而,實際應用的頁面一般是極其複雜的,須要一個模板引擎來渲染出 HTML。能夠把 JSP 看做是一種模板,只要不在 JSP 頁面中編寫複雜的 Java 代碼。咱們的設計目標是實現對 JSP 和 Velocity 這兩種模板的支持。

和集成 IoC 框架相似,咱們須要解耦 MVC 與模板系統,所以,TemplateFactory 用於初始化模板引擎,並返回 Template 模板對象。TemplateFactory 定義見清單 11。

清單 11. 定義 TemplateFactory
public abstract class TemplateFactory { 
    private static TemplateFactory instance; 
    public static TemplateFactory getTemplateFactory() { 
        return instance; 
    } 

    public abstract Template loadTemplate(String path) throws Exception; 
}

Template 接口則實現真正的渲染任務。定義見清單 12。

清單 12. 定義 Template
public interface Template { 
    void render(HttpServletRequest request, HttpServletResponse response, 
        Map<String, Object> model) throws Exception; 
}

以 JSP 爲例,實現 JspTemplateFactory 很是容易。代碼見清單 13。

清單 13. 定義 JspTemplateFactory
public class JspTemplateFactory extends TemplateFactory { 
    private Log log = LogFactory.getLog(getClass()); 

    public Template loadTemplate(String path) throws Exception { 
        if (log.isDebugEnabled()) 
            log.debug("Load JSP template '" + path + "'."); 
        return new JspTemplate(path); 
    } 

    public void init(Config config) { 
        log.info("JspTemplateFactory init ok."); 
    } 
}

JspTemplate 用於渲染頁面,只須要傳入 JSP 的路徑,將 Model 綁定到 HttpServletRequest,就能夠調用 Servlet 規範的 forward 方法將請求轉發給指定的 JSP 頁面並渲染。代碼見清單 14。

清單 14. 定義 JspTemplate
public class JspTemplate implements Template { 
    private String path; 

    public JspTemplate(String path) { 
        this.path = path; 
    } 

    public void render(HttpServletRequest request, HttpServletResponse response, 
            Map<String, Object> model) throws Exception { 
        Set<String> keys = model.keySet(); 
        for (String key : keys) { 
            request.setAttribute(key, model.get(key)); 
        } 
        request.getRequestDispatcher(path).forward(request, response); 
    } 
}

另外一種比 JSP 更加簡單且靈活的模板引擎是 Velocity,它使用更簡潔的語法來渲染頁面,對頁面設計人員更加友好,而且徹底阻止了開發人員試圖在頁面中編寫 Java 代碼的可能性。使用 Velocity 編寫的頁面示例如清單 15 所示。

清單 15. Velocity 模板頁面
<html> 
    <head><title>${title}</title></head> 
    <body><h1>Hello, ${name}!</body> 
</html>

經過 VelocityTemplateFactory 和 VelocityTemplate 就能夠實現對 Velocity 的集成。不過,從 Web 開發人員看來,並不須要知道具體使用的模板,客戶端僅須要提供模板路徑和一個由 Map<String, Object> 組成的 Model,而後返回一個 TemplateRenderer 對象。代碼如清單 16 所示。

清單 16. 定義 TemplateRenderer
public class TemplateRenderer extends Renderer { 
    private String path; 
    private Map<String, Object> model; 

    public TemplateRenderer(String path, Map<String, Object> model) { 
        this.path = path; 
        this.model = model; 
    } 

    @Override 
    public void render(ServletContext context, HttpServletRequest request, 
            HttpServletResponse response) throws Exception { 
        TemplateFactory.getTemplateFactory() 
                .loadTemplate(path) 
                .render(request, response, model); 
    } 
}

TemplateRenderer 經過簡單地調用 render 方法就實現了頁面渲染。爲了指定 Jsp 或 Velocity,須要在 web.xml 中配置 DispatcherServlet 的初始參數。配置示例請參考清單 17。

清單 17. 配置 Velocity 做爲模板引擎
<servlet> 
    <servlet-name>dispatcher</servlet-name> 
    <servlet-class>org.expressme.webwind.DispatcherServlet</servlet-class> 
    <init-param> 
        <param-name>template</param-name> 
        <param-value>Velocity</param-value> 
    </init-param> 
</servlet>

若是沒有該缺省參數,那就使用默認的 Jsp。

相似的,經過擴展 TemplateFactory 和 Template,就能夠添加更多的模板支持,例如 FreeMarker。

回頁首

設計攔截器

攔截器和 Servlet 規範中的 Filter 很是相似,不過 Filter 的做用範圍是整個 HttpServletRequest 的處理過程,而攔截器僅做用於 Controller,不涉及到 View 的渲染,在大多數狀況下,使用攔截器比 Filter 速度要快,尤爲是綁定數據庫事務時,攔截器能縮短數據庫事務開啓的時間。

攔截器接口 Interceptor 定義如清單 18 所示。

清單 18. 定義 Interceptor
public interface Interceptor { 
    void intercept(Execution execution, InterceptorChain chain) throws Exception; 
}

和 Filter 相似,InterceptorChain 表明攔截器鏈。InterceptorChain 定義如清單 19 所示。

清單 19. 定義 InterceptorChain
public interface InterceptorChain { 
    void doInterceptor(Execution execution) throws Exception; 
}

實現 InterceptorChain 要比實現 FilterChain 簡單,由於 Filter 須要處理 Request、Forward、Include 和 Error 這 4 種請求轉發的狀況,而 Interceptor 僅攔截 Request。當 MVC 框架處理一個請求時,先初始化一個攔截器鏈,而後,依次調用鏈上的每一個攔截器。請參考清單 20 所示的代碼。

清單 20. 實現 InterceptorChain 接口
class InterceptorChainImpl implements InterceptorChain { 
    private final Interceptor[] interceptors; 
    private int index = 0; 
    private Object result = null; 

    InterceptorChainImpl(Interceptor[] interceptors) { 
        this.interceptors = interceptors; 
    } 

    Object getResult() { 
        return result; 
    } 

    public void doInterceptor(Execution execution) throws Exception { 
        if(index==interceptors.length) 
            result = execution.execute(); 
        else { 
            // must update index first, otherwise will cause stack overflow: 
            index++; 
            interceptors[index-1].intercept(execution, this); 
        } 
    } 
}

成員變量 index 表示當前鏈上的第 N 個攔截器,當最後一個攔截器被調用後,InterceptorChain 才真正調用 Execution 對象的 execute() 方法,並保存其返回結果,整個請求處理過程結束,進入渲染階段。清單 21 演示瞭如何調用攔截器鏈的代碼。

清單 21. 調用攔截器鏈
class Dispatcher  { 
    ... 
    private Interceptor[] interceptors; 
    void handleExecution(Execution execution, HttpServletRequest request, 
        HttpServletResponse response) throws ServletException, IOException { 
        InterceptorChainImpl chains = new InterceptorChainImpl(interceptors); 
        chains.doInterceptor(execution); 
        handleResult(request, response, chains.getResult()); 
    } 
}

當 Controller 方法被調用完畢後,handleResult() 方法用於處理執行結果。

回頁首

渲染

因爲咱們沒有強制 HTTP 處理方法的返回類型,所以,handleResult() 方法針對不一樣的返回值將作不一樣的處理。代碼如清單 22 所示。

清單 22. 處理返回值
class Dispatcher  { 
    ... 
    void handleResult(HttpServletRequest request, HttpServletResponse response, 
            Object result) throws Exception { 
        if (result==null) 
            return; 
        if (result instanceof Renderer) { 
            Renderer r = (Renderer) result; 
            r.render(this.servletContext, request, response); 
            return; 
        } 
        if (result instanceof String) { 
            String s = (String) result; 
            if (s.startsWith("redirect:")) { 
                response.sendRedirect(s.substring(9)); 
                return; 
            } 
            new TextRenderer(s).render(servletContext, request, response); 
            return; 
        } 
        throw new ServletException("Cannot handle result with type '"
                + result.getClass().getName() + "'."); 
    } 
}

若是返回 null,則認爲 HTTP 請求已處理完成,不作任何處理;若是返回 Renderer,則調用 Renderer 對象的 render() 方法渲染視圖;若是返回 String,則根據前綴是否有「redirect:」判斷是重定向仍是做爲 HTML 返回給瀏覽器。這樣,客戶端能夠沒必要訪問 HttpServletResponse 對象就能夠很是方便地實現重定向。代碼如清單 23 所示。

清單 23. 重定向
@Mapping("/register") 
String register() { 
    ... 
    if (success) 
        return "redirect:/reg/success"; 
    return "redirect:/reg/failed"; 
}

擴展 Renderer 還能夠處理更多的格式,例如,向瀏覽器返回 JavaScript 代碼等。

回頁首

擴展

使用 Filter 轉發

對於請求轉發,除了使用 DispatcherServlet 外,還可使用 Filter 來攔截全部請求,並直接在 Filter 內實現請求轉發和處理。使用 Filter 的一個好處是若是 URL 沒有被任何 Controller 的映射方法匹配到,則能夠簡單地調用 FilterChain.doFilter() 將 HTTP 請求傳遞給下一個 Filter,這樣,咱們就沒必要本身處理靜態文件,而由 Web 服務器提供的默認 Servlet 處理,效率更高。和 DispatcherServlet 相似,咱們編寫一個 DispatcherFilter 做爲前置處理器,負責轉發請求,代碼見清單 24。

清單 24. 定義 DispatcherFilter
public class DispatcherFilter implements Filter { 
    ... 
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 
    throws IOException, ServletException { 
        HttpServletRequest httpReq = (HttpServletRequest) req; 
        HttpServletResponse httpResp = (HttpServletResponse) resp; 
        String method = httpReq.getMethod(); 
        if ("GET".equals(method) || "POST".equals(method)) { 
            if (!dispatcher.service(httpReq, httpResp)) 
                chain.doFilter(req, resp); 
            return; 
        } 
        httpResp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 
    } 
}

若是用 DispatcherFilter 代替 DispatcherServlet,則咱們須要過濾「/*」,在 web.xml 中添加聲明如清單 25 所示。

清單 25. 聲明 DispatcherFilter
<filter> 
    <filter-name>dispatcher</servlet-name> 
    <filter-class>org.expressme.webwind.DispatcherFilter</servlet-class> 
</filter> 
<filter-mapping> 
    <filter-name>dispatcher</servlet-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping>

訪問 Request 和 Response 對象

如何在 @Mapping 方法中訪問 Servlet 對象?如 HttpServletRequest,HttpServletResponse,HttpSession 和 ServletContext。ThreadLocal 是一個最簡單有效的解決方案。咱們編寫一個 ActionContext,經過 ThreadLocal 來封裝對 Request 等對象的訪問。代碼見清單 26。

清單 26. 定義 ActionContext
public final class ActionContext { 
    private static final ThreadLocal<ActionContext> actionContextThreadLocal 
            = new ThreadLocal<ActionContext>(); 

    private ServletContext context; 
    private HttpServletRequest request; 
    private HttpServletResponse response; 

    public ServletContext getServletContext() { 
        return context; 
    } 

    public HttpServletRequest getHttpServletRequest() { 
        return request; 
    } 

    public HttpServletResponse getHttpServletResponse() { 
        return response; 
    } 

    public HttpSession getHttpSession() { 
        return request.getSession(); 
    } 

    public static ActionContext getActionContext() { 
        return actionContextThreadLocal.get(); 
    } 

    static void setActionContext(ServletContext context, 
            HttpServletRequest request, HttpServletResponse response) { 
        ActionContext ctx = new ActionContext(); 
        ctx.context = context; 
        ctx.request = request; 
        ctx.response = response; 
        actionContextThreadLocal.set(ctx); 
    } 

    static void removeActionContext() { 
        actionContextThreadLocal.remove(); 
    } 
}

在 Dispatcher 的 handleExecution() 方法中,初始化 ActionContext,並在 finally 中移除全部已綁定變量,代碼見清單 27。

清單 27. 初始化 ActionContext
class Dispatcher { 
    ... 
    void handleExecution(Execution execution, HttpServletRequest request, 
    HttpServletResponse response) throws ServletException, IOException { 
        ActionContext.setActionContext(servletContext, request, response); 
        try { 
            InterceptorChainImpl chains = new InterceptorChainImpl(interceptors); 
            chains.doInterceptor(execution); 
            handleResult(request, response, chains.getResult()); 
        } 
        catch (Exception e) { 
            handleException(request, response, e); 
        } 
        finally { 
            ActionContext.removeActionContext(); 
        } 
    } 
}

這樣,在 @Mapping 方法內部,能夠隨時得到須要的 Request、Response、 Session 和 ServletContext 對象。

處理文件上傳

Servlet API 自己並無提供對文件上傳的支持,要處理文件上傳,咱們須要使用 Commons FileUpload 之類的第三方擴展包。考慮到 Commons FileUpload 是使用最普遍的文件上傳包,咱們但願能集成 Commons FileUpload,可是,不要暴露 Commons FileUpload 的任何 API 給 MVC 的客戶端,客戶端應該能夠直接從一個普通的 HttpServletRequest 對象中獲取上傳文件。

要讓 MVC 客戶端直接使用 HttpServletRequest,咱們能夠用自定義的 MultipartHttpServletRequest 替換原始的 HttpServletRequest,這樣,客戶端代碼能夠經過 instanceof 判斷是不是一個 Multipart 格式的 Request,若是是,就強制轉型爲 MultipartHttpServletRequest,而後,獲取上傳的文件流。

核心思想是從 HttpServletRequestWrapper 派生 MultipartHttpServletRequest,這樣,MultipartHttpServletRequest 具備 HttpServletRequest 接口。MultipartHttpServletRequest 的定義如清單 28 所示。

清單 28. 定義 MultipartHttpServletRequest
public class MultipartHttpServletRequest extends HttpServletRequestWrapper { 
    final HttpServletRequest target; 
    final Map<String, List<FileItemStream>> fileItems; 
    final Map<String, List<String>> formItems; 

    public MultipartHttpServletRequest(HttpServletRequest request, long maxFileSize) 
    throws IOException { 
        super(request); 
        this.target = request; 
        this.fileItems = new HashMap<String, List<FileItemStream>>(); 
        this.formItems = new HashMap<String, List<String>>(); 
        ServletFileUpload upload = new ServletFileUpload(); 
        upload.setFileSizeMax(maxFileSize); 
        try {

...解析Multipart ...

        } 
        catch (FileUploadException e) { 
            throw new IOException(e); 
        } 
    } 

    public InputStream getFileInputStream(String fieldName) throws IOException { 
        List<FileItemStream> list = fileItems.get(fieldName); 
        if (list==null) 
            throw new IOException("No file item with name '" + fieldName + "'."); 
        return list.get(0).openStream(); 
    }; 
}

對於正常的 Field 參數,保存在成員變量 Map<String, List<String>> formItems 中,經過覆寫 getParameter()、getParameters() 等方法,就可讓客戶端把 MultipartHttpServletRequest 也看成一個普通的 Request 來操做,代碼見清單 29。

清單 29. 覆寫 getParameter
public class MultipartHttpServletRequest extends HttpServletRequestWrapper { 
    ... 
    @Override 
    public String getParameter(String name) { 
        List<String> list = formItems.get(name); 
        if (list==null) 
            return null; 
        return list.get(0); 
    } 

    @Override 
    @SuppressWarnings("unchecked") 
    public Map getParameterMap() { 
        Map<String, String[]> map = new HashMap<String, String[]>(); 
        Set<String> keys = formItems.keySet(); 
        for (String key : keys) { 
            List<String> list = formItems.get(key); 
            map.put(key, list.toArray(new String[list.size()])); 
        } 
        return Collections.unmodifiableMap(map); 
    } 

    @Override 
    @SuppressWarnings("unchecked") 
    public Enumeration getParameterNames() { 
        return Collections.enumeration(formItems.keySet()); 
    } 

    @Override 
    public String[] getParameterValues(String name) { 
        List<String> list = formItems.get(name); 
        if (list==null) 
            return null; 
        return list.toArray(new String[list.size()]); 
    } 
}

爲了簡化配置,在 Web 應用程序啓動的時候,自動檢測當前 ClassPath 下是否有 Commons FileUpload,若是存在,文件上傳功能就自動開啓,若是不存在,文件上傳功能就不可用,這樣,客戶端只須要簡單地把 Commons FileUpload 的 jar 包放入 /WEB-INF/lib/,不需任何配置就能夠直接使用。核心代碼見清單 30。

清單 30. 檢測 Commons FileUpload
class Dispatcher { 
    private boolean multipartSupport = false; 
    ... 
    void initAll(Config config) throws Exception { 
        try { 
            Class.forName("org.apache.commons.fileupload.servlet.ServletFileUpload"); 
            this.multipartSupport = true; 
        } 
        catch (ClassNotFoundException e) { 
            log.info("CommonsFileUpload not found."); 
        } 
        ... 
    } 

    void handleExecution(Execution execution, HttpServletRequest request, 
            HttpServletResponse response) throws ServletException, IOException { 
        if (this.multipartSupport) { 
            if (MultipartHttpServletRequest.isMultipartRequest(request)) { 
                request = new MultipartHttpServletRequest(request, maxFileSize); 
            } 
        } 
        ... 
    } 
    ... 
}

回頁首

小結

要從頭設計並實現一個 MVC 框架其實並不困難,設計 WebWind 的目標是改善 Web 應用程序的 URL 結構,並經過自動提取和映射 URL 中的參數,簡化控制器的編寫。WebWind 適合那些從頭構造的新的互聯網應用,以便天生支持 REST 風格的 URL。可是,它不適合改造已有的企業應用程序,企業應用的頁面不須要搜索引擎的索引,其用戶對 URL 地址的友好程度一般也並不關心。

相關文章
相關標籤/搜索