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=2129650。
有經驗的 Java Web 開發人員會使用 URL 重寫的方式來實現類似的 URL,例如,為前端 Apache 服 務器配置 mod_rewrite 模塊,並依次為每個需要實現 URL 重寫的地址編寫負責 轉換的正則表達式,或者,通過一個自定義的 RewriteFilter,使用 Java Web 服務器提供的 Filter 和請求轉發(Forward)功能實現 URL 重寫,不過,仍需 要為每個地址編寫正則表達式。
既然 URL 重寫如此繁瑣,為何不直接設計一個原生支持 REST 風格的 MVC 框架呢?
要設計並實現這樣一個 MVC 框架並不困難,下面,我們從零開 始,仔細研究如何實現 REST 風格的 URL 映射,並與常見的 IoC 容器如 Spring 框架集成。這個全新的 MVC 框架暫命名為 WebWind。
術語
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 等。
設計目標
和傳統的 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 中的參數 $1、 $2 ……則將作為方法參數傳入。對於一個“/blog/1234/5678”的 URL,對應的 方法將自動獲得參數 userId=1234 和 postId=5678。同時,也無需任何與 URL 映射相關的 XML 配置文件。
使用 $1、$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. 請求處理流程
當掃描到標記有 @Mapping 注解的方法時,需要首先檢查 URL 與方法參數是 否匹配,UrlMatcher 用於將 @Mapping 中包含 $1、$2 ……的字符串變為正則 表達式,進行預編譯,並檢查參數個數是否符合方法參數,代碼見清單 5。
清單 5. 定義 UrlMatcher
final class UrlMatcher {
final String url;
int[] orders;
Pattern pattern;
public UrlMatcher(String url) {
...
}
}
將 @Mapping 中包含 $1、$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 地址的友好程度通常也並不關心。