用 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,手不离开键盘就完成工作,可以专注于编码环境, ...
随机推荐
- 安装 MinGW-w64
简介 MinGW-w64 是 MinGW 项目的 64 位版本.MinGW(Minimalist GNU for Windows)是 GCC 编译套件和 GNU Binutils 移植到 Window ...
- Java Web专题攻关
servlet概念 servlet其实就是运行在服务器的一个小程序 如何去理解呢?我们访问服务器的资源包括静态资源和动态资源,其中静态资源是我们放置的模板,CSS.JS等文件,是不变的.而我们访问的动 ...
- elasticsearch单机版安装及安装过程踩的坑整理
elasticsearch单机版安装及安装过程踩的坑整理 环境及版本 Linux版本:centos7.3 JDK版本:1.8 Elasticsearch版本: Linux用户:esuser 说明:因为 ...
- DPABInet做 Network Contruction时一直报错“函数或变量 'nets_netmats' 无法识别”
DPABInet模块做 Network Contruction时一直显示报错"函数或变量 'nets_netmats' 无法识别",是因为没有将FSLNets导入路径,所以找不到该 ...
- redis 基准性能测试与变慢优化
redis 参考目录: 生产级Redis 高并发分布式锁实战1:高并发分布式锁如何实现 https://www.cnblogs.com/yizhiamumu/p/16556153.html 生产级Re ...
- 生产级Redis 高并发分布式锁实战2:缓存架构设计问题优化
对于大多数高并发场景,都是读多写少.比如商品信息,医生挂号信息等.提交订单页只有一个操作. 对于一个普通的缓存架构设计,实现商品的增删改查功能,代码如下: Controller 层 @RestCont ...
- vue 中 elementUI el-table 实现滚动加载
vue 中 elementUI el-table 实现滚动加载 一.需求 vue 中 elementUI el-table 实现滚动加载,场景:当表格需要显示大量数据时,又想通过一页来进行展示数据. ...
- SQL无法解决排序规则 Chinese_PRC_CI_AS 和 Latin1_General_CI_AS 的冲突
最近在执行一些跨库关联查询语句的时候提示了 "Cannot resolve the collatior conflict between "Chinese_PRC_Ci As&qu ...
- 【论文解读】RLAIF基于人工智能反馈的强化学习
[论文解读]RLAIF基于人工智能反馈的强化学习 一.简要介绍 人类反馈强化学习(RLHF)可以有效地将大型语言模型(LLM)与人类偏好对齐,但收集高质量的人类偏好标签是一个关键瓶颈.论文进行了一场R ...
- 【倒计时3天】“CSIG企业行”走进合合信息,大咖解密智能文档处理背后的底层技术及AI未来展望
3月18日,由中国图象图形学会(CSIG)主办,合合信息.CSIG文档图像分析与识别专业委员会联合承办的"CSIG企业行"系列活动将正式举办,通过搭建学术界与企业交流合作平台,为企 ...