首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

设计 REST 风格的 MVC 框架-2 集成 IoC

设计 REST 风格的 MVC 框架-2 集成 IoC

集成 IoC当接收到来自浏览器的请求,并匹配到合适的 URL 时,应该转发给某个 Controller 实例的某个标记有 @Mapping 的方法,这需要持有所有 Controller 的实例。不过,让一个 MVC 框架去管理这些组件并不是一个好的设计,这些组件可以很容易地被 IoC 容器管理,MVC 框架需要做的仅仅是向 IoC 容器请求并获取这些组件的实例。
为了解耦一种特定的 IoC 容器,我们通过 ContainerFactory 来获取所有 Controller 组件的实例,如清单 2 所示。
清单 2. 定义 ContainerFactory
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
7
8
9
10
11
12
13
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));
        }
        return beans;
    }
    ...
}




对于 Guice 容器,通过 Injector 实例可以返回所有绑定对象的实例,代码见清单 4。
清单 4. 定义 GuiceContainerFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
1
2
3
4
5
6
7
8
9
final class UrlMatcher {
    final String url;
    int[] orders;
    Pattern pattern;

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




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




请注意,Java 字符串需要两个连续的“\\”表示正则表达式中的转义字符“\”。将“/”排除在变量匹配之外可以避免很多歧义。
调用一个实例方法则由 Action 类表示,它持有类实例、方法引用和方法参数类型,代码见清单 6。
清单 6. 定义 Action
1
2
3
4
5
6
7
8
9
10
11
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
1
2
3
4
5
class Dispatcher  {
    private UrlMatcher[] urlMatchers;
    private Map<UrlMatcher, Action> urlMap = new HashMap<UrlMatcher, Action>();
    ....
}




当 Dispatcher 接收到一个 URL 请求时,遍历所有的 UrlMatcher,找到第一个匹配 URL 的 UrlMatcher,并从 URL 中提取方法参数,代码见清单 8。
清单 8. 匹配并从 URL 中提取参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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] = m.group(i+1);
        }
        return params;
    }
}




根据 URL 找到匹配的 Action 后,就可以构造一个 Execution 对象,并根据方法签名将 URL 中的 String 转换为合适的方法参数类型,准备好全部参数,代码见清单 9。
清单 9. 构造 Exectuion
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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. 处理静态文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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/ 目录,否则将造成安全漏洞。
返回列表