系列文章

上文 中我们实现了 mini-spring 的 1.0 版本,接下来我们在此基础上进行优化,将 init() 方法中的代码进行封装。按照之前的思路,先搭建基础框架,再 “填肉注血”。

初始化阶段

init

将 init() 方法中冗长的代码进行模块化拆分:

@Override
public void init(ServletConfig config) throws ServletException {
// 1. 加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation")); // 2. 扫描相关类
doScanner(contextConfig.getProperty("scanPackage")); // 3. 初始化扫描到的类,并将它们放到IoC容器中
doInstance(); // 4. 依赖注入
doAutowired(); // 5. 初始化handlerMapping
initHandlerMapping();
}

声明全局成员变量,其中 IoC 容器就是注册时单例的具体实例:

// 保存application.properties配置文件中的内容
private Properties contextConfig = new Properties(); // 保存扫描到所有的类名
private List<String> classNames = new ArrayList<>(); // 传说中的IoC容器,我们来揭开它的神秘面纱
// 为了简化程序,暂时不考虑ConcurrentHashMap,主要关注原理和设计思想
private Map<String, Object> IoC = new HashMap<>(); // 保存url -> method的映射关系
private Map<String, Method> handlerMapping = new HashMap<>();

加载配置文件

实现 doLoadConfig() 方法:

/**
* 加载配置文件,本例中配置文件configFileName为application.properties
*/
private void doLoadConfig(String configFileName) {
// 找到Spring主配置文件所在路径
// 读取出来保存到Properties文件中
// 本例中配置文件中的内容只有一行: scanPackage=org.example.minispring
InputStream is = this.getClass().getClassLoader().getResourceAsStream(configFileName);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

扫描相关的类

实现 doScanner() 方法:

/**
* 扫描相关的类,本例中scanPackage=org.example.minispring
*/
private void doScanner(String scanPackage) {
// 转换为文件路径,手机上就是把"."替换为"/"
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classDir = new File(url.getFile());
for (File file : classDir.listFiles()) {
if (file.isDirectory()) {
// 递归扫描子文件夹
doScanner(scanPackage + "." + file.getName());
} else {
// 只需要扫描.class文件
if (!file.getName().endsWith(".class"))
continue;
String clazzName = scanPackage + "." + file.getName().replace(".class", "");
classNames.add(clazzName);
}
}
}

类实例化

实现 doInstance() 方法,原理是通过反射机制将类实例化,保存至 IoC 容器中:

/**
* 初始化扫描到的类,并将它们放到IoC容器中
*
* 本例中IoC容器中的内容如下:
* org.example.minispring.service.IDemoService = org.example.minispring.service.impl.DemoService@c97ae21
* org.example.minispring.service.impl.DemoService = org.example.minispring.service.impl.DemoService@c97ae21
* org.example.minispring.action.DemoAction = org.example.minispring.action.DemoAction@25051c3
*/
private void doInstance() {
if (classNames.isEmpty())
return;
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
// 什么类需要实例化呢?
// 加了注解的类需要实例化, 本例中需要实例化@MyController, @MyService注解的类
if (clazz.isAnnotationPresent(MyController.class)) {
Object instance = clazz.newInstance();
String beanName = clazz.getName();
IoC.put(beanName, instance);
} else if (clazz.isAnnotationPresent(MyService.class)) {
// 自定义的beanName
MyService service = clazz.getAnnotation(MyService.class);
String beanName = service.value();
if ("".equals(beanName.trim())) {
beanName = clazz.getName();
} Object instance = clazz.newInstance();
IoC.put(beanName, instance);
// 根据接口类型自动赋值
for (Class<?> i : clazz.getInterfaces()) {
if (IoC.containsKey(i.getName())) {
throw new Exception("The '" + i.getName() + "' already exists!");
}
// 把接口的类型直接当做key
IoC.put(i.getName(), instance);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

依赖注入

进行依赖注入,实现 doAutowired() 方法:

/**
* 完成依赖注入
*/
private void doAutowired() {
if (IoC.isEmpty())
return;
for (Entry<String, Object> entry : IoC.entrySet()) {
// 获取所有的字段,包括public, private, protected, default类型的
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(MyAutowired.class))
continue;
MyAutowired autoWired = field.getAnnotation(MyAutowired.class); // 如果用户没有定义beanName,默认就根据类型注入
String beanName = autoWired.value().trim();
if ("".equals(beanName)) {
beanName = field.getType().getName();
} // 如果是public之外的类型,只要加了@MyAutowired注解都要强制赋值
// 反射中叫做暴力访问
field.setAccessible(true); try {
// 用反射机制动态给字段赋值
// 赋值后DemoAction.demoService = org.example.minispring.service.impl.DemoService@c97ae21
// 也即DemoService实例被注入到了DemoAction对象中,此谓之依赖注入
field.set(entry.getValue(), IoC.get(beanName));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}

初始化 handlerMapping

实现 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 url = (baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
// 保存url -> method的对应关系
handlerMapping.put(url, method);
System.out.println("Mapped " + url + " -> " + method);
}
}
}

到这里初始化工作完成,接下来实现运行的逻辑。

运行阶段

首先看 doPost() 和 doGet() 方法的代码:

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
} @Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req, resp);
} catch (Exception e) {
e.printStackTrace();
resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
}
}

