设计REST风格的MVC框架[转]

传统的JavaEE MVC框架如Struts等都是基于Action设计的后缀式映射,然而,流行的Web趋势是REST风格的架构。尽管使用Filter或者Apache mod_rewrite能够通过URL重写实现REST风格的URL,为什么不直接设计一个全新的REST风格的MVC框架呢?本文将讲述如何从头设计一个基于REST风格的Java MVC框架,配合Annotation,最大限度地简化Web应用的开发,您甚至编写一行代码就可以实现“Hello, world”。

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所示。

01. // 清单 1. 管理 Blog 的 Controller 定义
02.   
03. public class Blog {
04.     @Mapping("/create/$1")
05.     Public void create(int userId) { ... }
06.   
07.     @Mapping("/display/$1/$2")
08.     Public void display(int userId, int postId) { ... }
09.   
10.     @Mapping("/edit/$1/$2")
11.     Public void edit(int userId, int postId) { ... }
12.   
13.     @Mapping("/delete/$1/$2")
14.     Public String delete(int userId, int postId) { ... }
15. }

@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所示。

01. // 清单 2. 定义 ContainerFactory
02.   
03. public interface ContainerFactory {
04.   
05.     void init(Config config);
06.   
07.     List<Object> findAllBeans();
08.   
09.     void destroy();
10. }

其中,关键方法findAllBeans()返回IoC容器管理的所有Bean,然后,扫描每一个Bean的所有public方法,并引用那些标记有@Mapping的方法实例。

我们设计目标是支持Spring和Guice这两种容器,对于Spring容器,可以通过ApplicationContext获得所有的Bean引用,代码见清单3。

01. // 清单 3. 定义 SpringContainerFactory
02.   
03. public class SpringContainerFactory implements ContainerFactory {
04.     private ApplicationContext appContext;
05.   
06.     public List<Object> findAllBeans() {
07.         String[] beanNames = appContext.getBeanDefinitionNames();
08.         List<Object> beans = new ArrayList<Object>(beanNames.length);
09.         for (int i=0; i<beanNames.length; i++) {
10.             beans.add(appContext.getBean(beanNames[i]));
11.         }
12.         return beans;
13.     }
14.     ...
15. }

对于Guice容器,通过Injector实例可以返回所有绑定对象的实例,代码见清单4。

01. // 清单 4. 定义 GuiceContainerFactory
02.   
03. public class GuiceContainerFactory implements ContainerFactory {
04.     private Injector injector;
05.   
06.     public List<Object> findAllBeans() {
07.         Map<Key<?>, Binding<?>> map = injector.getBindings();
08.         Set<Key<?>> keys = map.keySet();
09.         List<Object> list = new ArrayList<Object>(keys.size());
10.         for (Key<?> key : keys) {
11.             Object bean = injector.getInstance(key);
12.             list.add(bean);
13.         }
14.         return list;
15.     }
16.     ...
17. }

类似的,通过扩展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请求处理流程如下图所示。

当扫描到标记有@Mapping注解的方法时,需要首先检查URL与方法参数是否匹配,UrlMatcher用于将@Mapping中包含$1、$2……的字符串变为正则表达式,进行预编译,并检查参数个数是否符合方法参数,代码见清单5。

01. // 清单 5. 定义 UrlMatcher
02.   
03. final class UrlMatcher {
04.     final String url;
05.     int[] orders;
06.     Pattern pattern;
07.   
08.     public UrlMatcher(String url) {
09.         ...
10.     }
11. }

将@Mapping中包含$1、$2 ……的字符串变为正则表达式的转换规则是,依次将每个$n替换为([^\\/]*),其余部分作精确匹配。例如,“/blog/$1/$2”变化后的正则表达式为: ^\\/blog\\/([^\\/]*)\\/([^\\/]*)$

请注意,Java字符串需要两个连续的“\\”表示正则表达式中的转义字符“\”。将“/”排除在变量匹配之外可以避免很多歧义。

调用一个实例方法则由Action类表示,它持有类实例、方法引用和方法参数类型,代码见清单6。

01. // 清单 6. 定义 Action
02.   
03. class Action {
04.     public final Object instance;
05.     public final Method method;
06.     public final Class[] arguments;
07.   
08.     public Action(Object instance, Method method) {
09.         this.instance = instance;
10.         this.method = method;
11.         this.arguments = method.getParameterTypes();
12.     }
13. }

负责请求转发的Dispatcher通过关联UrlMatcher与Action,就可以匹配到合适的URL,并转发给相应的Action,代码见清单7。

1. // 清单 7. 定义 Dispatcher
2.   
3. class Dispatcher  {
4.     private UrlMatcher[] urlMatchers;
5.     private Map<UrlMatcher, Action> urlMap = new HashMap<UrlMatcher, Action>();
6.     ....
7. }

当Dispatcher接收到一个URL请求时,遍历所有的UrlMatcher,找到第一个匹配URL的UrlMatcher,并从URL中提取方法参数,代码见清单8。

01. // 清单 8. 匹配并从 URL 中提取参数
02.   
03. final class UrlMatcher {
04.     ...
05.   
06.     /**
07.      * 根据正则表达式匹配 URL,若匹配成功,返回从 URL 中提取的参数
08.      * 若匹配失败,返回 null
09.      */
10.     public String[] getMatchedParameters(String url) {
11.         Matcher m = pattern.matcher(url);
12.         if (!m.matches())
13.             return null;
14.         if (orders.length==0)
15.             return EMPTY_STRINGS;
16.         String[] params = new String[orders.length];
17.         for (int i=0; i<orders.length; i++) {
18.             params[orders[i]] = m.group(i+1);
19.         }
20.         return params;
21.     }
22. }

根据URL找到匹配的Action后,就可以构造一个Execution对象,并根据方法签名将URL中的String转换为合适的方法参数类型,准备好全部参数,代码见清单9。

01. // 清单 9. 构造 Exectuion
02.   
03. public class Execution {
04.     public final HttpServletRequest request;
05.     public final HttpServletResponse response;
06.     private final Action action;
07.     private final Object[] args;
08.     ...
09.   
10.     public Object execute() throws Exception {
11.         try {
12.             return action.method.invoke(action.instance, args);
13.         }
14.         catch (InvocationTargetException e) {
15.             Throwable t = e.getCause();
16.             if (t!=null && t instanceof Exception)
17.                 throw (Exception) t;
18.             throw e;
19.         }
20.     }
21. }

调用execute()方法就可以执行目标方法,并返回一个结果。请注意,当通过反射调用方法失败时,我们通过查找InvocationTargetException的根异常并将其抛出,这样,客户端就能捕获正确的原始异常。

为了最大限度地增加灵活性,我们并不强制要求URL的处理方法返回某一种类型。我们设计支持以下返回值:

  • String:当返回一个String时,自动将其作为HTML写入HttpServletResponse;
  • void:当返回void时,不做任何操作;
  • Renderer:当返回Renderer对象时,将调用Renderer对象的render方法渲染HTML页面。

最后需要考虑的是,由于我们将DispatcherServlet映射为“/”,即默认的Servlet,则所有的未匹配成功的URL都将由DispatcherServlet处理,包括所有静态文件,因此,当未匹配到任何Controller的@Mapping方法后,DispatcherServlet将试图按URL查找对应的静态文件,我们用StaticFileHandler封装,主要代码见清单10。

01. // 清单 10. 处理静态文件
02.   
03. class StaticFileHandler {
04.     ...
05.     public void handle(HttpServletRequest request, HttpServletResponse response)
06.     throws ServletException, IOException {
07.         String url = request.getRequestURI();
08.         String path = request.getServletPath();
09.         url = url.substring(path.length());
10.         if (url.toUpperCase().startsWith("/WEB-INF/")) {
11.             response.sendError(HttpServletResponse.SC_NOT_FOUND);
12.             return;
13.         }
14.         int n = url.indexOf('?');
15.         if (n!=(-1))
16.             url = url.substring(0, n);
17.         n = url.indexOf('#');
18.         if (n!=(-1))
19.             url = url.substring(0, n);
20.         File f = new File(servletContext.getRealPath(url));
21.         if (! f.isFile()) {
22.             response.sendError(HttpServletResponse.SC_NOT_FOUND);
23.             return;
24.         }
25.         long ifModifiedSince = request.getDateHeader("If-Modified-Since");
26.         long lastModified = f.lastModified();
27.         if (ifModifiedSince!=(-1) && ifModifiedSince>=lastModified) {
28.             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
29.             return;
30.         }
31.         response.setDateHeader("Last-Modified", lastModified);
32.         response.setContentLength((int)f.length());
33.         response.setContentType(getMimeType(f));
34.         sendFile(f, response.getOutputStream());
35.     }
36. }

处理静态文件时要过滤/WEB-INF/目录,否则将造成安全漏洞。

集成模板引擎

