`
iluoxuan
  • 浏览: 577282 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

自己写mvc,参考别人的练习原理

 
阅读更多

这个是ibm上的一篇文章,还有源代码:努力学习,精通原理,山寨一切

设计 REST 风格的 MVC 框架

 

廖 雪峰, 软件工程师, HP

 

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

本文的标签:  mvcrestspringstudywebwind

 

 

发布日期: 2010 年 1 月 04 日 
级别: 中级 
访问情况 : 21709 次浏览 
评论: 0 (查看 | 添加评论 - 登录)

平均分 4 星 共 18 个评分 平均分 (18个评分)
为本文评分

 

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. 请求处理流程
图 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 地址的友好程度通常也并不关心。


参考资料

学习

获得产品和技术

讨论

关于作者

作者照片

廖雪峰,精通 Java/Java EE/Java ME/Android/Python/C#/Visual Basic,对开源框架有深入研究,著有《Spring 2.0 核心技术与最佳实践》一书,创建有开源框架 JOpenID,其官方博客是 http://www.liaoxuefeng.com/ 和 http://michael-liao.appspot.com/。

为本文评分

平均分 4 星 共 18 个评分 平均分 (18个评分)

1 星1 星1 星
2 星2 星2 星
3 星3 星3 星
4 星4 星4 星
5 星5 星5 星

评论

添加评论:

请 登录 或 注册 后发表评论。

注意:评论中不支持 HTML 语法

有新评论时提醒我剩余 1000 字符

 


快来添加第一条评论

标签

Help
使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

更多更少

查看方式 | 列表

分享到:
评论

相关推荐

    韩顺平 mvc 雇员管理系统自己跟着敲的练习代码

    【标题】"韩顺平 MVC 雇员管理系统自己跟着敲的练习代码"涉及的核心知识点是基于PHP的MVC(Model-View-Controller)架构设计模式来开发一个雇员管理系统。MVC是一种广泛用于Web应用开发的设计模式,它将业务逻辑、...

    Mvc 用户登录系统

    本项目是一个基于MVC模式的简单用户登录系统,特别适合初学者用来学习和理解MVC架构的工作原理。 ### MVC模式简介 MVC模式包含以下三个核心组件: 1. **Model(模型)**:这是应用程序的核心,负责处理业务逻辑和...

    Programming Microsoft ASP.NET MVC (2ed, Dino Esposito)

    本书是为***开发人员提供的一个详尽的参考资源,深入探讨了*** MVC框架的原理、内部机制和使用技巧。Dino Esposito是一位在***和移动技术领域公认的专家、培训师和顾问,同时也是多本畅销书的作者,并且是MSDN杂志的...

    NIIT第三模块考试试题及答案 mvc3 J2ME

    6. **实践应用**:理论学习的同时,应结合实际项目练习,例如开发简单的J2ME手机游戏或使用MVC3构建Web应用,这将有助于巩固所学知识,并提升实际操作能力。 总的来说,这份资源对于正在学习J2ME和MVC3的学员来说是...

    lxc.rar_java MVC_java温度计_文件处理

    - "MovieCat" 未明确说明,但可能是另一个项目,可能是一个电影分类或播放应用,进一步练习MVC模式。 - "aSimpleBrowser" 应该是简单浏览器的源代码。 - "aFileDialog" 可能是一个文件对话框的实现,用于选择或打开...

    springMVC初学者

    作为初学者,理解Spring MVC的基本概念和工作原理是非常重要的。以下是对Spring MVC的详细介绍,以及如何使用它来快速搭建Web框架。 1. **Spring MVC架构**: Spring MVC遵循模型-视图-控制器(MVC)设计模式,将...

    动态网站初学者小练习

    动态网站初学者小练习是一个适合新手的项目,旨在帮助学习者掌握基本的动态网站构建技巧。这个练习将静态网页设计与服务器端编程...同时,记得参考在线教程,参与社区讨论,寻求他人的指导,这都是快速成长的有效途径。

    j2ee考试练习题

    通过解答这些练习题,你可以检查自己对J2EE的理解程度,找出知识盲点,并进行针对性的复习。记得不仅要了解每个概念,还要尝试将它们应用到实际项目中,这样才能真正掌握J2EE技术。在学习过程中,如果遇到难题,可以...

    提供给学员的上机练习素材.rar

    以下是一些可能涵盖的知识点,以供参考: 1. **编程语言**:如果压缩包中含有不同类型的代码文件(如.java、.py、.cpp、.js等),这可能是教授学员编程语言的练习。学员可能需要学习如何编写、调试和运行这些代码,...

    学习javaweb练习的代码Java资源

    虽然现代开发更多地转向Spring MVC,但Struts仍然是理解MVC模式和JavaWeb开发历史的重要参考。 9. **Hibernate**: Hibernate是一个对象关系映射(ORM)框架,它简化了Java应用与数据库之间的交互。通过Hibernate,...

    accp8.0.0 SSM框架第二章参考答案

    8. **上机练习参考答案**:这些可能是针对特定练习题目的解答,提供了解决问题的思路和完整代码,帮助学生巩固所学知识并提高实战技能。 通过深入学习和实践这些内容,开发者可以熟练掌握SSM框架,从而更高效地开发...

    交友网站项目练习

    这个项目可能涵盖了从数据库设计到前端交互的多个环节,对于初学者来说,是理解Web应用程序工作原理的一个良好起点。 首先,让我们从数据库层面探讨。`member.sql`文件通常包含SQL脚本,用于创建或填充数据库表。在...

    asp参考手册pdf版

    通过这本ASP参考手册PDF版,你可以深入探究每一个主题,并通过实际项目练习来巩固所学知识。它不仅适合初学者入门,也对经验丰富的开发者提供查阅和解决问题的参考。不论何时,拥有一份详尽的参考手册都是提升技能和...

    传智播客 springmvc 视频 源代码 文档

    5. **视图解析**:探讨视图解析器如InternalResourceViewResolver的工作原理,以及如何使用JSP、FreeMarker或其他模板引擎来展示结果。 6. **数据绑定与验证**:介绍如何自动将请求参数绑定到模型对象,以及使用@...

    springmvc学习指南

    本学习指南旨在帮助开发者深入理解 Spring MVC 的核心概念、工作原理以及如何在实际项目中应用。 一、Spring MVC 架构概览 Spring MVC 提供了一个模型-视图-控制器的设计模式,将业务逻辑、数据处理和用户界面分离...

    JSP基础教程(第2版)实验练习与提高

    **JSP基础教程(第2版)实验练习与提高** 本书是清华大学出版社出版的一本针对JSP初学者的实验指导书,由张跃平和耿祥义两位专家编著。内容涵盖了一系列关于JSP的基础知识和实践操作,旨在帮助读者深入理解和掌握...

    C#试题库及参考答案.rar

    参考答案对于每个题目都提供了标准解答,有助于学习者自我检查,及时纠正错误,深入理解C#语言的原理。通过反复练习和比对答案,学习者可以逐步提升编程技能,为参加C#相关的资格认证考试或者实际项目开发做好准备。...

    java参考书,包括spring,strus等

    在Java参考书中,通常会详细讲解这两个框架的原理、配置、使用方法以及实战案例。Spring部分可能包括了Bean的声明和管理、AOP的实现、数据访问对象(DAO)的创建、Spring Boot的快速开发、Spring Security的安全管理...

    练习项目源码

    由于这些代码是个人的学习笔记,可能并不具备通用性,对其他开发者来说可能不具备直接的应用价值,但仍然可以作为理解Struts2工作原理的一个参考。 【标签】"个人源码"意味着这是一份个人开发的代码,可能没有经过...

    java_技术参考汇集

    Java 技术参考汇集 Java 是一种广泛应用的高级编程语言,以其“一次编写,到处运行”的跨平台特性闻名。在本资源包中,你将找到关于Java技术的多个方面的参考资料,包括正则表达式、Spring框架、Tomcat服务器以及...

Global site tag (gtag.js) - Google Analytics