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

设计 REST 风格的 MVC 框架-2 集成 IoC
集成 IoC当接收到来自浏览器的请求,并匹配到合适的 URL 时,应该转发给某个 Controller 实例的某个标记有 @Mapping 的方法,这需要持有所有 Controller 的实例。不过,让一个 MVC 框架去管理这些组件并不是一个好的设计,这些组件可以很容易地被 IoC 容器管理,MVC 框架需要做的仅仅是向 IoC 容器请求并获取这些组件的实例。
为了解耦一种特定的 IoC 容器,我们通过 ContainerFactory 来获取所有 Controller 组件的实例,如清单 2 所示。
清单 2. 定义 ContainerFactory1
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. 定义 SpringContainerFactory1
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. 定义 GuiceContainerFactory1
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. 定义 UrlMatcher1
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. 定义 Action1
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. 定义 Dispatcher1
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. 构造 Exectuion1
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/ 目录,否则将造成安全漏洞。 |
|
|
|
|
|