作为示例,返回一个“<h1>Hello, world!</h1>”作为HTML页面非常容易。然而,实际应用的页面通常是极其复杂的,需要一个模板引擎来渲染出HTML。可以把JSP看作是一种模板,只要不在JSP页面中编写复杂的Java代码。我们的设计目标是实现对JSP和Velocity这两种模板的支持。

和集成IoC框架类似,我们需要解耦MVC与模板系统,因此,TemplateFactory用于初始化模板引擎,并返回Template模板对象。TemplateFactory定义见清单11。

01. // 清单 11. 定义 TemplateFactory
02.   
03. public abstract class TemplateFactory {
04.     private static TemplateFactory instance;
05.     public static TemplateFactory getTemplateFactory() {
06.         return instance;
07.     }
08.   
09.     public abstract Template loadTemplate(String path) throws Exception;
10. }

Template接口则实现真正的渲染任务。定义见清单12。

1. // 清单 12. 定义 Template
2.   
3. public interface Template {
4.     void render(HttpServletRequest request, HttpServletResponse response,
5.         Map<String, Object> model) throws Exception;
6. }

以JSP为例,实现JspTemplateFactory非常容易。代码见清单13。

01. // 清单 13. 定义 JspTemplateFactory
02.   
03. public class JspTemplateFactory extends TemplateFactory {
04.     private Log log = LogFactory.getLog(getClass());
05.   
06.     public Template loadTemplate(String path) throws Exception {
07.         if (log.isDebugEnabled())
08.             log.debug("Load JSP template '" + path + "'.");
09.         return new JspTemplate(path);
10.     }
11.   
12.     public void init(Config config) {
13.         log.info("JspTemplateFactory init ok.");
14.     }
15. }

JspTemplate用于渲染页面,只需要传入JSP的路径,将Model绑定到HttpServletRequest,就可以调用Servlet规范的forward方法将请求转发给指定的JSP页面并渲染。代码见清单14。

01. // 清单 14. 定义 JspTemplate
02.   
03. public class JspTemplate implements Template {
04.     private String path;
05.   
06.     public JspTemplate(String path) {
07.         this.path = path;
08.     }
09.   
10.     public void render(HttpServletRequest request, HttpServletResponse response,
11.             Map<String, Object> model) throws Exception {
12.         Set<String> keys = model.keySet();
13.         for (String key : keys) {
14.             request.setAttribute(key, model.get(key));
15.         }
16.         request.getRequestDispatcher(path).forward(request, response);
17.     }
18. }

另一种比JSP更加简单且灵活的模板引擎是Velocity,它使用更简洁的语法来渲染页面,对页面设计人员更加友好,并且完全阻止了开发人员试图在页面中编写Java代码的可能性。使用Velocity编写的页面示例如清单15所示。

1. 清单15. Velocity 模板页面
2.   
3. <html>
4.     <head><title>${title}</title></head>
5.     <body><h1>Hello, ${name}!</body>
6. </html>

通过VelocityTemplateFactory和VelocityTemplate就可以实现对Velocity的集成。不过,从Web开发人员看来,并不需要知道具体使用的模板,客户端仅需要提供模板路径和一个由Map

01. // 清单16. 定义 TemplateRenderer
02.   
03. public class TemplateRenderer extends Renderer {
04.     private String path;
05.     private Map<String, Object> model;
06.   
07.     public TemplateRenderer(String path, Map<String, Object> model) {
08.         this.path = path;
09.         this.model = model;
10.     }
11.   
12.     @Override
13.     public void render(ServletContext context, HttpServletRequest request,
14.             HttpServletResponse response) throws Exception {
15.         TemplateFactory.getTemplateFactory()
16.                 .loadTemplate(path)
17.                 .render(request, response, model);
18.     }
19. }

TemplateRenderer通过简单地调用render方法就实现了页面渲染。为了指定Jsp或Velocity,需要在web.xml中配置DispatcherServlet的初始参数。配置示例请参考清单17。

01. 清单 17. 配置 Velocity 作为模板引擎
02.   
03. <servlet>
04.     <servlet-name>dispatcher</servlet-name>
05.     <servlet-class>org.expressme.webwind.DispatcherServlet</servlet-class>
06.     <init-param>
07.         <param-name>template</param-name>
08.         <param-value>Velocity</param-value>
09.     </init-param>
10. </servlet>

如果没有该缺省参数,那就使用默认的Jsp。

类似的,通过扩展TemplateFactory和Template,就可以添加更多的模板支持,例如FreeMarker。

设计拦截器

拦截器和Servlet规范中的Filter非常类似,不过Filter的作用范围是整个HttpServletRequest的处理过程,而拦截器仅作用于Controller,不涉及到View的渲染,在大多数情况下,使用拦截器比Filter速度要快,尤其是绑定数据库事务时,拦截器能缩短数据库事务开启的时间。

拦截器接口Interceptor定义如清单18所示。

1. // 清单 18. 定义 Interceptor
2.   
3. public interface Interceptor {
4.     void intercept(Execution execution, InterceptorChain chain) throws Exception;
5. }

和Filter类似,InterceptorChain代表拦截器链。InterceptorChain定义如清单19所示。

1. // 清单 19. 定义 InterceptorChain
2.   
3. public interface InterceptorChain {
4.     void doInterceptor(Execution execution) throws Exception;
5. }

实现InterceptorChain要比实现FilterChain简单,因为Filter需要处理Request、Forward、Include和Error这4种请求转发的情况,而Interceptor仅拦截Request。当MVC框架处理一个请求时,先初始化一个拦截器链,然后,依次调用链上的每个拦截器。请参考清单20所示的代码。

01. // 清单 20. 实现 InterceptorChain 接口
02.   
03. class InterceptorChainImpl implements InterceptorChain {
04.     private final Interceptor[] interceptors;
05.     private int index = 0;
06.     private Object result = null;
07.   
08.     InterceptorChainImpl(Interceptor[] interceptors) {
09.         this.interceptors = interceptors;
10.     }
11.   
12.     Object getResult() {
13.         return result;
14.     }
15.   
16.     public void doInterceptor(Execution execution) throws Exception {
17.         if(index==interceptors.length)
18.             result = execution.execute();
19.         else {
20.             // must update index first, otherwise will cause stack overflow:
21.             index++;
22.             interceptors[index-1].intercept(execution, this);
23.         }
24.     }
25. }

成员变量index表示当前链上的第N个拦截器,当最后一个拦截器被调用后,InterceptorChain才真正调用Execution对象的execute()方法,并保存其返回结果,整个请求处理过程结束,进入渲染阶段。清单21演示了如何调用拦截器链的代码。

01. // 清单 21. 调用拦截器链
02.   
03. class Dispatcher  {
04.     ...
05.     private Interceptor[] interceptors;
06.     void handleExecution(Execution execution, HttpServletRequest request,
07.         HttpServletResponse response) throws ServletException, IOException {
08.         InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
09.         chains.doInterceptor(execution);
10.         handleResult(request, response, chains.getResult());
11.     }
12. }

当Controller方法被调用完毕后,handleResult()方法用于处理执行结果。

渲染

由于我们没有强制HTTP处理方法的返回类型,因此,handleResult()方法针对不同的返回值将做不同的处理。代码如清单22所示。

01. // 清单 22. 处理返回值
02.   
03. class Dispatcher  {
04.     ...
05.     void handleResult(HttpServletRequest request, HttpServletResponse response,
06.             Object result) throws Exception {
07.         if (result==null)
08.             return;
09.         if (result instanceof Renderer) {
10.             Renderer r = (Renderer) result;
11.             r.render(this.servletContext, request, response);
12.             return;
13.         }
14.         if (result instanceof String) {
15.             String s = (String) result;
16.             if (s.startsWith("redirect:")) {
17.                 response.sendRedirect(s.substring(9));
18.                 return;
19.             }
20.             new TextRenderer(s).render(servletContext, request, response);
21.             return;
22.         }
23.         throw new ServletException("Cannot handle result with type '"
24.                 + result.getClass().getName() + "'.");
25.     }
26. }

如果返回null,则认为HTTP请求已处理完成,不做任何处理;如果返回Renderer,则调用Renderer对象的render()方法渲染视图;如果返回String,则根据前缀是否有“redirect:”判断是重定向还是作为HTML返回给浏览器。这样,客户端可以不必访问HttpServletResponse对象就可以非常方便地实现重定向。代码如清单23所示。

01. // 清单 23. 重定向
02.   
03. @Mapping("/register")
04. String register() {
05.     ...
06.     if (success)
07.         return "redirect:/reg/success";
08.     return "redirect:/reg/failed";
09. }

扩展Renderer还可以处理更多的格式,例如,向浏览器返回JavaScript代码等。

扩展

以下是对MVC框架核心功能的扩展。

使用Filter转发