核心逻辑在 doDispatch() 方法中实现,其中用到了委派模式的思想:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
if (!this.handlerMapping.containsKey(url)) {
resp.getWriter().write("404 Not Found");
return;
}
// 根据url找到对应的方法
// 例如url(/demo/query)对应的method为
// org.example.minispring.action.DemoAction.query(HttpServletRequest, HttpServletResponse, String)
Method method = (Method) this.handlerMapping.get(url);
// 获取请求参数, 此处为: name = [watermark]
Map<String, String[]> parameterMap = req.getParameterMap(); // 1. method.getDeclaringClass().getName(): beanName, 此处为org.example.minispring.action.DemoAction
// 2. IoC.get(beanName): 根据beanName获取到对应的bean实例:org.example.minispring.action.DemoAction@51e3ce14
// 3. method.invoke调用的就是org.example.minispring.action.DemoAction@51e3ce14.query(req, resp, name)
String beanName = method.getDeclaringClass().getName();
method.invoke(IoC.get(beanName), new Object[] { req, resp, parameterMap.get("name")[0] });
}

在以上代码中,doDispatch() 虽然完成了动态委派并进行了反射调用,但是对 url 参数的处理还是静态的,即只能对 "name" 参数做解析。要实现 url 参数的动态获取,还有些工作要做,我们继续优化 doDispatch() 方法的实现:

private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
if (!this.handlerMapping.containsKey(url)) {
resp.getWriter().write("404 Not Found");
return;
}
// 根据url找到对应的方法
// 例如url(/demo/query)对应的method为
// org.example.minispring.action.DemoAction.query(HttpServletRequest, HttpServletResponse, String)
Method method = (Method) this.handlerMapping.get(url);
// 获取请求参数, 此处为: name = [watermark]
Map<String, String[]> parameterMap = req.getParameterMap(); // 1. method.getDeclaringClass().getName(): beanName, 此处为org.example.minispring.action.DemoAction
// 2. IoC.get(beanName): 根据beanName获取到对应的bean实例:org.example.minispring.action.DemoAction@51e3ce14
// 3. method.invoke调用的就是org.example.minispring.action.DemoAction@51e3ce14.query(req, resp, name)
// String beanName = method.getDeclaringClass().getName();
// method.invoke(IoC.get(beanName), new Object[] { req, resp, parameterMap.get("name")[0] }); // url参数的动态处理
// 获取method的形参列表
Class<?>[] parameterTypes = method.getParameterTypes();
// 保存赋值参数的位置
Object[] paramValues = new Object[parameterTypes.length];
// 根据参数的位置动态赋值
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
if (parameterType == HttpServletRequest.class) {
paramValues[i] = req;
continue;
} else if (parameterType == HttpServletResponse.class) {
paramValues[i] = resp;
continue;
} else if (parameterType == String.class) {
// 提取方法中加了注解的参数
Annotation[][] pa = method.getParameterAnnotations();
for (int j = 0; j < pa.length; j++) {
for (Annotation a : pa[j]) {
if (a instanceof MyRequestParam) {
String paramName = ((MyRequestParam) a).value().trim();
if (!"".equals(paramName)) {
String paramValue = Arrays.toString(parameterMap.get(paramName))
.replaceAll("\\[|\\]", "")
.replaceAll("\\s", ",");
paramValues[i] = paramValue;
}
}
}
}
}
}
String beanName = method.getDeclaringClass().getName();
// 根据动态获取的参数去invoke method
method.invoke(IoC.get(beanName), paramValues);
}

运行演示

到此为止我们就实现了 mini-spring 的 2.0 版本。

2.0 版本在 1.0 基础上做了优化,但是还有很多工作要做,例如 HandlerMapping 还不能像 SpringMVC 一样支持正则,url 参数还不支持类型转换,在 3.0 版本中我们将继续优化,请看下篇 用 300 行代码手写提炼 Spring 核心原理 [3]

参考

[1] 《Spring 5 核心原理与 30 个类手写实战》,谭勇德著。

