好的。
这里是一个新的工程。
今天我们来手写一个 Spring Framework。
在手写之前大家要清楚一件事情:Spring 是干什么的?Spring 就负责两件事——一件事是造对象,一件事是拿对象。
这里有一个 Spring 的核心类叫 ApplicationContext
,我们就用它来负责造对象和拿对象。
我这里提供了三个函数,分别是拿对象的三个 API:
这里是一个 getBean
。getBean
是什么意思呢?通过一个名字拿到一个对象。
这里还有一个 getBean
,这个 getBean
是通过一个类型拿到一个对象。那么在 Spring 上下文中,这个类型可能有多个,所以这里还有一个 getBeans
。getBeans
是返回一个 List。
好的,我们只定义这三个函数——拿对象的函数。
我们定义好了之后,我们就要定义造对象的函数。那么造的对象是什么时候造的?应该在我们上下文初始化的时候就造出来了。所以我们定义一个函数叫 initContext
。在这个函数中,我们直接让我们所有应该造的对象都初始化完成。
public class ApplicationContext {
public Object getBean(String name) {
return null;
}
public <T> T getBean(Class<T> beanType) {
return null;
}
public <T> List<T> getBeans(Class<T> beanType) {
return null;
}
public void initContext() {
ApplicationCentext.class.getClassLoader().getResource("");
}
}
那么我们现在只需要填充这个造对象的函数就行了。
那么造对象我们又有两个问题:
第一个问题是:造什么对象?
第二个问题是:怎么造?
当然,这个“怎么造”一定是调用它的构造函数。因为 Java 在不使用黑科技的前提下,只能通过调用构造函数来创建对象。
那么这个“怎么造”指的不是我们调用构造函数,而是:如果它的构造函数上有参数,那么这些参数从哪来?如果这个对象里有一些属性是需要自动注入的,那这些属性从哪来?如果这个对象定义了一些生命周期的函数,这些生命周期由谁来执行?这些才是我们“怎么造对象”的问题。
我们先来回答“造什么对象”的问题。
我们定义一个注解叫 @Component
,然后给这个注解增加一些元信息:@Target
,然后增加一个运行时可以获得这个注解信息的元注解(@Retention(RetentionPolicy.RUNTIME)
)。
OK,加了两个元注解。这句话的意思是:我们这个 @Component
注解可以加在类上;第二个注解的意思是,我们可以在运行时获得 @Component
的注解信息。
那么我们现在规定:只要添加了这个 @Component
注解的类,我们都需要把它加载到容器中。
那么我们如何扫描带有 @Component
的类呢?我们可以通过 ClassLoader
里面的 getResource
去拿到我们 classpath 下面的资源。比如说我们拿到一个 ClassLoader
,然后去 getResource
,我们可以拿到对应的资源。
但是我们要拿到哪些文件夹下面的资源呢?我们不可能把整个 classpath 全都扫描一遍。所以这里我们需要一个包名,我们只扫描某些包名下面的类。
这里其实我们可以支持多个包名的扫描,但是为了简单化,我们这里只支持一个包名。
那么我们在初始化的时候,我们可以传进来一个 package name。我们可以在构造函数的时候直接把这个初始化完成。
那么我们在这定义一个构造函数:如果你想创建一个 ApplicationContext
的这个容器的话,你需要给我一个包名。这个包名叫 packageName
。我们在这里面直接初始化我们的容器上下文。
我们可以在这定义一个扫描包的函数,这个函数叫 scanPackage
,然后我们需要一个 packageName
。
我们应该返回一个什么呢?我们应该返回一个集合,这个集合装的是我们这个包里面的所有类的定义信息。
我们先不填充函数。我们通过这个 scanPackage
,我们把这个 packageName
传给他。
我们需要过滤一下。为什么要过滤一下?因为我们不是拿到一个 class 就需要给他创建对象,我们是需要判断他是否拥有我们的 @Component
注解。
那么我们可以在这有一个 isAnnotationPresent
,然后把我们的 Component
信息传进去。这样我们在 toList
,拿到的就是我们所有的 @Component
注解的类信息。
那么我们可以叫 componentClassList
。
好的,我们格式化一下。
我们可以通过这个 componentClassList
里面的 class 信息去直接开始造对象吗?如果它里面还有需要自动注入的属性,那么我们还需要拿到它的属性。这一系列的操作,我们都需要用反射去直接拿到它对应的信息。
public class ApplicationContext {
public ApplicationContext(String packageName) {
initContext(packageName);
}
public void initContext(String packageName) {
List<Class<?>> componentClassList =
scanPackage(packageName).stream().filter(c -> c.isAnnotationPresent(Component.class)).toList();
}
public Object getBean(String name) {
return null;
}
public <T> T getBean(Class<T> beanType) {
return null;
}
public <T> List<T> getBeans(Class<T> beanType) {
return null;
}
}
那么可能这里面我们就需要大段的代码。那么我们不如把它重新封装一下,让我们拿一个可以自己定义的、自己拓展的这么一个类。
我们先把它定义就叫 BeanDefinition
。
OK,这是一个 bean 的定义。
那么我们在创建一个类信息的时候需要什么呢?需要一个 class 的对象叫 type
。
如果 ApplicationContext
是一个 bean 工厂的话,那么 bean 就是我们这个工厂生产出来的产品,这个 BeanDefinition
就是这个产品的设计图。
那么这个设计图应该有什么东西呢?我们每一个 bean 都应该有一个名字。我们还可以直接用 BeanDefinition
去拿到我们想调用的构造函数(Constructor
)。对,Constructor
这里有一个泛型。
public class BeanDefinition {
public BeanDefinition(Class<?> type) {
}
public String getName() {
return "";
}
public Constructor<?> getConstructor() {
return null;
}
}
我们还可以把我们依赖的关系定义在这个 BeanDefinition
里面。但是现在我们还没有注入的功能,所以我们先不考虑这些问题。我们只需要去创建一个这个东西就可以了。
我们回到我们的 ApplicationContext
里面。那么在这我们重新写一下我们的函数。
我们先 scan
一下这个 package,我们拿到了一些 class。我们把它 stream
一下,然后我们把它 filter
一下。filter
里面的含义是什么呢?是什么样的类是我们可以生产的?
那么我们在这可以再封装一个对象,是一个布尔值,叫 canCreate
。我们传进来一个 class 对象,那么我们在里面返回一个这个 type 是否拥有我们的 @Component
注解。
好的,为什么我把它设为 protected
呢?因为如果我们有其他的类想继承我们的 ApplicationContext
,它只要重写我们这个函数,它就可以自定义什么样的类它是支持加载的。
好的,我们在这直接用方法引用就可以了:scan.filter(this::canCreate)
。
因为 filter
之后我们拿到的还是类,那么我们还要把我们的类变成我们的 BeanDefinition
。
那么我们在这再创建一个函数,叫 wrap
。它应该通过什么呢?它应该有一个函数,这个函数也是一个 type。
那么我们先直接返回一个 new BeanDefinition(type)
。我们把 type 传进去。
那么我们在这再 map
一下:.map(this::wrap)
。
好的,我们在 toList
,我们拿到的是一个 BeanDefinition
的集合。下面我们就不要了。
好的,我们这是一句话——BeanDefinition
的集合。
那么接下来我们要通过这个 BeanDefinition
去创建对象了。那么我们在这再写一个函数,叫 createBean
。
createBean
应该拿一个什么呢?应该拿一个 BeanDefinition
。
好的,这里我们先不填。
那么在这句话里面我们就可以再改一次:那么我们就可以让它循环——循环什么呢?循环我们的 createBean
。
OK,我们的初始化操作我们就通过流的方式去只变成了一句话。
好的,我们整个流程其实就已经定义完了。那么我们只需要填充这里面的函数就可以了。
public class BeanDefinition {
public BeanDefinition(Class<?> type) {
}
public String getName() {
return "";
}
public Constructor<?> getConstructor() {
return null;
}
}
我们先来填充这个包扫描的函数。
这里注意:我们传进来的包名应该是 a.b.c
。我们通过 ClassLoader
查询的文件资源应该是 a/b/c
的文件夹形式。当然如果你是 Windows 的话,这边应该是一个反斜杠。不过没关系,我们可以通过 API 的方式去把它替换掉。
好的,我们先来写一个 getClass().getClassLoader()
,然后我们有一个 getResource
。我们要加载一个资源,这个资源我们可以传一个文件夹的路径:a/b/c
。
那么因为我们传进来的包名是 a.b.c
,所以我们需要把这个包名 replace
,把它的点替换成 File.separator
。
我们拿到一个资源,这个资源是一个 URL
。这个 URL
是什么意思呢?它是通过 classpath 去找到对应的下面的路径。
那么现在我们这个项目的 classpath 就在 target/classes
里面。也就是说,我们传进来的如果是 take.inside.spring
,那么我们就会在这个 classes
里面去找到 take/inside/spring
这么一个文件夹。
那么我们通过它去拿到对应的文件夹:.getFile()
。它拿到一个 string。我们可以把这个 string 换成一个 Path
。
我们拿到一个 Path
路径。那么这个 path 我们可以通过 JDK 为我们提供的高级 API:Files.walkFileTree
。
这个函数可以帮我们递归地遍历一个 path。这个函数有两个参数:第一个参数是一个 path,问我们遍历哪个文件夹;第二个参数是一个 FileVisitor
的对象。这是一个经典的访问者模式。如果你不知道的话,也没关系,可以跟着我一起写。