对于请求转发,除了使用DispatcherServlet外,还可以使用Filter来拦截所有请求,并直接在Filter内实现请求转发和处理。使用Filter的一个好处是如果URL没有被任何Controller的映射方法匹配到,则可以简单地调用FilterChain.doFilter()将HTTP请求传递给下一个Filter,这样,我们就不必自己处理静态文件,而由Web服务器提供的默认Servlet处理,效率更高。和DispatcherServlet类似,我们编写一个DispatcherFilter作为前置处理器,负责转发请求,代码见清单24。

01. // 清单 24. 定义 DispatcherFilter
02.   
03. public class DispatcherFilter implements Filter {
04.     ...
05.     public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
06.     throws IOException, ServletException {
07.         HttpServletRequest httpReq = (HttpServletRequest) req;
08.         HttpServletResponse httpResp = (HttpServletResponse) resp;
09.         String method = httpReq.getMethod();
10.         if ("GET".equals(method) || "POST".equals(method)) {
11.             if (!dispatcher.service(httpReq, httpResp))
12.                 chain.doFilter(req, resp);
13.             return;
14.         }
15.         httpResp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
16.     }
17. }

如果用DispatcherFilter代替DispatcherServlet,则我们需要过滤“/*”,在web.xml中添加声明如清单25所示。

01. 清单25. 声明 DispatcherFilter
02.   
03. <filter>
04.     <filter-name>dispatcher</servlet-name>
05.     <filter-class>org.expressme.webwind.DispatcherFilter</servlet-class>
06. </filter>
07. <filter-mapping>
08.     <filter-name>dispatcher</servlet-name>
09.     <url-pattern>/*</url-pattern>
10. </filter-mapping>

访问Request和Response对象

如何在@Mapping方法中访问Servlet对象?如HttpServletRequest,HttpServletResponse,HttpSession和ServletContext。ThreadLocal是一个最简单有效的解决方案。我们编写一个ActionContext,通过ThreadLocal来封装对Request等对象的访问。代码见清单26。

01. // 清单 26. 定义 ActionContext
02.   
03. public final class ActionContext {
04.     private static final ThreadLocal<ActionContext> actionContextThreadLocal
05.             = new ThreadLocal<ActionContext>();
06.   
07.     private ServletContext context;
08.     private HttpServletRequest request;
09.     private HttpServletResponse response;
10.   
11.     public ServletContext getServletContext() {
12.         return context;
13.     }
14.   
15.     public HttpServletRequest getHttpServletRequest() {
16.         return request;
17.     }
18.   
19.     public HttpServletResponse getHttpServletResponse() {
20.         return response;
21.     }
22.   
23.     public HttpSession getHttpSession() {
24.         return request.getSession();
25.     }
26.   
27.     public static ActionContext getActionContext() {
28.         return actionContextThreadLocal.get();
29.     }
30.   
31.     static void setActionContext(ServletContext context,
32.             HttpServletRequest request, HttpServletResponse response) {
33.         ActionContext ctx = new ActionContext();
34.         ctx.context = context;
35.         ctx.request = request;
36.         ctx.response = response;
37.         actionContextThreadLocal.set(ctx);
38.     }
39.   
40.     static void removeActionContext() {
41.         actionContextThreadLocal.remove();
42.     }
43. }

在Dispatcher的handleExecution()方法中,初始化ActionContext,并在finally中移除所有已绑定变量,代码见清单27。

01. // 清单 27. 初始化 ActionContext
02.   
03. class Dispatcher {
04.     ...
05.     void handleExecution(Execution execution, HttpServletRequest request,
06.     HttpServletResponse response) throws ServletException, IOException {
07.         ActionContext.setActionContext(servletContext, request, response);
08.         try {
09.             InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
10.             chains.doInterceptor(execution);
11.             handleResult(request, response, chains.getResult());
12.         }
13.         catch (Exception e) {
14.             handleException(request, response, e);
15.         }
16.         finally {
17.             ActionContext.removeActionContext();
18.         }
19.     }
20. }

这样,在@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所示。

01. // 清单 28. 定义 MultipartHttpServletRequest
02.   
03. public class MultipartHttpServletRequest extends HttpServletRequestWrapper {
04.     final HttpServletRequest target;
05.     final Map<String, List<FileItemStream>> fileItems;
06.     final Map<String, List<String>> formItems;
07.   
08.     public MultipartHttpServletRequest(HttpServletRequest request, long maxFileSize)
09.     throws IOException {
10.         super(request);
11.         this.target = request;
12.         this.fileItems = new HashMap<String, List<FileItemStream>>();
13.         this.formItems = new HashMap<String, List<String>>();
14.         ServletFileUpload upload = new ServletFileUpload();
15.         upload.setFileSizeMax(maxFileSize);
16.         try {
17.            ...解析Multipart ...
18.         }
19.         catch (FileUploadException e) {
20.             throw new IOException(e);
21.         }
22.     }
23.   
24.     public InputStream getFileInputStream(String fieldName) throws IOException {
25.         List<FileItemStream> list = fileItems.get(fieldName);
26.         if (list==null)
27.             throw new IOException("No file item with name '" + fieldName + "'.");
28.         return list.get(0).openStream();
29.     };
30. }

对于正常的Field参数,保存在成员变量Map<String, List<String>> formItems中,通过覆写getParameter()、getParameters()等方法,就可以让客户端把MultipartHttpServletRequest也当作一个普通的Request来操作,代码见清单29。

01. // 清单 29. 覆写 getParameter
02.   
03. public class MultipartHttpServletRequest extends HttpServletRequestWrapper {
04.     ...
05.     @Override
06.     public String getParameter(String name) {
07.         List<String> list = formItems.get(name);
08.         if (list==null)
09.             return null;
10.         return list.get(0);
11.     }
12.   
13.     @Override
14.     @SuppressWarnings("unchecked")
15.     public Map getParameterMap() {
16.         Map<String, String[]> map = new HashMap<String, String[]>();
17.         Set<String> keys = formItems.keySet();
18.         for (String key : keys) {
19.             List<String> list = formItems.get(key);
20.             map.put(key, list.toArray(new String[list.size()]));
21.         }
22.         return Collections.unmodifiableMap(map);
23.     }
24.   
25.     @Override
26.     @SuppressWarnings("unchecked")
27.     public Enumeration getParameterNames() {
28.         return Collections.enumeration(formItems.keySet());
29.     }
30.   
31.     @Override
32.     public String[] getParameterValues(String name) {
33.         List<String> list = formItems.get(name);
34.         if (list==null)
35.             return null;
36.         return list.toArray(new String[list.size()]);
37.     }
38. }

为了简化配置,在Web应用程序启动的时候,自动检测当前ClassPath下是否有Commons FileUpload,如果存在,文件上传功能就自动开启,如果不存在,文件上传功能就不可用,这样,客户端只需要简单地把Commons FileUpload的jar包放入/WEB-INF/lib/,不需任何配置就可以直接使用。核心代码见清单30。

01. // 清单 30. 检测 Commons FileUpload
02.                   
03. class Dispatcher {
04.     private boolean multipartSupport = false;
05.     ...
06.     void initAll(Config config) throws Exception {
07.         try {
08.             Class.forName("org.apache.commons.fileupload.servlet.ServletFileUpload");
09.             this.multipartSupport = true;
10.         }
11.         catch (ClassNotFoundException e) {
12.             log.info("CommonsFileUpload not found.");
13.         }
14.         ...
15.     }
16.   
17.     void handleExecution(Execution execution, HttpServletRequest request,
18.             HttpServletResponse response) throws ServletException, IOException {
19.         if (this.multipartSupport) {
20.             if (MultipartHttpServletRequest.isMultipartRequest(request)) {
21.                 request = new MultipartHttpServletRequest(request, maxFileSize);
22.             }
23.         }
24.         ...
25.     }
26.     ...
27. }

小结

要从头设计并实现一个MVC框架其实并不困难,设计WebWind的目标是改善Web应用程序的URL结构,并通过自动提取和映射URL中的参数,简化控制器的编写。WebWind适合那些从头构造的新的互联网应用,以便天生支持REST风格的URL。但是,它不适合改造已有的企业应用程序,企业应用的页面不需要搜索引擎的索引,其用户对URL地址的友好程度通常也并不关心。

参考资料

参考Servlet 2.4规范:http://jcp.org/aboutJava/communityprocess/final/jsr154/index.html

参考Spring框架:http://www.springsource.org/

参考Guice框架:http://code.google.com/p/google-guice/

参考Velocity引擎:http://velocity.apache.org/

参考Commons FileUpload:http://commons.apache.org/fileupload/

下载

下载WebWind:http://code.google.com/p/webwind/downloads/list

下载WebWind SVN源码:http://webwind.googlecode.com/svn/trunk/

转载于:https://www.cnblogs.com/wen12128/archive/2010/01/27/1657835.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值