用 300 行代码手写提炼 Spring 核心原理 [2]的更多相关文章

  1. 【面试题】手写async await核心原理,再也不怕面试官问我async await原理

    前言 async await 语法是 ES7出现的,是基于ES6的 promise和generator实现的 generator函数 在之前我专门讲个generator的使用与原理实现,大家没了解过的 ...

  2. 30个类手写Spring核心原理之环境准备(1)

    本文节选自<Spring 5核心原理> 1 IDEA集成Lombok插件 1.1 安装插件 IntelliJ IDEA是一款非常优秀的集成开发工具,功能强大,而且插件众多.Lombok是开 ...

  3. 自定义控件?试试300行代码实现QQ侧滑菜单

    Android自定义控件并没有什么捷径可走,需要不断得模仿练习才能出师.这其中进行模仿练习的demo的选择是至关重要的,最优选择莫过于官方的控件了,但是官方控件动辄就是几千行代码往往可能容易让人望而却 ...

  4. 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍

    概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...

  5. 通过 Mesos、Docker 和 Go,使用 300 行代码创建一个分布式系统

    [摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...

  6. 【Xamarin挖墙脚系列:代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧(转)】

    正愁如何选择构建项目中的视图呢,现在官方推荐画板 Storybord...但是好像 xib貌似更胜一筹.以前的老棒子总喜欢装吊,用代码写....用代码堆一个HTML页面不知道你们尝试过没有.等页面做出 ...

  7. Python:游戏:300行代码实现俄罗斯方块

    本文代码基于 python3.6 和 pygame1.9.4. 俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块.但是想到旋转,停靠,消除等操作,感觉好像很难啊, ...

  8. 代码手写UI,xib和StoryBoard间的博弈,以及Interface Builder的一些小技巧

    近期接触了几个刚入门的iOS学习者,他们之中存在一个普遍和困惑和疑问.就是应该怎样制作UI界面.iOS应用是非常重视用户体验的,能够说绝大多数的应用成功与否与交互设计以及UI是否美丽易用有着非常大的关 ...

  9. 通过Mesos、Docker和Go,使用300行代码创建一个分布式系统

    [摘要]虽然 Docker 和 Mesos 已成为不折不扣的 Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略 Mesos .Docker 和 Go 配合带来的强大破坏力 ...

  10. 关于代码手写UI,xib和StoryBoard

    代码手写UI 这种方法经常被学院派的极客或者依赖多人合作的大型项目大规模使用.Geek们喜欢用代码构建UI,是因为代码是键盘敲出来的,这样可以做到不开IB,手不离开键盘就完成工作,可以专注于编码环境, ...

随机推荐

  1. 【爬虫实战】——利用bs4和正则表达式,简单实现爬取数据

    前言 好久没有写博客了,由于一直比较忙,感觉快荒废了学习的步伐,最近由于需要利用爬虫爬取数据,总结一下,以便以后查阅. 目录 一.bs4的安装 二.bs4解析器 三.定位查找标签 四.转换格式 五.提 ...

  2. SpringBoot启动项目报错:java.lang.UnsatisfiedLinkError: D:\files\software\jdk-15.0.1\jdk-17.0.3.1\bin\tcnative-1.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform

    目录 问题描述 解决方法: 问题描述 在运行向的时候出现报错: java.lang.UnsatisfiedLinkError: D:\files\software\jdk-15.0.1\jdk-17. ...

  3. 如何调用openai的TTS模型

    这是24年1月份写的了,调用代码大概率有变动,仅供参考. 1 什么是OpenAI的TTS模型 OpenAI的TTS模型是一种文本到语音(Text-to-Speech)模型,它可以将给定的文本转换为自然 ...

  4. Goby 漏洞发布|(CVE-2024-45195)Apache OFBiz /viewdatafile 代码执行漏洞【已复现】

    漏洞名称:Apache OFBiz /viewdatafile 代码执行漏洞(CVE-2024-45195) English Name:Apache OFBiz /viewdatafile Code ...

  5. 闲的蛋疼整理了一下Dockerfile的命令和参数备查

    Dockerfile 主要指令及参数: 指令 主要参数 作用 用法示例 FROM <image>[:<tag>] [AS <name>] 指定基础镜像 FROM u ...

  6. 『读书笔记』你不知道的JavaScript(上)

    前言 文章只记录理解以及容易遗忘的知识点. 词法作用域.块作用域 词法作用域 词法作用域:简单的说,词法作用域就是定义在词法阶段的作用域.换句话说,词法作用域就是在你写代码时将变量和块作用域写在哪里来 ...

  7. 如何将图片转换为向量?(通过DashScope API调用)

    本文介绍如何通过模型服务灵积DashScope将 图片转换为向量 ,并入库至向量检索服务DashVector中进行向量检索. 模型服务灵积DashScope,通过灵活.易用的模型API服务,让各种模态 ...

  8. Bit, Byte, ASCII, Unicode, UTF, Base64

    前言 做项目偶尔会接触到 stream 这个感念,不管是 memory stream 还是 file stream,它们又会提到 bytes. 还有像 Identity – 安全基础知识 中提到的 S ...

  9. ASP.NET Core 单元测试

    前言 单元测试是好, 但是也很花时间. 有些功能封装好了以后也不怎么会再打开, 所以通常就是徒手测试一下, 过了就过了. 但是往往就是那么神奇, 就是会有需求漏掉. 后来要加, 又由于不想潜水, 对自 ...

  10. Qt表格入门

    摘要     表格作为数据展示的界面,会在很多场景下使用.Qt为我们提供了使用简单方便和扩展性强的表格视图,这里做一个简单的入门整理.     个人能力有限,有错误欢迎留言指正,如果你有更好的方法,也 ...