用 300 行代码手写提炼 Spring 核心原理 [3]
系列文章
- 用 300 行代码手写提炼 Spring 核心原理 [1]
- 用 300 行代码手写提炼 Spring 核心原理 [2]
- 用 300 行代码手写提炼 Spring 核心原理 [3]
上文 中我们实现了 mini-spring 的 2.0 版本,基本功能已经实现,但依然还有很多工作要做,例如 HandlerMapping 还不能像 SpringMVC 一样支持正则,url 参数还不支持类型转换,在 3.0 版本中我们将继续优化。
初始化阶段
HandlerMapping
首先改造 HandlerMapping,在真实的 Spring 源码中,HandlerMapping 其实是一个 List 而非 Map,List 中的元素是自定义类型的。现在我们来仿真写一段代码,先定义一个内部类 Handler:
/**
* 内部类,记录Controller中RequestMapping和Method的对应关系
*/
private class Handler {
protected Object controller; // 保存方法对应的实例
protected Method method; // 保存映射的方法
protected Pattern pattern; // url pattern
protected Map<String, Integer> paramIndexMapping; // 保存参数名-参数index的映射
/**
* 构造方法
* @param pattern
* @param controller
* @param method
*/
protected Handler(Pattern pattern, Object controller, Method method) {
this.pattern = pattern;
this.controller = controller;
this.method = method;
paramIndexMapping = new HashMap<>();
putParamIndexMapping(method);
}
private void putParamIndexMapping(Method method) {
// 提取方法中加了注解的参数
Annotation[][] pa = method.getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
for (Annotation a: pa[i]) {
if (a instanceof MyRequestParam) {
String paramName = ((MyRequestParam) a).value().trim();
if (!"".equals(paramName)) {
paramIndexMapping.put(paramName, i);
}
}
}
}
// 提取方法中的request和response参数
Class<?>[] paramsTypes = method.getParameterTypes();
for (int i = 0; i < paramsTypes.length; i++) {
Class<?> type = paramsTypes[i];
if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
paramIndexMapping.put(type.getName(), i);
}
}
}
}
然后优化 HandlerMapping 的结构:
// 保存url -> method的映射关系
private List<Handler> handlerMapping = new ArrayList<>();
修改 initHandlerMapping() 方法:
/**
* 初始化HandlerMapping
*
* 本例中handlerMapping中的内容是:
* /demo/query = org.example.minispring.action.DemoAction.query(HttpServletRequest, HttpServletResponse, String)
*/
private void initHandlerMapping() {
if (IoC.isEmpty())
return;
for (Entry<String, Object> entry : IoC.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(MyController.class))
continue;
String baseUrl = "";
if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
MyRequestMapping requestMapping = clazz.getAnnotation(MyRequestMapping.class);
baseUrl = requestMapping.value();
}
// 解析@MyController中方法上的@MyRequestMapping注解
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(MyRequestMapping.class)) {
continue;
}
MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
// 组合方法签名上的完整url,正则替换是为防止路径中出现多个连续多个"/"的不规范写法
String regex = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
Pattern pattern = Pattern.compile(regex);
// 保存url -> method的对应关系
handlerMapping.add(new Handler(pattern, entry.getValue(), method));
System.out.println("Mapped " + regex + " -> " + method);
}
}
}
修改 doDispatch() 方法,为了支持 url 的正则匹配和 url 参数的类型转换,我们新增了两个方法 getHandler() 和 convert():
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Handler handler = getHandler(req);
if (handler == null) {
resp.getWriter().write("404 Not Found!");
return;
}
// 获得方法的形参列表
Class<?>[] paramTypes = handler.method.getParameterTypes();
Object[] paramValues = new Object[paramTypes.length];
// 从req中接收到的参数是Map类型,需要按照handler的method中的参数顺序传递
// handler.paramIndexMapping就发挥作用了:按照参数名称映射到正确的index位置
// 保证了paramValues中参数按顺序正确传递给method
Map<String, String[]> params = req.getParameterMap();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
String value = Arrays.toString(entry.getValue()).replaceAll("\\[|\\]", "").replaceAll("\\s", ",");
if (!handler.paramIndexMapping.containsKey(entry.getKey()))
continue;
int index = handler.paramIndexMapping.get(entry.getKey());
paramValues[index] = convert(paramTypes[index], value);
}
if (handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}
if (handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}
Object returnValue = handler.method.invoke(handler.controller, paramValues);
if (returnValue == null || returnValue instanceof Void)
return;
resp.getWriter().write(returnValue.toString());
}
/**
* 根据request的url找到匹配的handler
*/
private Handler getHandler(HttpServletRequest req) {
if (handlerMapping.isEmpty())
return null;
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
for (Handler handler : handlerMapping) {
Matcher matcher = handler.pattern.matcher(url);
// 没有匹配上,继续匹配下一个
if (!matcher.matches())
continue;
return handler;
}
return null;
}
/*
* url传过来的参数都是String类型的,只需要把String转换为任意类型
*/
private Object convert(Class<?> type, String value) {
if (Integer.class == type) {
return Integer.valueOf(value);
}
// 如果还有double或者其他类型的参数,继续添加if
return value;
}
运行阶段
至此,mini-spring 的 3.0 版本就已经完成了。运行效果演示:
参考
[1] 《Spring 5 核心原理与 30 个类手写实战》,谭勇德著。