Spring 手动实现Spring底层机制
【摘要】 Spring 第六节 手动实现Spring底层机制 万字详解!
目录
3.自定义用于封装Bean信息的BeanDefinition类:
一、前言
- 第六节内容,我们一起来手动实现一下Spring的底层机制,这有助于加深我们对Spring的理解;包括手动实现Spring容器结构,手动实现IOC依赖注入,手动实现Bean的后置处理器机制,以及手动实现AOP机制等(PS : AOP 底层是基于 BeanPostProcessor 机制的)。
- 注意事项——①代码中的注释也很重要;②不要眼高手低,自己跟着敲一遍才真正有收获;③点击文章的侧边栏目录或者文章开头的目录可以进行跳转。
- PS : up会博文中会展示出所有的源代码,不过考虑到有朋友们想要完整源代码包的需求,up会将代码上传到CSDN 和 Github,没会员可以 。
- 良工不示人以朴,up所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!
二、Spring底层整体架构
1.准备工作 :
在IDEA中搭建Maven项目,原型Archetype选择“org.apache.maven.archetypes:maven-archetype-webapp”,建立Maven项目后,在pom.xml配置文件中引入如下依赖 :
建立java, resources, test包,并右键通过 “Mark Directory As” 进行标记,如下图所示:
接着,up在java包下,建了OrderDAO, OrderService, OrderServlet这几个类,如下图所示 :
OrderDAO类代码如下 :
OrderService类代码如下 :
OrderServlet类代码如下 :
然后,up在resources目录下建立一个beans.xml配置文件,代码如下 :
PS : 在IDEA中,如果是Java项目,beans.xml文件要放在src目录下;而在Maven项目中,需要放在resources目录下。
接着,我们新建一个spring.test包,并在该包下新建一个类AppMain,如下图所示 :
AppMain类代码如下 :
运行结果 :
可以看到,在默认情况下,我们配置的@Component, @Controller, @Service, @Repository组件都是单例的。
成功运行后,我们可以看到实际的工作目录target/classes下,可以找到beans.xml文件,如下图所示 :
2.架构分析 : (重要)
Spring整体底层架构如下图所示 : (从上至下,从左到右)
3.环境搭建 :
在上文“准备工作”创建的Maven项目的基础上,新建一个并行的模块Module(也可以不这么做, whatever),选择同样的Maven原型,只不过Parent为None,然后引入同样的maven依赖,并创建beans.xml配置文件。如下图所示 :
创建这个新的模块的目的是为了有一个干净的环境,不会被之前创建的包干扰(当然,你就在原来那个模块下写也是可以的)。
接着,我们右键选择“Open Module Settings”,把两个模块的Language level都修改为17(注意:修改pom.xml配置文件中的依赖,会引起此处Language level的变化,需要再手动修改回来),如下图所示 :
另外,在"Settings-->Build,Execution,Deployment-->Compiler-->Java Compiler"中,将右侧的version也改成17,如下图所示 :
三、手动实现Spring容器结构
1.自定义注解 :
1.1 @Component注解
在annotation包下自定义一个Component注解,用于模拟Spring原生的"@Component"注解,该注解可以标注一个类为Bean组件。代码如下 :
1.2 @Scope注解
在annotation包下自定义一个Scope注解,用于模拟Spring原生的"@Scope"注解,该注解可以标注一个Bean为单例还是多例(singleton OR prototype)。代码如下 :
2.自定义组件 :
在component包下创建OrderService和OrderDAO两个类,表示待注入的组件,并用我们自定义的@Component注解和自定义的@Scope注解进行标注。(假设OrderService为单例singleton,而OrderDAO为多例prototype)
OrderService类代码如下 :
OrderDAO类代码如下 :
假设com.cyan.spring.component包就是我们要扫描的包,beans.xml配置文件代码如下 :
3.自定义用于封装Bean信息的BeanDefinition类:
通过上文“Spring底层架构分析图”我们可以得知——通过Dom4J解析beans.xml后,我们不能直接将获取到的Bean信息封装到Map容器中,而是应该先将需要的Bean的信息封装在一个BeanDefinition对象中,再将该BeanDefinition对象保存到IOC容器维护的Map容器中。BeanDefinition类代码如下 :
4.自定义IOC容器 :
在自定义的IOC容器中,首先我们肯定得获取到被扫描的包的信息,这里up使用Dom4j来解析target/classes目录下的beans.xml配置文件,通过DOM操作来获取到被扫描的包,这一点和我们在“ ”一文中手动实现Spring注解配置机制有所不同,但我们在“ ”中已经有过“使用DOM4j解析XML配置文件”的经验了,不熟悉的朋友可以先去快速看一下up之前写的源码。
得到被扫描的包后,后续步骤同我们之前写的手动实现Springt注解配置机制类似,即遍历该包下的.class文件,然后得到每个资源的Class对象。我们需要对每个Class对象进行判断,如果它是一个Bean类型,就要将Bean的配置信息封装到BeanDefinition对象中,并将BeanDefinition对象保存在IOC容器维护的Map容器中。这是初始化IOC容器的第一步,beanDefinitionMap初始化完毕后,还需要初始化单例池singletonObjects。
记住,beanDefinitionMap的初始化是单例池初始化的前提,因为单例池需要根据beanDefinitionMap中的信息来创建Bean实例。当然,基于OOP的思想,“初始化beanDefinitionMap”,“初始化singletonObjects”,以及“反射创建Bean实例”的代码我们都可以分别封装在方法中,然后在自定义的IOC容器的构造器中调用需要的方法即可。
当初始化IOC容器的任务完成后,我们就需要定义自己的getBean(String id)方法,正如我们在上文“Spring整体底层架构分析图”中描绘的那样,我们在getBean(..)方法中需要先判断传入的beanName是否是beanDefinitionMap中的一个key。若不存在,可以考虑直接throw一个NullPointerException异常对象;若存在,继续判断它是单例还是多例,如果是singleton单例,就直接从单例池中获取实例,并返回,如果是prototype多例,就调用已经封装好的createBean(BeanDefinition beanDefinition)方法来反射创建新的对象,并返回。
CyanApplicationContext类代码如下 :
5.运行测试 :
在test包下新建一个AppMain类用于测试,如下图所示 :
在AppMain类中我们通过getBean方法分别获取3次OrderService实例和3次OrderDAO实例,由于我们配置的@Scope中,OrderService为单例,OrderDAO为多例,所以预期结果应该是——3次获取到的OrderService实例是同一个Bean对象,而3次获取到的OrderDAO实例是三个不同的Bean对象。
AppMain类代码如下 :
运行结果 :
四、手动实现Spring依赖注入
1.准备工作 :
首先,回顾一下,我们曾在“
自定义的@Autowired注解类代码如下 :
为了验证我们自定义的@Autowired注解成功实现自动装配,我们在OrderDAO类中定义一个方法,OrderDAO类代码如下 :
同样地,OrderService类中要维护有一个OrderDAO类型的属性,该属性使用自定义的@Autowired注解修饰;在OrderService类中也定义一个save()方法,通过orderDAO属性调用。代码如下 :
2.代码实现 :
其实我们在上文“自定义IOC容器”中,已经实现了依赖注入,就在我们封装好的createBean(BeanDefinition beanDefinition)方法中,需要注意一点,由于OrderService类维护的OrderDAO属性是被private关键字修饰的,即是私有的,所以我们在装配前,一定要先调用setAccessible(true)方法实现暴力反射。如下图所示 :
3.运行测试 :
在测试类AppMain中,我们获取OrderService实例,并调用save()方法,检测OrderDAO实例的save()方法是否被调用,若成功调用,则说明“通过自动装配实现依赖注入”实现成功。AppMain类代码如下 :
运行结果 :
五、手动实现Spring后置处理器
1.实现思路 :
我们先来简单回顾一下原生Spring的后置处理器,那么要回顾后置处理器,就必须先回顾一下Bean的生命周期,如下图所示 :
我们知道,在原生Spring框架中,后置处理器的本质是一个实现了BeanPostProcessor接口的Java类,而这个实现类中重写了BeanPostProcessor接口中的两个方法,分别为postProcessBeforeInitialization(...) 和 postProcessAfterInitialization(...)。这两个方法允许该后置处理器在Bean的初始化方法(即配置Bean时init-method属性所指定的初始化方法)调用前 和 调用后进行相应的业务处理。
那么,既然我们想自己模拟实现Spring的后置处理器机制,首先就要解决一个问题——初始化Bean的方法从哪儿来?
因为在原生Spring框架中,我们是可以直接在beans.xml配置文件中为Bean指定初始化方法的,但是现在我们的beans.xml配置文件仅仅起到“指明扫描包”的作用,因为我们是手动模拟呀,当然要自己想办法弄一个Bean的初始化方法。诶,我们发现在原生Spring框架中,有这么一个接口,叫InitializingBean,如下图所示 :
可以看到,接口中有个“afterPropertiesSet()”方法,嗯?这个方法不就是“在属性注入之后”的意思吗?噢~,想想我们的Bean的生命周期,属性注入之后,那不就是Bean的初始化方法嘛。所以,可以想到,如果我们能自己模拟一个InitializingBean接口,岂不是可以利用afterPropertiesSet()方法来充当Bean的初始化方法?
噢,这么一来,我们只需要让JavaBean去实现模拟的InitializingBean接口,并重写afterPropertiesSet()方法,Bean实例便有了自己的初始化方法。
那儿,有了初始化方法后,我们是不是得考虑,上哪儿去调用这个初始化方法呢?嗯?那当然是我们之前定义的IOC容器类CyanApplicationContext了。在我们自定义的容器类中,我们不是封装了一个createBean(BeanDefinition beanDefinition)方法吗,在这个方法中我们反射创建了Bean实例,并且实现了依赖注入。那我们可以在创建好Bean实例后,判断是否需要调用初始化Bean的方法。(PS : 在容器中,我们常常根据一个类是否实现了某个接口,来判断是否要执行相应的某个业务逻辑,这其实就是Java接口编程的实际运用。实际开发中,我们有时会让类去实现一个什么方法都没有定义的接口,称为“标记接口”,其实就是利用接口编程实现业务。)
这还没完,要知道,原生的Spring的后置处理器是要在Bean的初始化方法调用前和调用后,分别调用postProcessBeforeInitialization(...) 和 postProcessAfterInitialization(...)方法。那么到现在为止,我们仅仅是解决了Bean的初始化方法的问题,剩下的才是重头戏。我们还需要自定义一个BeanPostProcessor接口,并在其中定义postProcessBeforeInitialization(...) 和 postProcessAfterInitialization(...)方法,从而让接口的实现类(自定义的后置处理器)去重写这两个方法。显然,接口的实现类需要用@Component注解修饰,表明这是一个组件,即Bean的后置处理器;它会和其他单例Bean一样,在容器启动时被实例化并放入到单例池中保存。So,我们只需要设法从单例池中取出所有的后置处理器对象,并在createBean方法的合适位置调用接口中的方法即可。
2.代码实现 :
首先,up新建一个processor包,并在改包下定义一个自己的InitializingBean接口,如下图所示 :
InitializingBean接口代码如下 :
接着,我们让OrderService 去实现这个自定义接口,并重写afterPropertiesSet()方法。(此处仅让OrderService去实现接口,是为了在IOC容器中进行判断测试,即判断Bean是否定义了初始化方法。)
OrderService类代码如下 :
在IOC容器类中,我们要在之前封装好的createBean(..)方法中进行判断,若当前Bean定义了初始化方法,就进行调用,如下图所示 :
继续,仍是在processor包下,自定义BeanPostProcessor接口,代码如下 :
接着,在component包下,定义自己的后置处理器实现类,CyanBeanPostProcessor类代码如下 : (要注意后置处理器也是一个组件)
继续,我们在IOC容器类中定义一个方法,用于取出单例池中所有的后置处理器对象,并返回,代码如下 :
有了这个方法,我们便可以在createBean(..)方法中去调用后置处理器对象,createBean(..)方法代码如下 : (注意,相比之前,形参增加了String beanName)
3.运行测试 :
我们在测试类中AppMain中只保留启动容器的代码,如下图所示 :
运行结果 :
可以看到,在OrderService的初始化方法调用前和调用后,分别调用了Bean的后置处理器的postProcessBeforeInitialization方法和postProcessAfterInitialization方法。需要注意的是,我们之前配置的OrderDAO是"prototype"多例,按道理讲,它在容器初始化的时候,根本就不会进入单例池中,而且我们也没有在测试类中调用getBean方法,那为什么运行结果会显示后置处理器也作用于OrderDAO呢?
其实,这是因为我们上文已经实现的“依赖注入”,即下图所示 :
由于OrderService的属性使用了@Autowired注解修饰,所以此处会进行Bean的自动装配,从而间接调用了getBean方法,而又因为OrderDAO是多例prototype,所以在getBean方法中又调用了createBean方法,从而触发了Bean的后置处理器。
六、手动实现SpringAOP机制
1.准备工作 :
首先,在annotation包下,我们当然需要定义用于标记AOP切面类的@Aspect注解,代码如下 :
其次,还需要定义用于实现各种通知的注解,up这里只定义了@Before注解 和 @AfterReturning注解,用于实现前置通知 和 返回通知。@Before注解代码如下 :
@AfterReturning注解代码如下 :
在component包下,新建Calculator接口,以及一个它的实现类Calculator_Demo1。
Calculator接口代码如下 :
Calculator_Demo1类代码如下 : (注意,实现类是一个组件,因此需要用@Component注解修饰)
2.代码实现 :
定义切面类CalculatorAspect,用我们自定义的@Aspect注解和@Component注解修饰,并在其中定义用于前置通知和返回通知的方法,代码如下 :
之前我们说,"Bean的后置处理器"机制是"AOP"机制的底层支撑,现在我们就将深刻体会这一点。我们需要在CyanBeanPostProcessor的postProcessAfterInitialization(..)方法中做点手脚。
首先,如果判断当前Bean是一个切面类,我们就必须获取到该切面类中定义的各种通知方法以及需要被切入的方法(即配置的切入点表达式),并且我们还得把这种映射关系保存到一个Map集合中,保存到Map集合中有什么用呢?诶,别急。
如果判断当前Bean是一个需要被代理的对象,就利用JDK的Proxy动态代理,返回一个代理对象,注意,在匿名内部重写的invoke方法中,我们便可以利用之前已经保存的Map集合,来反射调用前置通知的方法和返回通知的方法。
CyanBeanPostProcessor类代码如下 :
3.运行测试 :
在测试类AppMain中,我们获取Calculator_Demo1对象,打印出它的运行类型,查看是否成功获得代理对象;接着,通过获取的代理对象调用 add方法,查看切面类配置的前置通知和返回通知是否切入成功。AppMain类代码如下 :
运行结果 :
可以看到,动态代理 + AOP 成功实现。
七、总结
- 🆗,以上就是Spring系列博文第六小节的全部内容了。
- 总结一下,最重要的就是“Spring 底层架构”的那张图,理解了那张图之后,实现Spring底层容器结构便不是难事;实现依赖注入也就是在IOC容器类上做做手脚。当然,Bean后置处理器机制的实现也很重要,因为它直接作为AOP机制的底层支撑。总体来说,代码量较大,结合注释来看会好一点,但需要亲手去敲,亲手反复地去敲。
- 下一节内容——Spring jdbcTemplate,我们不见不散😆。感谢阅读!
System.out.println("END-----------------------------------------------------");
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)