第一个参数,我们要告诉他这个 path 我们要遍历哪个文件夹。那么第二个参数我们有一个 SimpleFileVisitor
。
好的,这是一个访问者对象。我们先把所有的异常都抛出去。我们所有的项目既不判空,也不处理异常。
好的,我们在这重写一个方法,这个方法叫 visitFile
。也就是说,我们递归地调用文件夹的时候,每一次遍历到我们的文件,我们是如何处理这个文件的——这就是我们如何处理的函数逻辑。
那么它有一个返回值,这个返回值是一个标记,这个标记表示当你遍历到某个条件的时候,你是否还要继续呢?那么我们在这遇到什么条件,我们都需要继续,所以我们就直接返回一个 FileVisitResult.CONTINUE
。好的,这个意思就是说:无论我们遇到什么问题,我们都把所有的文件都递归遍历完。
那么在这,我们可以拿到我们这个函数的绝对路径。那么在这我们可以判断一下:如果这个路径它的 toString()
是 endsWith(".class")
,也就是说它是一个 class 文件,那么我们就打印一下它的名字。
public class ApplicationContext {
public ApplicationContext(String packageName) {
initContext(packageName);
}
public void initContext(String packageName) throws IOException {
scanPackage(packageName).stream().filter(this::scanCreate).map(this::createBean).forEach(this::createBean);
}
protected boolean scanCreate(Class<?> type) {
return type.isAnnotationPresent(Component.class);
}
protected void createBean(BeanDefinition beanDefinition) {
}
protected BeanDefinition wrapper(Class<?> type) {
return new BeanDefinition(type);
}
private List<Class<?>> scanPackage(String packageName) throws IOException {
URL resource = this.getClass().getClassLoader().getResource(packageName.replace(".", File.separator));
Path path = Path.of(resource.getFile());
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path absolutePath = file.toAbsolutePath();
if (absolutePath.toString().endsWith(".class")) {
System.out.println(absolutePath);
}
return FileVisitResult.CONTINUE;
}
});
return null;
}
public Object getBean(String name) {
return null;
}
public <T> T getBean(Class<T> beanType) {
return null;
}
}
好的,我们先写一个测试。我们先创建一个 main 函数,我们
public class Main {
public static void main(String[] args) throws IOException {
new ApplicationContext("tech.insight.spring");
}
}
好的,我们在这有没有异常?要处理一下。好的,我们在这抛一下异常——我们就把所有的异常都抛出来算了。
好的,这个还是会报错,因为我们所有的逻辑都没有填充完,很多地方都是返回 null 的。但是没有关系,我们只想看到这里面是不是打印出来的就可以。
OK,没问题。我们这里拿到了很多 class 文件。这个 ApplicationContext$1
就是我们刚才创建的这个匿名的访问者对象。
OK,现在我还想测试一下:他是否能拿到我们子文件夹的对象?那么我们就在这创建一个 sub.Cat
吧,我们创建一只猫。
好的,什么都不用变。我们再跑一下,我们看看我们能否拿到这个 cat 对象。
OK,没问题。我们确实是拿到了子文件夹。也就是说,这个函数既帮我们拿到了这个文件夹的文件,也帮我们拿到这个文件夹子文件夹的文件。
好的,那么我们只需要在这儿进行处理就行了。
既然我们已经拿到了这个绝对路径,这个绝对路径是这个东西,那么我现在需要用这个绝对路径的字符串把它变成一个真正的 class 类。
我们只需要拿到这个绝对路径,把这个斜杠再转成点。然后我们传进来的这个包名,比如说是 take.inside.spring
,那么我们在这 take.inside.spring
,我们只需要把它开头后面的全都拿到,然后把这个 .class
去掉,我们就拿到了这么一个真正的 class 全类名。
好的,我们直接开启。
我们先让它 path.toString()
,先把它 replace
,先把它的 File.separator
再转成点。我们把这个 replace 的字符串拿到,叫 replaceStr
。
然后把它 indexOf
,indexOf
什么呢?我们传进来的这个包名。
好的,我们拿到了一个 index,这个叫 packageIndex
。
好的,我们直接把它 .substring(packageIndex)
。我们再打印一下,我们看一下拿到的这个字符串是什么。
执行一下。OK,没问题,这就是他的全类名。我们只需要把最后的这个 .class
去掉就可以了。
那么我们可以直接用 substring
:第一个参数是一个开始,最后一个参数是一个结尾。那么我们只需要把它的长度,它的 length
减去一个 .class
的 length
。
OK,我们再执行一下。好的,我们拿到的就是他们的全类名了。
那么既然我们拿到了他们的全类名,我们在这接收一下 className
。
好的,我们最终要返回的是一个 class 对象。那么我们要拿到一个容器,叫 classList
,创建一个 ArrayList
,把它加一个 Class.forName(className)
,把这个传进来,把它返回回去。
所有的异常我们全抛出来。在这我们返回我们的 classList
。
好的,那么我们在这我们把所有的异常都抛出来。好的,在这也抛出来这个异常,在这也抛出这个异常。
好的,我们在调用的时候,我们就可以看到我们传进来的这个 package。我们在这打一个断点,看一下我们拿到的是不是我们想要的这些类。
好的,我们拿到了六个类。这六个类分别是我们的包下的所有的类名。OK,那没问题。
好的,接下来我们开始创建对象。
那么既然有创建对象,我们就应该有一个地方去装着我们创建的对象。我们在这先写一个容器,写一个 map。这个 map 的 key 是我们对象的名字,对象的值就是我们真正的对象。它就是我们的 IOC 本体。
好的,那么创建对象其实也非常简单。我们首先拿到我们 bean 的 name:beanDefinition.getName()
。
好的,如果我们的 IOC 容器里有这个 name,就说明我们已经创建好了,那么我们就直接把它返回就可以了。如果我们的 IOC 容器里没有这个 name,那么我们就要创建这个对象。
我们可以封装一个函数叫 doCreateBean
。好的,我们把我们的 beanDefinition
传进去。
那么现在我们要写一个 doCreateBean
。好的,我们写一个 doCreateBean
。
这个 doCreateBean
也非常简单:因为我们之前在定义的时候,我们直接可以把我们需要调用的 constructor 拿到,就是我们的构造函数,直接里面有一个 newInstance()
。
好的,那么我们默认先让它只有一个无参的构造函数。然后我们创建出来了一个对象,这个对象就是我们真正的 bean。
我们先把它创建出来,然后我们要把这个 bean 扔到我们的 IOC 容器中。然后我们要通过 ioc.put(beanDefinition.getName(), bean)
。
OK,我们解决一下这个异常,因为我们在这是用方法引用调用的,所以我们的异常不能往上抛。所以我们在这直接捕获一下。
好的,我们把它改成 Exception e
。那我们的代码就写完了。
那么最后就是一个 type 是如何变成 BeanDefinition
的。
那么在这我们应该解析出一个名字。那么这个名字应该是什么呢?其实我们应该在 @Component
里面去增加一个属性:String value() default "";
。他应该有一个默认值,是一个空字符串。如果你不写这个名字,那么我们就默认你是用的你的当前的类名。如果你写了名字,那么我就要用你的名字。
那么在这我们可以判断一下:拿到我们的 declaredAnnotation
,然后把我们的 Component
拿到。因为在这之前我们是已经判断过,所以我们传进来的这个 type 一定是有这个 @Component
注解的,叫 component
。
然后我们在这拿到一个 name。OK,这个 name 等于它的 value()
。isEmpty()
?如果它是空,那么我们就让这个 type.getSimpleName()
。OK,否则就用我们的 component.value()
。
好的,OK,那么名字我们就解析完了。那么在这我们只需要返回这个名字。
最后就是我们的构造函数。我们先默认它有一个无参的构造函数。我们在这去写一个构造函数:type.getConstructor()
—— 无参的构造函数。
OK,我们在这捕获一下异常吧。我们把我们的构造函数返回回去。
OK,这就是我们的 BeanDefinition
的解析过程。
我们回到 main 函数去执行一下我们的 ApplicationContext
。我们拿到我们的 IOC 容器,我们看看我们的 IOC 是否能 getBean
。我们叫 cat
,我们打印一下这个 cat。
OK,是 null。为什么是 null?因为我们还没有实现 getBean
的函数。
那么这些函数也非常简单:
- 通过名字,我们就直接可以从 IOC 容器中拿到就可以了:
return ioc.get(name);
OK,第二个函数我们通过类型去拿到对应的 bean。那么因为我们的 IOC 是通过名字去做 key 的,所以我们没有办法直接拿到对应的类型的 bean。所以我们需要把它遍历出来。
那么在这里我们直接用 ioc.values().stream()
,然后给它一个 filter
。filter 什么呢?我们用我们的这个 bean 的 getClass()
,我们用我们传进来这个参数的 Class.isAssignableFrom(bean.getClass())
。
这个参数是什么意思呢?是我们传进来的 bean 是否可以赋值给我们的目标 bean。如果他匹配了,也就是说这个东西返回的是 true,那就说明我们的这个 bean 的类型是可以转换成我们的参数的。
那么什么样的类型是可以转换的呢?那就是当我们传进来的是一个我们的 bean 实现的接口,或者是我们的 bean 的父类,我们就可以。
那么当我们 filter 了之后,我们只需要返回任何一个匹配的,所以我们这里有一个 findAny()
。
好的,那么我们如果没有拿到,我们就返回一个 null,然后返回回去就行了。
这里有一个强转,因为我们拿到的是一个 object,那么我们返回的是一个 T 类型。那么我们可以在这再 map
一下,把我们的 bean 强转成 T 类型。
OK,这样我们就写完了。在这里我们把它格式化一下。
那么在这里你可能想说:为什么我们不直接用这个类型当 key 呢?我们再存一个 value 不就可以了?
假如说我们有这个 Cat
,我们用这个 Cat
的类型去做 key,那么我们确实是可以通过 Cat
去拿到对应的值。但是如果我们传进来的是一个 Object
,是一个接口,是一个父类,我们就没有办法通过这单一的类型匹配去拿到我们对应的 bean。
所以很多小伙伴在质疑:在 Spring 用类型去拿到 bean 的时候,为什么要把整个 IOC 容器都遍历一遍?这就是你没有实现过就很难理解的一个逻辑——因为我们确实没有办法用这个类型去做 key。
那么接下来这个函数就已经非常简单了。我们直接把它粘过来。我们在这是匹配到任何一个值,那么我们直接在这写一个 toList()
。
好的,那么我们这个函数就写完了。
那么我们再回到我们的 main 函数中,我们看一下我们是否能拿到这个 cat。
还是没有拿到。我们看一下这个 cat 是没有加上我们的注解的,所以我们要加一个 @Component
的注解。因为我们 bean 的默认名字是这个类名,所以我们在这应该是用一个大写。
我们再查一下。好的,这样我们就拿到了一只猫。
假如说我们在这再写一个类叫 Dog
,我们在这加一个 @Component
注解,我们的名字叫 myDog
。
好的,我们再查一下我们是否能拿到这个 myDog
—— Dog
。
好的,我们再执行一下。OK,没问题。
那么我们把最简单的对象创建就完成了。
但是我们的 Spring 还是有很多问题的。比如说当我们的名字重复的时候,我们不会报错。第二点是我们还不支持依赖注入的功能。
那么我们先来解决第一个问题。
我们回到我们的 ApplicationContext
中。我们在创建 BeanDefinition
的时候,也就是这个 wrap
的函数的时候,我们应该创建完了之后去判断一下这个 bean 的名字是否重复。
那么我们在这可以写一个容器,叫 beanDefinitionMap
,等于一个 HashMap
。
我们在这判断一下:如果 beanDefinitionMap
已经包含了这个 beanDefinition.getName()
,那么我们就让它报错。这里可以自定义一个异常,我们就直接抛出一个 RuntimeException("Bean name duplicate: " + name)
。
好的,然后我们再把这个 beanDefinition
放到我们的 beanDefinitionMap
里面。
好的,最后我们把这个 beanDefinition
返回。
这样我们就解决了 bean 名字重复的问题。
接下来我们开始实现自动注入的功能。
比如说我们现在这个 Cat
,它里面有一个注解叫 @Autowired
。他希望注入一个 Dog
,也就是说他希望我们把 Spring 容器中的 Dog
给它注入到这个属性中。
那么我们可以直接创建一个 annotation。
好的,然后我们加入一些元注解:我们让这个 @Autowired
可以放在属性上面,元注解我们可以让它在运行时获取。
那么我们在这个 Cat
里面就可以加一个 @Autowired
。
我们再定义一个注解,这个注解叫 @PostConstruct
。这个注解我们可以把它放在函数上面。这个注解的意思是:如果当这个对象初始化完成之后,我们可以自动帮你调用一次添加了 @PostConstruct
注解的函数。
比如说我们在这写一个函数叫 init
函数,我们在这打印一句话:“cat 创建了,cat 里面有一个属性”,然后我们把我们的 dog 传进来。
OK,现在我们要实现这两个功能:一个是自动注入,一个是生命周期函数。
我们回到我们的主流程中——ApplicationContext
。
我们在 createBean
里面调用了一个 doCreateBean
。我们在创建这个 bean 结束之后,如果他拥有一个 @PostConstruct
函数,那么我们就在这帮他调用一下。
那么我们如何知道这个 bean 里面是否拥有这个 @PostConstruct
函数呢?我们可以在 BeanDefinition
里面去定义。我们可以在这定义一个函数,返回一个 Method
,叫 getPostConstructMethod
。
我们在 ApplicationContext
中可以通过这个函数去拿到它是否拥有一个 @PostConstruct
函数。我们在这里返回一个 Method
。
当然,如果你想支持的话,你这里甚至可以把它返回成一个集合,我们可以支持多个 @PostConstruct
函数。那么我们在这就定义一个 @PostConstruct
函数。
Method
他叫 postConstructMethod
,然后我们把它返回。
好的,我们在解析的时候,我们先拿到 type 的所有的函数,然后 filter
一下。filter 什么呢?这个函数 m 它是否拥有一个注解叫 @PostConstruct
。
OK,如果我们拥有,那么我们就把它返回回去。因为我们这里只支持一个函数,所以我们在这直接一个 findFirst()
就可以了,orElse(null)
。
好的,然后我们让我们的 postConstructMethod
等于它。
OK,我们格式化一下。好的。
然后我们回到我们的主流程中。我们看一下这个 bean 在创建之后,这个 beanDefinition
是否拥有一个 @PostConstruct
函数。如果它不是 null 的话,我们就给它用反射来调用一下 invoke
。invoke 什么呢?invoke 这个 bean,用这个 bean 来调用这个函数。
我们回到我们的 main 函数中,我们把多余的代码都删掉,我们只创建一个我们的容器。
我们看一下我们在 cat 里面定义的这个 @PostConstruct
,我们是否能调用起来。
好的,我们执行一下。没问题,我们在 cat 创建之后,我们可以把 cat 里面定义的 @PostConstruct
函数用反射的方式去把它调用起来。
但是我们自动注入的属性还没有完成。
由于视频时长原因,本期视频就到此为止。下一期视频我将带大家实现自动注入和一些生命周期回调的函数。
感谢你的一键三连。
今天的思考题是留给面试官的:你能告诉我 BeanDefinition
这么简单的东西,你面试总喜欢问这个逼玩意儿干啥?
我们这个视频来实现一下 @Autowired
自动注入的功能。
我们回到我们的 ApplicationContext
中。我们在 doCreateBean
的时候,在创建这个 bean 实例之后,我们需要把它需要自动注入的属性给它注入进去。
那么首先我们就应该知道:什么样的属性是需要自动注入的呢?
我们回到我们的 BeanDefinition
,我们在这可以提供一个属性叫 autowiredFields
。这个属性是什么意思?就是我们需要自动注入的属性。
那么在这里我们只支持属性的自动注入。
那么这个属性我们如何解析呢?其实我们和解析我们的 @PostConstruct
函数一样:我们让这个 type 拿到所有的 field,然后我们 filter 一下。filter 什么呢?filter 我们这个属性上面有一个注解,这个注解叫 @Autowired
。然后我们再把它 toList
,toList
了之后,给我们这个 autowiredFields
的属性赋值。
好的,我们格式化一下。然后我们需要提供一个 get 方法,把这个属性返回回去。
那么我们回到我们的主流程中。我们在这实例化了我们的 bean,那么在实例化之后,我们需要给它自动注入。那么我们可以写一个方法叫 autowireBean
。
那么这个函数我们都需要什么呢?我们首先需要一个 bean 的实例,第二个参数我们需要一个 BeanDefinition
,我们需要知道我们有什么属性是需要自动注入的。
那么我们在这创建一个函数叫 autowireBean
。然后我们在这首先拿到我们的 BeanDefinition
里面的 autowiredFields
。我们在这遍历一下所有的我们遍历出来的属性,就是我们需要自动注入的属性。
那么我们就开始调用反射的 API。首先我们让这个属性是一个可访问的,然后我们需要把这个属性赋值。那么给谁赋值呢?给这个 bean 赋值。给这个 bean 的属性赋什么值呢?我们需要拿到一个 object,这个 object 就是我们准备给它赋值的那个属性,也就是我们的 @Autowired
的 bean。
我们如何获取呢?比如说我们的 Cat
需要这个 Dog
,那么这个 Dog
是从哪来的?自然是从 ApplicationContext
中来的。那么我们如何从这个 IOC 容器中拿到这个 Dog
呢?我们可以 getBean
一个 Dog
。
那么我的问题是:如果这个时候 Dog
还没有初始化,那么我们自然拿到的就是一个 null。
所以我们要修改一下我们 getBean
的函数。
我们回到我们的 getBean
这个函数中。比如说我们通过一个名字拿到一个 bean,如果这个 IOC 容器中有,那么万事大吉,我们直接把它返回就好了。但是如果这个容器中没有,我们是否应该判断到底是这个容器中真的没有,还是说我们还没有加载到我们想要的这个 bean 呢?
那么我们重写一下这个函数。我们先把这个 bean 接收到 bean,如果这个 bean 不是一个 null,那么我们就直接把它返回就可以了。那么如果这个 bean 是 null,我们就需要判断到底是这个名字真的没有,还是说我们还没有加载到。
那么我们如何判断这个名字有没有呢?那么我们只要看一下我们是否有这个名字的 BeanDefinition
就可以了。
但是我们回到我们的主流程看一下:我们的主流程是我们用流的方式去一个一个地加载这个 BeanDefinition
。所以我们要在这把这个流程重新写一下。
我们不能在每一次遍历的时候都把它加载出一个 BeanDefinition
,然后把它 createBean
。我们应该让所有的 BeanDefinition
都初始化完成之后,然后我们再把所有的 BeanDefinition
依次地 createBean
。
那么我们重新写一下这些函数。我们在这可以直接把它 forEach
。这个函数的意思是我们扫描了之后,把所有的 BeanDefinition
放在了我们的 beanDefinitionMap
里面。然后我们可以把 beanDefinitionMap
它的 values 就是我们所有的 BeanDefinition
,它点 forEach
,然后 createBean
。这样的话我们就实现了先把所有的 BeanDefinition
加载,然后再初始化我们的 bean。
那么我们回到我们的 getBean
函数中。在这我们就可以判断:如果我们的 beanDefinitionMap
—— BeanDefinitionMap
—— 如果我们有这个 key,那么我们就需要返回一个 createBean
,然后把我们的 beanDefinitionMap
里面的 BeanDefinition
传进去。
我们看一下为什么报错?因为我们的 doCreateBean
是没有返回值的。我们在这把它创建一个返回值:如果 IOC 容器中有这个 name,我们就直接把 IOC 中的这个 name 返回回去就可以了;否则我们就返回这个 doCreateBean
。
那么 doCreateBean
我们也应该返回一个 object。前面的流程是我们创建 bean 的过程,那么最后我们只需要把这个 bean 返回就可以了。
前面的函数我们就写完了。我们重新回到我们的 getBean
里面:如果这个 BeanDefinitionMap
里面有这个名字,那说明我们还没有创建这个对象,我们就把它创建出来返回回去就可以了。如果这个 BeanDefinitionMap
里面没有的话,我们就直接返回一个 null,那么就说明你的 getBean
是确实我们的容器里没有的。
那么同样的,当我们用类型去查找某个 bean 的时候,其实我们本身就需要遍历这个 IOC 容器。那么其实我们是可以遍历这个 BeanDefinition
的,然后我们通过遍历这个 BeanDefinition
拿到这个 bean 的名字,然后再交给我们的方法重载,让这个通过名字的 getBean
去真正拿到我们的目标 bean。
那么在这我们重新写一下我们的逻辑。我们用 beanDefinitionMap
,我们拿到所有的 BeanDefinition
,然后 filter 一下。filter 什么呢?filter 就是这个东西是一个 BD。
那么我们的 BeanDefinition
本身是没有这个 class 的。那么所以我们在 BeanDefinition
里面,我们就需要提供一下我们传进来的这个 type。那么我们在这可以写一个属性叫 beanType
,它是一个 Class<?> beanType
。然后我们在构造函数的时候,我们直接把传进来的这个 type 交给我们的 beanType
。
这个属性我们这个单词拼错了,我们重新修改一下 autowired
。然后我们需要提供一个 get 方法。
好的,我们回到我们的 ApplicationContext
中,我们直接拿到我们的 BD 的 getBeanType
去替换我们之前的逻辑。在这我们过滤出了一个 BeanDefinition
,然后我们直接把它映射成 BeanDefinition
的名字 —— BeanDefinition.getName()
。
现在我们拿到了这个 BeanDefinition
的名字,然后我们 findFirst
。我们拿到的是一个 Optional
,因为我们是可能拿不到的,所以这里调用一个 orElse(null)
。
好的,我们现在拿到的是一个 bean 的名字 —— beanName
。好的,下面就都不要了。那么我们用名字去判断的时候,我们可以再判断一个空:如果我们传进来的名字等于 null,那么我们就直接返回一个 null 就可以了。
然后我们拿到这个 beanName
,我们就返回 getBean
,然后把这个 beanName
传给他。我们用方法重载的方式,然后我们在这强转成 T。
好的,然后我们格式化一下我们的代码:Stream.filter().map().findFirst().orElse()
。
OK,我们的 getBean
通过类型拿到 bean 的方式就写完了。
那么我们再重新写我们这个 getBeans
(复数形式)。其实它也是同样的道理:我们还是遍历我们的 beanDefinitionMap
。把这段代码粘过来。我们过滤出所有类型符合我们传进来的类型的 BeanDefinition
。
在这我们拿到了所有的 beanName
,然后我们把它映射成我们的对象。那么这个对象是如何拿到呢?我们直接 this.getBean()
好了。我们把它映射成了我们的 getBean
对象,然后我们把它强转成 T。那么这个 bean 就强转成 T 的 bean。
OK,然后最后再 toList
,这就是我们拿到的所有的 getBeans
,然后我们把它返回回去。
好的,这就是我们重写的这三个 getBean
的函数。
好的,我们的 getBean
函数就重写完了。
那么我们回到我们的 autowireBean
里面。我们如何拿到我们要被赋值的 bean 呢?我们就可以直接 getBean
出来就可以了。
那么现在的问题是:我们要拿到什么对象?我们是通过类型来获取,还是通过名称来获取呢?
我们这里先规定我们只通过类型来获取。我们可以直接拿到我们要赋值的这个属性的类型,叫 getType()
。我们把这个类型去交给我们的 IOC 容器,让它 getBean
出来一个对象。
好的,然后我们在这抛一个异常。
那么这样我们就完成了我们的 autowireBean
。
我们回到我们的 main 函数中,我们去看一下我们的自动注入是否能完成。那我们执行一下我们的逻辑。
OK,我们的 cat 里面确实是拿到了。我们看一下我们 cat 里面,我们在 init 这个函数里面打印了我们被自动注入的这个 dog。那说明我们的自动注入是完成了。我们可以把容器中的 dog 去注入给我们的这个 cat。
但是现在我们的容器依然是不完善的。为什么呢?假如说我们在 dog 里面再引用了我们的 cat。
我们再次启动一下我们的容器,我们会发现我们的栈被撑爆了。为什么呢?
我们打一个断点看一下。我们在 doCreateBean
的时候,我们打一个断点。
好的,我们现在拿到了一个 BD,我们创建了一个 cat。然后这个 cat 在 autowireBean
的时候,我们进去。我们在创建一个猫的时候,我们发现我们要一个狗,于是我们向容器中要这只狗。
那我们进到这个 getBean
里面,然后我们发现我们最后拿到了一个狗的名字叫 myDog
。然后我们再次进入这个 getBean
,然后会再次调用这个 createBean
。然后我们在调用这个 createBean
的时候,然后发现我们拿到的这个狗还没有初始化,那么我们就继续往下走,在创建这只狗。
那么在创建这只狗的时候,我们初始化了这个狗没有问题,然后我们要给这个狗进行 autowire
,然后发现我们要有一只猫。那么这只猫还在初始化的过程中,所以我们容器中还没有这只猫。所以我们就变成了一个死循环:就是猫需要狗,狗又需要猫。
那么我们如何解决这个问题呢?
我们先把断点停掉。我们回到我们的流程中。我们看一下我们 doCreateBean
的时候,我们在初始化之后,我们并没有直接把这个 bean 放到这个容器中。我们只有完成这个 bean 所有的创建生命周期之后,我们才把这个 bean 放在我们真正的 IOC 容器中。
那么我们是否可以修改一下这个逻辑?如果我们有一个集合,我们这个集合放着的是还没有初始化完成的 bean。那么我们在 autowire
的时候,我们只需要看一下这个没有完成的 bean 有没有。如果有的话,我们也直接把它拿到。然后当我们初始化完成之后,我们就把它从没有完成的 bean 的容器中移除,然后放在我们真正的 IOC 容器中。
我们先写一个容器叫 loadingIoc
。容器中装着的就是还没有初始化完成的对象。
那么我们需要改一些逻辑。我们在 createBean
的时候,我们判断 IOC 容器是否有这个对象。如果没有的话,我们就创建这个对象。我们这里还要判断一下:如果 loadingIoc
中没有我们的 name,那么我们才真正创建这个 bean。
那么我们直接复制这行代码,我们把 loadingIoc
写在这。
好的,这样的话我们前面就做了两层缓存。
我们再把 bean 实例化之后,我们就应该把它放到我们的 loadingIoc
中。也就是说 loadingIoc.put()
,put 什么呢?put beanDefinition
的名字,然后把我们的 bean 放进去。这就是还没有完成、正在加载的 bean。
那么当我们加载结束之后,也就是说在这,我们就不应该单单地把这个 bean 放到这个 IOC 容器里,而是应该我们从 loadingIoc
中把我们正在初始化的这个 bean 拿出来。也就是说我们可以直接把它 remove
掉,remove
我们的 beanDefinition.getName()
。而 remove
出来的返回值,就是我们还没有初始化的这个 bean。
那么我们先格式化一下代码,然后我们再跑一次,我们看看我们的程序能否正常地执行。
OK,正常的结束了。
那么我们再给 dog 再增加一个 @PostConstruct
函数,然后我们打印一句话:“dog 创建完成了,里面有一只 cat”,然后加上我们的 cat。
好的,我们再重新打印一下。虽然这两个类是循环依赖的,但是我们仍然解决了这个问题。
然后,那么我们再看一下:如果他自己引用了自己,我们是否能成功地引用到?我们在这里面有一只狗。好的,也没问题。这两个狗是一样的,那说明我们已经成功地解决了循环依赖的问题。
那么完成了自动注入的功能,我们就再增加一个生命周期的函数。
我们回到我们的主流程中,这就是我们的 createBean
。它分为:实例化这个 bean、autowire
这个 bean,后面就是一个调用 @PostConstruct
函数的逻辑。
那么我们可以把它封装成一个函数叫 initializeBean
。
好的,然后我们把 bean 和 BeanDefinition
传进去。那么我们把这段逻辑放在我们的 initializeBean
里面。好的,然后我们把异常都抛出去。
那现在我们要定义一个接口,这个接口叫 BeanPostProcessor
。这也是 Spring 一个经典的接口,叫 BeanPostProcessor
。
这个接口有两个函数:一个叫 postProcessBeforeInitialization
,一个叫 postProcessAfterInitialization
。然后这两个函数有两个参数:一个是当前的 bean,一个是当前的 bean name。这个返回值也是一个 object。
这两个函数的意思是:我们可以通过实现 BeanPostProcessor
,我们对初始化的一个 bean 进行干预,并且我们可以把我们拿到的进行初始化之后的 bean 进行重置。
那么我们回到我们的 ApplicationContext
中。我们在初始化 bean 的前和后,我们可以分别地调用 BeanPostProcessor
的两个函数。
那么我们首先要拿到一个 BeanPostProcessor
。我们在这儿可以写一个容器:List<BeanPostProcessor> beanPostProcessors
。我们先不管这个 list 里面的东西是哪来的,我们先拿到它。
然后我们在初始化 bean 的前和后,我们去调用它的函数:defaultBeforeInitializeBean
,然后我们把 bean 和 beanDefinition.getName()
传给他。好的,那么因为它是有一个返回值的,我们可以把 bean 进行一个重新赋值。那么我们就把 bean 传给他。这也是一个经典的责任链模式。
那么同理,在我们的初始化之后,我们仍然可以调用它的 afterInitializeBean
。好的,那么在这我们可能把这个 bean 已经重新赋值了。那么我们这个函数应该把我们重新赋值的 bean 返回回去,是一个 object。然后我们把最后的这个 bean 返回回去。
OK,那么我们在这我们要接收到这个 bean。那么我们这个 bean 就等于它。
那么在这我们要注意:我们初始化的这个 bean 和我们最后拿到的这个 bean 可能不是一个 bean 了,因为我们可能通过 BeanPostProcessor
对它进行干预,而在中间进行了重新赋值。
所以我们的这句话就要重新写:我们要把我们最开始初始化的 bean 从我们的 loadingIoc
中移除,然后我们要把我们最后的这个 bean 放在我们的真正的 IOC 容器中。
现在的问题是:我们怎么拿到我们所有的 BeanPostProcessor
?
我们可以回到我们刷新容器的地方,也就是 refreshContext
里面。我们在这一行代码中,我们把所有的 BeanDefinition
都已经成功地放在了我们的 beanDefinitionMap
里面。那么我们就可以在这写一个方法叫 initBeanPostProcessors
。
好的,然后我们写这么一个函数。那么我们可以遍历 beanDefinitionMap
它的 values,filter 一下。filter 什么呢?我们要判断我们的 BeanDefinition
它是否是一个 BeanPostProcessor
。然后我们直接拿到 BeanDefinition
,然后我们看 BeanPostProcessor.class.isAssignableFrom(bd.getBeanType())
。如果我们这个函数成立了,那么我们就拿到了所有的 BeanPostProcessor
的 BeanDefinition
。
然后我们把它 map 一下,map 什么呢?map 我们的 createBean
。我们让所有的 BeanDefinition
去创建 bean。然后我们再 map 一下,因为我们拿到的是一个 object,所以我们要把它强转成 BeanPostProcessor
。然后我们再把它 forEach
,forEach
什么呢?我们要把它放在我们的 postProcessors
这个容器中,.add()
。
好的,那么这一行我们把它重新写一下。
好的,我们格式化一下。OK,那么这个函数我们就写完了。
我们在这我们定义一个 MyBeanPostProcessor
,我们实现我们的接口叫 BeanPostProcessor
。然后我们把它加上一个 @Component
。然后我们实现两个函数。
那么这两个函数,我们不是所有的 BeanPostProcessor
都需要实现的,所以我们可以直接给它增加一个默认的实现,我们让它直接返回我们的 bean 就可以了。那么这个也是同样的逻辑。
好的,那么我们的这个 BeanPostProcessor
我们有一个功能:我们在创建完成之后,我们想打印一下我们这个 bean 的名字。那么我们就在这只需要打印一下 beanName
,然后加上“初始化完成”。好的,然后我们再把这个 bean 返回回去。
那么我们回到我们的 main 函数中,我们再创建一下我们的容器。
好的,我们的 BeanPostProcessor
也生效了。那么先是创建了我们的狗,然后创建了我们的猫。
OK,这就是我们实现的 Spring Framework。
我们来重新梳理一下我们 Spring Framework 的流程:
首先我们拿到了一个包名,然后我们扫描这个包名下面所有的 class。然后我们根据一些规则去判断什么样的类是我们应该加载的。然后我们把这些类去重新封装成一个我们的 BeanDefinition
。然后去初始化我们 Spring 特殊的类,然后再把所有的类都初始化。
然后在真正初始化,也就是说这个 getBean
的时候,真正我们创建对象是用这个 doCreateBean
。然后下面就是我们创建类的这些流程:首先我们要把它进行一个实例化,然后我们要把对应的自动注入的属性加载进去,最后也就是一个初始化完成之后的 bean。
然后在这个初始化 bean 的过程中,我们调用了一些生命周期的函数。
这就是我们整个的初始化流程。
其实你应该能想到:因为我们整个的流程已经定义好了,那么在任何一个地方去加一些我们的功能都是可以随便添加的。比如说我们可以添加一个事件,在 bean 实例化之后,我们可以回调这个事件。那么我们就在这再加一行代码就可以了。之后的逻辑和我们的 BeanPostProcessor
是一模一样的。
只要你心中有这个主流程,那么一切的拓展都是主流程的额外分支。
这期视频就到这。下期视频我将使用我们自己手写的 Spring Framework 实现一个 Spring MVC,手把手教你实现真正的 Web 服务器。
今天的思考题:Spring @Autowired
的注解有一个属性,它是干嘛的?你可以在我们的项目中实现这个属性的功能吗?
Comments NOTHING