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
。html
有经验的 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 所示。浏览器
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。
此外,对于方法返回值,也未做强制要求。
当接收到来自浏览器的请求,并匹配到合适的 URL 时,应该转发给某个 Controller 实例的某个标记有 @Mapping 的方法,这须要持有全部 Controller 的实例。不过,让一个 MVC 框架去管理这些组件并非一个好的设计,这些组件能够很容易地被 IoC 容器管理,MVC 框架须要作的仅仅是向 IoC 容器请求并获取这些组件的实例。
为了解耦一种特定的 IoC 容器,咱们经过 ContainerFactory 来获取全部 Controller 组件的实例,如清单 2 所示。
public interface ContainerFactory { void init(Config config); List<Object> findAllBeans(); void destroy(); }
其中,关键方法 findAllBeans() 返回 IoC 容器管理的全部 Bean,而后,扫描每个 Bean 的全部 public 方法,并引用那些标记有 @Mapping 的方法实例。
咱们设计目标是支持 Spring 和 Guice 这两种容器,对于 Spring 容器,能够经过 ApplicationContext 得到全部的 Bean 引用,代码见清单 3。
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。
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 所示。
当扫描到标记有 @Mapping 注解的方法时,须要首先检查 URL 与方法参数是否匹配,UrlMatcher 用于将 @Mapping 中包含 $一、$2 ……的字符串变为正则表达式,进行预编译,并检查参数个数是否符合方法参数,代码见清单 5。
final class UrlMatcher { final String url; int[] orders; Pattern pattern; public UrlMatcher(String url) { ... } }
将 @Mapping 中包含 $一、$2 ……的字符串变为正则表达式的转换规则是,依次将每一个 $n 替换为 ([^\\/]*),其他部分做精确匹配。例如,“/blog/$1/$2”变化后的正则表达式为:
^\\/blog\\/([^\\/]*)\\/([^\\/]*)$
请注意,Java 字符串须要两个连续的“\\”表示正则表达式中的转义字符“\”。将“/”排除在变量匹配以外能够避免不少歧义。
调用一个实例方法则由 Action 类表示,它持有类实例、方法引用和方法参数类型,代码见清单 6。
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。
class Dispatcher { private UrlMatcher[] urlMatchers; private Map<UrlMatcher, Action> urlMap = new HashMap<UrlMatcher, Action>(); .... }
当 Dispatcher 接收到一个 URL 请求时,遍历全部的 UrlMatcher,找到第一个匹配 URL 的 UrlMatcher,并从 URL 中提取方法参数,代码见清单 8。
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。
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。
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。
public abstract class TemplateFactory { private static TemplateFactory instance; public static TemplateFactory getTemplateFactory() { return instance; } public abstract Template loadTemplate(String path) throws Exception; }
Template 接口则实现真正的渲染任务。定义见清单 12。
public interface Template { void render(HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) throws Exception; }
以 JSP 为例,实现 JspTemplateFactory 很是容易。代码见清单 13。
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。
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 所示。
<html> <head><title>${title}</title></head> <body><h1>Hello, ${name}!</body> </html>
经过 VelocityTemplateFactory 和 VelocityTemplate 就能够实现对 Velocity 的集成。不过,从 Web 开发人员看来,并不须要知道具体使用的模板,客户端仅须要提供模板路径和一个由 Map<String, Object> 组成的 Model,而后返回一个 TemplateRenderer 对象。代码如清单 16 所示。
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。
<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 所示。
public interface Interceptor { void intercept(Execution execution, InterceptorChain chain) throws Exception; }
和 Filter 相似,InterceptorChain 表明拦截器链。InterceptorChain 定义如清单 19 所示。
public interface InterceptorChain { void doInterceptor(Execution execution) throws Exception; }
实现 InterceptorChain 要比实现 FilterChain 简单,由于 Filter 须要处理 Request、Forward、Include 和 Error 这 4 种请求转发的状况,而 Interceptor 仅拦截 Request。当 MVC 框架处理一个请求时,先初始化一个拦截器链,而后,依次调用链上的每一个拦截器。请参考清单 20 所示的代码。
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 演示了如何调用拦截器链的代码。
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 所示。
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 所示。
@Mapping("/register") String register() { ... if (success) return "redirect:/reg/success"; return "redirect:/reg/failed"; }
扩展 Renderer 还能够处理更多的格式,例如,向浏览器返回 JavaScript 代码等。
对于请求转发,除了使用 DispatcherServlet 外,还可使用 Filter 来拦截全部请求,并直接在 Filter 内实现请求转发和处理。使用 Filter 的一个好处是若是 URL 没有被任何 Controller 的映射方法匹配到,则能够简单地调用 FilterChain.doFilter() 将 HTTP 请求传递给下一个 Filter,这样,咱们就没必要本身处理静态文件,而由 Web 服务器提供的默认 Servlet 处理,效率更高。和 DispatcherServlet 相似,咱们编写一个 DispatcherFilter 做为前置处理器,负责转发请求,代码见清单 24。
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 所示。
<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>
如何在 @Mapping 方法中访问 Servlet 对象?如 HttpServletRequest,HttpServletResponse,HttpSession 和 ServletContext。ThreadLocal 是一个最简单有效的解决方案。咱们编写一个 ActionContext,经过 ThreadLocal 来封装对 Request 等对象的访问。代码见清单 26。
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。
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 所示。
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。
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。
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 地址的友好程度一般也并不关心。