用 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 个类手写实战》,谭勇德著。
用 300 行代码手写提炼 Spring 核心原理 [3]的更多相关文章
- 【面试题】手写async await核心原理,再也不怕面试官问我async await原理
前言 async await 语法是 ES7出现的,是基于ES6的 promise和generator实现的 generator函数 在之前我专门讲个generator的使用与原理实现,大家没了解过的 ...
- 30个类手写Spring核心原理之环境准备(1)
本文节选自<Spring 5核心原理> 1 IDEA集成Lombok插件 1.1 安装插件 IntelliJ IDEA是一款非常优秀的集成开发工具,功能强大,而且插件众多.Lombok是开 ...
- 自定义控件?试试300行代码实现QQ侧滑菜单
Android自定义控件并没有什么捷径可走,需要不断得模仿练习才能出师.这其中进行模仿练习的demo的选择是至关重要的,最优选择莫过于官方的控件了,但是官方控件动辄就是几千行代码往往可能容易让人望而却 ...
- 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍
概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...
- 通过 Mesos、Docker 和 Go,使用 300 行代码创建一个分布式系统
[摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...
- 【Xamarin挖墙脚系列:代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧(转)】
正愁如何选择构建项目中的视图呢,现在官方推荐画板 Storybord...但是好像 xib貌似更胜一筹.以前的老棒子总喜欢装吊,用代码写....用代码堆一个HTML页面不知道你们尝试过没有.等页面做出 ...
- Python:游戏:300行代码实现俄罗斯方块
本文代码基于 python3.6 和 pygame1.9.4. 俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块.但是想到旋转,停靠,消除等操作,感觉好像很难啊, ...
- 代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧
近期接触了几个刚入门的iOS学习者,他们之中存在一个普遍和困惑和疑问.就是应该怎样制作UI界面.iOS应用是非常重视用户体验的,能够说绝大多数的应用成功与否与交互设计以及UI是否美丽易用有着非常大的关 ...
- 通过Mesos、Docker和Go,使用300行代码创建一个分布式系统
[摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...
- 关于代码手写UI,xib和StoryBoard
代码手写UI 这种方法经常被学院派的极客或者依赖多人合作的大型项目大规模使用.Geek们喜欢用代码构建UI,是因为代码是键盘敲出来的,这样可以做到不开IB,手不离开键盘就完成工作,可以专注于编码环境, ...
随机推荐
- ClassFinal防JAVA代码反编译
亲测可用!不过项目暂停更新了. 传送地址:https://gitee.com/roseboy/classfinal#%E5%8A%A0%E5%AF%86
- 全网最适合入门的面向对象编程教程:40 Python常用复合数据类型-枚举和enum模块的使用
全网最适合入门的面向对象编程教程:40 Python 常用复合数据类型-枚举和 enum 模块的使用 摘要: 在 Python 中,枚举(Enumeration, Enum)是一种复合数据类型,用于表 ...
- 第一个鸿蒙程序Hello Word
DevEco Studio介绍 HUAWEI DevEco Studio(以下简称DevEco Studio)是基于IntelliJ IDEA Community开源版本打造,面向华为终端全场景多设备 ...
- Vert.x HttpClient调用后端服务时使用Idle Timeout和KeepAlive Timeout的行为分析
其实网上有大量讨论HTTP长连接的文章,而且Idle Timeout和KeepAlive Timeout都是HTTP协议上的事情,跟Vert.x本身没有太大关系,只不过最近在项目上遇到了一些问题,用到 ...
- Vs Code, Visual Studio 2022, Angular and Live Server Running Through Https and IP Address
前言 之前就写过 angular cli, vs code liveserver, vs 2019 iis express 10, vs code kestrel 使用 https + ip. 但写的 ...
- GPUStack 0.2:开箱即用的分布式推理、CPU推理和调度策略
GPUStack 是一个专为运行大语言模型(LLM)设计的开源 GPU 集群管理器,旨在支持基于任何品牌的异构 GPU 构建统一管理的算力集群,无论这些 GPU 运行在 Apple Mac.Windo ...
- 彻底解决 user.config 文件损坏
症状见 发生 Configuration system failed to initialize 错误的一个特例 解决的办法,在去读 user.settings 之前捕获错误,比如 Main() 里面 ...
- linux(centos7)中安装7z
linux(centos7)中安装7z 一. 先安装wget yum -y install wget 二. 下载7z的压缩包 wget https://sourceforge.net/projects ...
- 标准库之 datetime和time 模块
一.time 模块 time模块是Python标准库中最基础.最常用的模块之一.它提供了各种处理时间的方法和函数,如获取当前时间.格式化时间.计算时间差等.time模块大部分函数的底层实现是 C 语言 ...
- Gradio 5 稳定版正式发布
在过去的几个月里,我们一直在努力工作,今天,我们想向大家展示成果:Gradio 5 稳定版现已发布. 有了 Gradio 5,开发者可以构建 生产级的机器学习 Web 应用,这些应用不仅性能优越.可扩 ...