Spring的轻量级实现

本文是参考公众号:码农翻身 的从零开始造Spring 教程的学习笔记

源码

github

开发方法

  1. 写一个测试用例
  2. 运行:失败
  3. 写Just enough的代码,让测试通过
  4. 重构代码保持测试通过,

然后循环往复。

说明

  • 仅实现核心功能
  • 基于spring-framework-3.2.18.RELEASE版本
  • 日志和异常处理待完善

Step1 通过XML实例化一个对象

解析XML文件,拿到Bean的id和完整路径,通过反射方式实例化一个对象。

tag: step1

Step2 基础工作

  • 日志支持:log4j2 + SLF4j
  • 异常处理

所有异常的顶层:BeansException

Step3 封装BeanDefinition

DefaultBeanFactory中的BEAN_MAP目前只包括了beanClassName信息,后续如果要扩展其他的信息,肯定需要增加字段,所以我们需要抽象出一个接口,方便后续扩展其他的字段。

Step4 封装Resource

在BeanFactory初始化的时候,传入的是XML格式的配置信息,比如bean-v1.xml, Spring会把这个抽象成一个Resource,常见Resource有
FileSystemResource->从文件地址读配置
ClassPathResource->从classpath下读配置
BeanFactory在创建Bean的时候,只关注Resource即可。

tag: step-4-2-resource

Step5 封装XML的解析逻辑到专门的一个类中

XmlBeanDefinitionReader
用于解析XML,传入Resource,即可获取所有BeanDefinition
由于要把BeanDefinition放入BEAN_MAP中,所以XmlBeanDefinitionReader需要持有一个DefaultBeanFactory,且DefaultBeanFactory需要有注册BeanDefinition和获取BeanDefintion的能力,这样DefaultBeanFactory的职责就不单一了,所以需要抽象出一个BeanDefinitionRegistry,这个BeanDefinitionRegistry负责注册BeanDefinition和获取BeanDefintion的能力,XmlBeanDefinitionReader持有BeanDefinitionRegistry即可将解析生成的BeanDefinition注入BEAN_MAP中。

修改BeanFactoryV1Test这个测试用例,重新执行测试

tag: vstep5-final

Step6 单例多例模式的配置实现

测试:BeanFactoryV1Test:testSingletonBean,testGetPrototypeBean

解析XML的XmlBeanDefinitionReader需要增加scope的解析逻辑
BeanDefinition这个数据结构增加是否单例,是否多例的属性
DefaultBeanFactory中getBean的时候,判断是否单例,如果是单例,则复用对象,如果是多例,则new新的对象。
抽象SingletonBeanRegistry这个接口,用于注册和获取单例对象
DefaultSingletonBeanRegistry实现这个接口

tag:vstep6-scope

Step7 整合,抽象ApplicationContext

我们使用Spring的时候,一般是这样做的:

ApplicationContext ctx = new ClassPathXmlApplicationContext("mycontainer.xml");
UserService userService = (UserService) ctx.getBean("userService");

ApplicationContext ctx = new FileSystemApplicationContext("src\\test\\resources\\bean-v1.xml");
UserService userService = (UserService) ctx.getBean("userService");

现在,我们需要抽象出ApplicationContext这个类来实现如上的功能
ApplicationContext

  • ClassPathXmlApplicationContext(从classpath中读配置文件)
  • FileSystemApplicationContext(从文件目录中读取配置文件)
  • ....

tag: vstep7-applicationcontext-v1

通过观察发现,ClassPathXmlApplicationContext和FileSystemApplicationContext大部分代码都是相同的,只有在获取Resource的时候,方法不一样,所以,我们通过模板方法这个设计模式,来抽象出公有方法到一个抽象类,所有ClassPathXmlApplicationContext和FileSystemApplicationContext都实现这个抽象类。

tag: vstep7-applicationcontext-v2

Step8 注入Bean和字符串常量

形如:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="org.spring.service.v2.UserService">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <property name="owner" value="test"/>
        <property name="version" value="2"/>
        <property name="checked" value="on"/>
    </bean>

    <bean id="accountDao" class="org.spring.dao.v2.AccountDao">
    </bean>
    <bean id="itemDao" class="org.spring.dao.v2.ItemDao">
    </bean>
</beans>

达到的目的就是,可以把“整型,字符串类型,简单对象类型”注入到一个Bean中。
这里我们需要解决以下几个问题:

  1. 把字符串转成各种各样的Value
    1. String->Integer/int
    2. String->Boolean/boolean
    3. ......

注:以上转换器的实现都是基于jdk中java.bean包中的PropertyEditorSupport这个类来完成的。

  • CustomBooleanEditor
  • CustomNumberEditor
  • ....

每种类型的转换都通过类似的方式实现,然后Spring抽象出了TypeConvert这个接口,并把这些转换器加入一个特定的Map中,Map的key就是要转换的目标的类型,Value就是对应的转换器的实现类,即可实现类型转换。

  1. 调用Bean的setXXX方法把这些Value值set到目标Bean中
    1. 抽象出PropertyValue
    2. BeanDefiniton需要增加方法获取PropertyValue
    3. GenericBeanDefinition中需要增加List
    4. XmlBeanDefinitionReader解析XML的时候,把List识别出来并加入BeanDefinition中(RuntimeBeanReference,TypedStringValue)
    5. BeanDefinitionValueResolver 把对应的PropertyValue给初始化好
    6. **setXXX的背后实现利用的是jdk原生:java.beans.Introspector 来实现 **见(DefaultBeanFactory的populateBean方法)

tag: vstep8-inject

Step9 构造器注入

处理形如以下的配置:

<bean id="userService" class="org.spring.service.v3.UserService">
        <constructor-arg ref="accountDao"/>
        <constructor-arg ref="itemDao"/>
        <constructor-arg value="1"/>
    </bean>

    <bean id="accountDao" class="org.spring.dao.v3.AccountDao">
    </bean>
    <bean id="itemDao" class="org.spring.dao.v3.ItemDao">
    </bean>

参考测试:
ApplicationContextTestV3,在v3版本的UserService中,定义两个构造方法,需要识别出正确的构造方法。

  1. 和Step8中类似,抽象出ConstructorArgument用于表示一个构造函数信息,每个BeanDefinition中持有这个对象
  2. XmlBeanDefinitionReader需要负责解析出ConstuctorArgument(parseConstructorArgElements)
  3. DefaultBeanFactory通过指定构造函数来生成Bean对象(ConstructorResolver注入Bean实例到构造方法中)

注:这里指定的构造函数的查找逻辑为:解析出XML的构造函数的参数列表,和通过反射拿到对应的构造函数的参数列表进行对比(每个参数的类型和个数必须一样)

tag:vstep9-constructor

Step10 实现注解

实现两个注解:@Component @Autowired(只针对属性注入,暂时不考虑方法注入)
且需要实现如下的XML的解析:

<?xml version="1.0" encoding="UTF-8"?>


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="org.spring.service.v4,org.spring.dao.v4">
</context:component-scan>


</beans>
  1. 定义注解Component ,Autowired
  2. 需要实现的第一个功能是:给一个包名,扫描获取到这个包以及子包下面的所有Class【PackageResourceLoaderTest】包名--> *.class 【涉及一个递归调用】
  3. 由于第二步中的BeanDefinition不像之前的xml那样,会对Bean配置一个id,所以,这里解析出来的Bean定义需要自动生成一个BeanId(默认先取value的配置,否则就就是类名第一个字母小写,抽象BeanNameGenerator来专门对Bean定义ID),同时,Spring中单独新建了一个AnnotatedBeanDefinition接口来定义包含注解的BeanDefinition, 封装第二步中的方法+BeanId的生成到一个类中:【ClassPathBeanDefinitionScannerTest】。
  4. 实现了第3步以后,我们得到了对应的Class文件,我们需要通过某种方式去解析这个Class文件,拿到这个Class中的所有信息,特别是注解信息。使用ASM这个jar【ASM的原始用法见:ClassReaderTest】 *.class -> class Info
  5. 步骤3虽然实现了读取Class中的信息这件事。但是用ASM的原生方式解析不太方便,解析ClassMetaData和Annotation都需要定义一个Visitor,所以Spring抽象了一个接口(MetadataReader)来封装ASM的实现【MetadataReaderTest】
  6. 拿到Bean中的所有Field(带注解的),并把他实例化成一个对象 : class info 中的Field -> new instance()【DependencyDescriptorTest】
  7. 将这个对象注入目标Bean中,new Instance() ——注入——>bean 【InjectionMetadataTest】
  8. 处理XML解析,注入ScannerBeanDefinition
  9. 整合,涉及到Bean初始化和Bean的生命周期【AutowiredAnnotationProcessorTest】

image.png
image.png
image.png
image.png
tag:vstep10-annotation-final

step11 实现Aop

<context:component-scan
		base-package="org.litespring.service.v5,org.litespring.dao.v5">
	</context:component-scan>

	<bean id="tx" class="org.litespring.tx.TransactionManager" />
	
	<aop:config>
		<aop:aspect ref="tx">
			<aop:pointcut id="placeOrder" 
                    expression="execution(* org.litespring.service.v5.*.placeOrder(..))" />
			<aop:before pointcut-ref="placeOrder" method="start" />
			<aop:after-returning pointcut-ref="placeOrder"	method="commit" />	
			<aop:after-throwing pointcut-ref="placeOrder" method = "rollback"/>		
		</aop:aspect>
	</aop:config>

image.pngimage.png

  1. 一些术语:Joint Point, Pointcut, Advice(拦截器,Before, After, Around....)
  2. 为什么要用aop:日志,安全,事务 作为切面和业务代码正交
  3. 运行时动态生成类

image.png

  1. PointcutTest: 给定一个表达式,然后判断某个类的某个方法是不是匹配这个表达式【依赖AspectJ】
    1. Pointcut:
    2. MethodMatcher: 给一个method,判断是否满足指定条件
    3. AspectJExpressionPointcut
  2. MethodLocatingFactoryTest:通过Bean的名称("tx")和方法名("start")定位到这个Method,然后反射调用这个Method
  3. 指定指定顺序的链式调用 (Aop alliance,使用了责任链这个设计模式) ReflectiveMethodInvocationTest,需要debug查看调用链路。

image.png

  1. 动态代理,在一个方法前后增加一些逻辑,而不用改动原始代码。CGlibTest, 其中testFilter方法是表示支持多个aop操作,(使用)

    1. 普通类:CGLib
    2. 接口:Jdk自带
  2. CglibAopProxyTest

image.png

tag: vaop-v1

  1. 抽象AbstractV5Test
  2. BeanFactoryTestV5:配置文件->Advice
  3. XML解析生成BeanDefinition
    1. aop标签的内容其实是包含在GenericDefinitionBean里面, 通过人工合成的,嵌套的Beandefinition处理
  4. BeandefinitionTestV5

ConfigBeanDefinitionParser.java
image.png
image.png
image.png

  1. BeanFactoryTestV5

嵌套Bean的处理

  1. ApplicationContextTest5

tag: vaop-v2

  1. 实现jdk动态代理

AspectJAutoProxyCreator
JdkAopProxyFactory

tag:vaop-v3
image.png

posted @ 2021-04-26 21:40  Grey Zeng  阅读(13)  评论(0编辑  收藏  举报