003-ioc容器

实现 ResourceResolver

要实现 ResourceResolver,可以按照以下步骤进行操作:

  1. 定义不变类 Resource,用于表示资源。这个类可以包含资源的相关信息和操作方法。

  2. 创建目录解析类 ResourceResolver,用于扫描目录中的资源。这个类应该具有以下功能:

    • 接受待搜索的包名作为输入参数。
    • 允许客户端定制只保留特定类型的资源,例如只保留 class 文件。
    • 扫描指定包下的所有文件,并将它们表示为 Resource 对象。
  3. 获取包名的类加载路径,可以使用 ClassLoader 来实现。这样可以得到 URL 地址,URL 地址和 URI 地址相同。

  4. 处理 URL 地址:

    • 如果扫描的是 JAR 包中的内容,URL 地址开头会有 jar:
    • 如果扫描的不是 JAR 包中的内容,URL 地址开头为 file:/
    • 注意有时候地址末尾会多出 /,需要避免这种情况。
  5. 将路径转换为标准路径,以便可以被 File 读取:

    • 根据 URI 的类型,去除 URI 前导的协议前缀,例如 jar:file:/
    • 根据操作系统的不同,替换路径分隔符,例如在 Linux 上保持不变,在 Windows 上将 / 替换为 \。可以使用 File.separator 动态获取分隔符。
  6. 遍历目录下的所有文件,可以使用 Files.walk() 方法结合流操作来实现:

    • 使用方法引用 Files::isRegularFile 判断是否是一个合法的文件路径,即判断是否为文件。
    • 对于每个文件路径,判断是否为 JAR 文件,根据文件类型生成相应名称的 Resource 对象。
    • Resource 对象添加到列表中。
  7. 完成 ResourceResolver 的实现。

需要注意的是,这个实现涉及到文件地址的类型、URI 和 URL 的区别以及转换等细节。因此,在实现过程中需要特别注意处理这些细节。

可能的 uri 路径如下:

jar:file:/E:/Program%20Files%20(x86)/maven/repository/org/yaml/snakeyaml/2.0/snakeyaml-2.0.jar!/META-INF/versions/9/org/yaml/

file:/D:/myCode/formal-projects/yelanyanyu-mvc/yelanyanyu-mvc/mvc-context/target/classes/com/yelanyanyu/io

注意

Jar 的读取

关于 Jar 的读取。jar 文件是一个压缩文件,所以我们不能简单粗暴用类加载地址的形式(即形如 D:/myCode/formal-projects/yelanyanyu-mvc/yelanyanyu-mvc/mvc-context/target/classes/com/yelanyanyu)传入 Files 进行遍历;

要对 jar 进行遍历加载,可以直接调用特殊的 api,这里给一个通用的:

private Path jarUriToPath(String basePackagePath, URI jarUri) {
	try {
		return FileSystems.newFileSystem(jarUri, Map.of()).getPath(basePackagePath);
	} catch (IOException e) {
		throw new RuntimeException(e);
	}
}

解读:

以下是一个简单的使用:

@Test
public void t2() throws Exception {
	URL resource = Thread.currentThread().getContextClassLoader().getResource("cn/hutool");
	Files.walk(jarUriToPath("cn/hutool", resource.toURIprintln;
}

uri 和 url 的格式区别

在该实现中,url 和 uri 的格式并没有什么区别。因为,都是在同一台主机上编译运行的。

文件的开头和结尾分隔符

当我们得到文件的路径或者 uri 时,开头或者结尾可能会带有分隔符 / 或者 \(仅 windows),我们需要将其去掉。我们举个例子:
即,扫描目录时,返回的路径可能是 abc/xyz,也可能是 abc/xyz/,需要注意处理末尾的 /
同一个文件,可能在 Java 中或显示为:

D:\a\b
D:\a\b\
\D:\a\b

我们通过两个 api 解决:

private String removeLeadingSlash(String s) {
	if (s.startsWith("/") || s.startsWith("\\")) {
		s = s.substring(1);
	}
	return s;
}

private String removeTrailingSlash(String string) {
	if (string.endsWith("/") || string.endsWith("\\")) {
		string = string.substring(0, string.length() - 1);
	}
	return string;
}

中文路径问题

为了解决中文路径问题,在 uri 转换为 string 的时候,需要用 utf 8 转换一下:

URLDecoder.decode(uri.toString(), StandardCharsets.UTF_8);

实现 PropertyResolver

扫描配置,形如 ${...},或者 .properties 文件中的 a.b=c 形式的配置;

创建 BeanDefinition

寻找符合条件的 bean

如果我们将其他 jar 的对象加入 ioc,那么就不能用 @Component 的注解了,我们需要用 @Bean 来注入,而 Bean 注解的注入依赖的是工厂设计模式。

spring 中的 ioc 容器提供了两种获取 bean 的方式:

获取所有BeanDefinition信息

先通过我们之前写的 ResourceResolver 得到 ComponentScan 注解下的所有资源。注意,我们还有获取通过 import 注解注入的对象。通过方法 scanForClassNames

真正的扫描类,并加入 beans map。我们再想一个问题,spring 有那么多注解,难道我们就一个个写 if 判断吗?其实不是的,controller 和 service 等注解都是基于 Component 注解的。也就是说 spring 的注解是有父子关系的。所以,我们只需要找到一个组件是否有 Component 注解即可。

通过方法 createBeanDefinitions 来注入 bean,基本的流程如下:

Bean 实例的创建

完成 bean 的创建,解决依赖注入的问题。这个步骤的创建是强依赖的注入。

  1. 创建 Bean 的实例,此时必须注入强依赖;
  2. 对 Bean 实例进行 Setter 方法注入和字段注入。

第一步如果遇到循环依赖则直接报错,第二步则不需要关心有没有循环依赖。

基本概念

bean 注入有 4 种方式:构造方法、工厂方法注入、Setter 方法注入、字段注入。

在这个阶段,我们完成前两种注入的方式。

基本步骤

创建 bean 的步骤如下:

  1. 在构造器中先过滤出所有的工厂 bean(被 @Configuration 修饰),先创建这些工厂 bean。这就意味着在工厂 bean 中无法注入其他 bean 对象,即被@Autowired 注释的属性(但是可以注入 @Value 修饰的值)
  2. 工厂 bean 本身是被由其默认无参构造器创建的;
  3. 创建非工厂类,详见方法 createBeanAsEarlySingleton
    1. 可能会存在强依赖的情况,所以首先需要判断该 bean 是否为强依赖。可以添加一个参数 Set<String> creatingBeanNames; ,该参数记录了之前已经创建过的 bean。如果在某个情况,尝试二次注入某个 bean,那么就极有可能是强依赖循环的错误,直接抛出异常即可;
    2. 不论是工厂方法还是构造器注入,都是依赖的是方法注入,而方法就有参数,我们先要解析参数,先将参数注入完成;
    3. 参数注入完成后,再调用方法。具体来说,如果是构造器注入,那么就调用 def.getConstructor().newInstance(args) ;若是工厂方式注入,那么就调用 def.getFactoryMethod().invoke(bean, args)

Bean 的初始化

根据 Setter 方法和字段完成弱依赖注入,接着调用用 @PostConstruct 标注的 init 方法,就完成了所有 Bean 的初始化。

在这个阶段,我们将会完成 bean 的最后两种注入方式。

注入 bean 的核心方法为:injectBean() and injectProperties

调用 init 方法的核心方法为:initBean and callMethod

实现 BeanPostProcessor

该类的作用其实就是类的替换,实际环境中,我们会将原来的类替换成一个代理类。

引入这个功能,会使得依赖注入,变得复杂。
但有两个规律:

根据以上规律,需要做如下的动作:

步骤

  1. 在创建 @Configuration 后,接着创建 BeanPostProcessor,再创建其他普通 Bean;
  2. 修改 createBeanAsEarlySingleton(),创建 Bean 实例后,调用 BeanPostProcessor 处理;
  3. BeanPostProcessor 加一个 postProcessOnSetProperty() 方法,让它返回原始 Bean。每当我们要实现一个后置处理器,就要实现这个方法,使其能够返回原始的 bean。具体做法是,在 processor 中加入一个 map 型的变量用来存储原始对象,然后在调用方法 postProcessOnSetProperty() 取出替换;
  4. injectBean() 改一下,不要直接拿 BeanDefinition.getInstance(),而是拿到原始 Bean。新建方法 getOriginBean(BeanDefinition def) 得到原始的 bean:
    1. 利用倒序调用 processor 的 postProcessOnSetProperty() 方法来达成恢复 Bean 的作用;
    2. 为什么要倒序调用?这是为了解决多重代理的情况。假如 A 由 B 代理,B 又由 C 代理,那么首先我们就要将 C 恢复成 B,最后再将 B 恢复成 A。显然是要倒序调用的;

收尾工作

提供对外,和对 framework 开发级别的接口,并实现相关方法。

有几点需要注意的: