系列文章
手把手教你实现spring-beans (一)
手把手教你实现spring-beans (二)
手把手教你实现spring-context (TODO)
手把手教你实现spring-aop (TODO)
关于
本系列是对tiny-spring项目的详细解读,聚焦spring-beans
的基本实现,对应着(first~sixth)-stage
这六个构建过程。这部分实现了基础的IoC
容器,DI
是它的核心(控制反转和依赖注入的相关概念可以看这里)。
spring-beans的使用流程
回想一下在使用BeanFactory.getBean(...)
之前,我们要做些什么?首先,定义xml配置文件,告诉Spring我们需要什么样的对象以及它们之前的关系,接着初始化BeanFactory
读取配置文件、加载其中的定义信息,最后才是调用BeanFactory.getBean()
,根据定义信息初始化bean并返回。
举个例子,假设我们有如下两个类:
public class Car {
private double price;
private String brand;
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
public class Person {
private String name;
private Car car;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
}
现在有一个叫Saber的妹子有一辆BYD产的价值240000.00的车,如果用Spring来管理的话,大概是这样:
// 1、定义配置文件,描述需求
240000.00
BYD
Saber
// 2、读取配置文件,加载定义信息
Resource resource = new ClassPathResource("test/config.xml");
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions(resource);
// 3、调用getBean()
Person saber = (Person) beanFactory.getBean("saber");
Car byd = saber.getCar();
System.out.println("name = " + saber.getName() + ", car-brand = " + byd.getBrand() + ", car-price = " + byd.getPrice());
// 4、得到打印结果
name = Saber, car-brand = BYD, car-price = 240000.0
一切从Resource开始
快速浏览完使用流程,我们知道首先是要有xml配置文件来告诉Spring我们需要的对象以及它们之间的关系。同时,真实的Spring不仅仅支持xml这一种格式,还支持properties文件格式甚至是自定义的格式。Spring是怎么做到的呢?这是因为Spring引入了一层对资源的抽象——Resource
接口。Resource
接口解决的是配置文件从哪里来、怎么读取它的问题。
// 在tiny-spring目录下输入命令 git checkout first-stage,切换到第一阶段,可以看到对Resource接口的描述(这个接口对真实的Resource接口进行了大幅精简)。
// 其中直接定义在Resource接口中的方法抽象了资源从哪里来,定义在父接口中的方法抽象了资源怎么读。
public interface InputStreamSource {
/**
* 返回代表资源的输入流。
*/
@Nullable
InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
/**
* 从类路径加载的伪URL协议前缀。
*/
String CLASSPATH_URL_PREFIX = "classpath:";
/**
* 文件系统中文件的URL协议名。
*/
String FILESYSTEM_URL_PROTOCOL = "file";
/**
* 检查资源是否真实存在。
*/
boolean exists();
/**
* 返回指向此资源的URL。
*/
@Nullable
URL getURL() throws IOException;
/**
* 返回表示此资源的文件。
*/
@Nullable
File getFile() throws IOException;
}
在tiny-spring的实现中,我们采用xml格式来作为配置文件,并且对支持的功能(也就是对应的xml标签)进行了删减,因此仅实现了ClassPathResource
用来加载classpath下的xml配置文件,作为源码解析来说应该是够用了。
// 输入命令git checkout second-stage,切换到第二阶段,
// 在DefaultXMLBeanDefinitionParser.java中查看tiny-spring支持的xml标签及属性。
private static final String TRUE_VALUE = "true";
private static final String BEAN_ELEMENT = "bean";
private static final String CLASS_ATTRIBUTE = "class";
private static final String ID_ATTRIBUTE = "id";
private static final String NAME_ATTRIBUTE = "name";
private static final String SINGLETON_ATTRIBUTE = "singleton";
private static final String DEPENDS_ON_ATTRIBUTE = "depends-on";
private static final String INIT_METHOD_ATTRIBUTE = "init-method";
private static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method";
private static final String CONSTRUCTOR_ARG_ELEMENT = "constructor-arg";
private static final String INDEX_ATTRIBUTE = "index";
private static final String TYPE_ATTRIBUTE = "type";
private static final String PROPERTY_ELEMENT = "property";
private static final String REF_ELEMENT = "ref";
private static final String BEAN_REF_ATTRIBUTE = "bean";
private static final String LIST_ELEMENT = "list";
private static final String VALUE_ELEMENT = "value";
private static final String NULL_ELEMENT = "null";
private static final String LAZY_INIT_ATTRIBUTE = "lazy-init";
private static final String AUTOWIRE_ATTRIBUTE = "autowire";
private static final String AUTOWIRE_BY_NAME_VALUE = "byName";
private static final String AUTOWIRE_BY_TYPE_VALUE = "byType";
private static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor";
private static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect";
可以看到,tiny-spring复刻了真实Spring IoC Container的功能子集:每一个
标签都定义了一个容器管理的对象,同时支持setter注入和构造函数注入两种方式,分别由
和
标签代表,bean之间的引用由标签代表,注入的值可以是简单类型,由
标签代表,也可以是复合类型,比如数组,由
标签代表(map/set在tiny-spring中就不做支持了)。其他的诸如自动装配、懒加载、生命周期回调等属性也同样支持。
XML配置文件到BeanDefinition的转换
在读取配置文件之前,思考一下提取出来的信息如何保存?我们说过,每一个
标签都定义了一个容器管理的对象,自然就引出了BeanDefinition
,可以说一个
标签就对应着一个BeanDefinition
实例,singleton
、autowire
等等都是它的属性。
// 输入命令git checkout third-stage,切换到第三阶段,查看BeanDefinition的具体定义。
/**
* 保存从xml中解析出来的bean的定义信息。
*/
public class BeanDefinition {
// 不进行自动装配
public static final int AUTOWIRE_NO = 0;
// 通过bean名称自动装配
public static final int AUTOWIRE_BY_NAME = 1;
// 通过bean类型自动装配
public static final int AUTOWIRE_BY_TYPE = 2;
// 自动装配构造函数
public static final int AUTOWIRE_CONSTRUCTOR = 3;
// 自适应装配模式
public static final int AUTOWIRE_AUTODETECT = 4;
// bean所属的类, bean的名称由BeanFactoryRegistry管理
private final Class> beanClass;
// 是单实例还是每次获取都创建,默认为true
private boolean singleton = true;
// 对单实例的bean是否需要懒加载,
// 默认为false,在BeanFactory初始化时就
// 初始化所有单实例bean
private boolean lazyInit = false;
// 自动装配的模式
private int autowireMode = AUTOWIRE_NO;
// 所依赖的其他bean的名称
// dependsOn所代表的bean会在
// 当前bean初始化之前得到初始化
private String[] dependsOn;
// 自定义的初始化方法名,要求无参
private String initMethodName;
// 自定义的销毁方法名,要求无参
private String destroyMethodName;
// setter注入的相关信息
private MutablePropertyValues propertyValues;
// 构造函数注入的相关信息
private ConstructorArgumentValues constructorArgumentValues;
// 省略若干
......
}
而对于
和
标签,它们也有着各自的子标签和属性,因此分别由MutablePropertyValues
和ConstructorArgumentValues
两个类来表示。很简单的两个类,各位同学自行查看一下third-stage
的代码即可,这里就不贴了。
BeanDefinition的注册
概念上我们知道了一个
标签等于一个BeanDefinition
实例,那么tiny-spring怎么实现从XML配置文件到BeanDefinition
实例的转换呢?这就引出了XMLBeanDefinitionReader
接口,它只有一个方法,从Resource
中提取出BeanDefinition(s)
。
/**
* 对xml配置文件读取器的抽象。
* 读取器最主要的目的是读取一个个标签,
* 解析出其中的信息,生成对应的BeanDefinition。
*/
public interface XMLBeanDefinitionReader {
/**
* 加载bean的定义信息。
* @param resource 代表一个xml配置文件
*/
void loadBeanDefinition(@NotNull Resource resource);
}
回想一下BeanFactory.getBean(...)
的调用场景,我们传入一个bean name
,容器根据bean name
找到对应的BeanDefinition
,通过BeanDefinition
描述的信息生成对象并返回。也就是说容器持有着beanName -> BeanDefinition
的对应关系,这一层抽象出来也就是BeanDefinitionRegistry
。
/**
* 这个接口管理着BeanFactory中BeanDefinition注册
* 的相关事宜,因此BeanFactory的实现类也会实现这个接口。
* 单独抽取出这个接口,是为了让BeanFactory的职责更清晰,
* 避免成为上帝接口。BeanFactory就是一个bean工厂,司职于bean的获取查询。
*/
public interface BeanDefinitionRegistry {
/**
* 向BeanFactory中注册bean的定义信息
*/
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);
}
显然,这两个接口是要组合使用的。XMLBeanDefinitionReader
加载出BeanDefinition(s)
之后,由BeanDefinitionRegistry
来执行注册。Spring在实现时,额外提供了一个策略接口XMLBeanDefinitionParser
来进行真正的解析,tiny-spring的DefaultXMLBeanDefinitionReader
也是这么实现的。
/**
* 对xml配置文件解析器的抽象。
* 这是一个策略接口,XMLBeanDefinitionReader通过
* XMLBeanDefinitionParser来做具体的解析。
*/
public interface XMLBeanDefinitionParser {
/**
* 读取标签的定义生成BeanDefinition,再通过
* BeanDefinitionRegistry注册进BeanFactory。
* @param document 代表xml配置文件的Document对象
* @param classLoader 加载标签对应JavaBean的类加载器
* @param registry 用来注册BeanDefinition的注册器
*/
void registerBeanDefinitions(@NotNull Document document,
@NotNull ClassLoader classLoader,
@NotNull BeanDefinitionRegistry registry);
}
XMLBeanDefinitionReader
将真正的解析行为代理给了XMLBeanDefinitionParser
。NOTE:说是策略模式可以,说是代理模式也可以。具体如何解析,只是一个对应的算法,从这个层面说是策略模式,ok;XMLBeanDefinitionReader
本身不进行xml文件的解析,而是将这个行为委托给了XMLBeanDefinitionParser
,这么说是代理也没啥毛病吧。设计模式吧,大多都是语意上的区别,理解就好,犯不着钻牛角尖,Spring中有很多地方用到了这种模式。
XML配置文件的解析
以上都理解了之后,下面就进入DefaultXMLBeanDefinitionParser
执行真正的配置文件解析了。按照Spring xml配置文件的格式,首先获取最顶层标签
(这里其实是什么标签都可以),
标签是
的子标签,因此我们逐个遍历
的子标签找到其中的
标签,因此重心便转到了解析
标签上。
@Override
public void registerBeanDefinitions(Document document, ClassLoader classLoader, BeanDefinitionRegistry registry) {
// 获取顶层元素(也就是标签)
Element root = document.getDocumentElement();
// 获取下的子标签列表
NodeList nodes = root.getChildNodes();
// 统计标签的数量
int numberOfBeans = 0;
// 遍历子标签列表
for (int i = 0; i < nodes.getLength(); ++i) {
Node node = nodes.item(i);
// 找到标签
if (node instanceof Element &&
BEAN_ELEMENT.equals(node.getNodeName())) {
// 每一个标签就对应一个BeanDefinition
numberOfBeans++;
// 加载其配置信息
loadBeanDefinition((Element) node, classLoader, registry);
}
}
System.out.println("一共找到" + numberOfBeans + "个标签");
}
每个
标签对应着一个BeanDefinition
,因此在解析的过程中我们创建了一个BeanDefinition
实例来保存解析的结果。tiny-spring并不支持bean name alias
和inner bean
,也不支持BeanFactory
的层级结构,因此
标签必须指定id
属性和class
属性,解析出来的BeanDefinition
就直接交给BeanDefinitionRegistry
注册了。
/**
* 解析并注册标签
*/
private void loadBeanDefinition(Element element, ClassLoader classLoader, BeanDefinitionRegistry registry) {
// tiny spring不支持inner bean,也不支持bean的别名,
// 因此获取到的id就是bean的名称,也是关联对应BeanDefinition的key
String beanName = element.getAttribute(ID_ATTRIBUTE);
if (!StringUtils.hasLength(beanName)) {
throw new BeansException("每个标签都必须明确指定id属性");
}
// 解析出对应的BeanDefinition
BeanDefinition beanDefinition = parseBeanDefinition(beanName, element, classLoader);
// 检验一下是否有效
beanDefinition.validate();
// 并注册进BeanFactory
registry.registerBeanDefinition(beanName, beanDefinition);
System.out.println("已解析出[" + beanName + "]对应的bean定义[" + beanDefinition + "]");
}
解析的过程是非常直白的,查看
标签有没有定义lazy-init
、singleton
和init-method
等属性,有的话提取出来存储进BeanDefinition
。
/**
* 解析标签
*/
private BeanDefinition parseBeanDefinition(String beanName, Element element, ClassLoader classLoader) {
// tiny spring也没有支持BeanFactory的层次结构,
// 因此每个bean也需要明确指明其所属的类
String beanClassName = element.getAttribute(CLASS_ATTRIBUTE);
if (!StringUtils.hasLength(beanClassName)) {
throw new BeansException("每个标签都必须明确指定class属性");
}
try {
// 加载这个类
Class> beanClass = Class.forName(beanClassName, true, classLoader);
// 获取所有标签的内容
MutablePropertyValues propertyValues = parseAllPropertyElements(beanName, element);
// 获取所有标签的内容
ConstructorArgumentValues constructorArgumentValues = parseAllConstructorArgElements(beanName, element);
// 生成bean的定义信息
BeanDefinition beanDefinition = new BeanDefinition(beanClass, propertyValues, constructorArgumentValues);
// 获取依赖信息
if (element.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {
String dependsOn = element.getAttribute(DEPENDS_ON_ATTRIBUTE);
beanDefinition.setDependsOn(StringUtils.split(dependsOn, ",; ", true, true));
}
// 获取自动装配模式
String autowire = element.getAttribute(AUTOWIRE_ATTRIBUTE);
beanDefinition.setAutowireMode(getAutowireMode(autowire));
// 获取自定义的初始化方法名
String initMethodName = element.getAttribute(INIT_METHOD_ATTRIBUTE);
if (StringUtils.hasLength(initMethodName)) {
beanDefinition.setInitMethodName(initMethodName);
}
// 获取自定义的销毁方法名
String destroyMethodName = element.getAttribute(DESTROY_METHOD_ATTRIBUTE);
if (StringUtils.hasLength(destroyMethodName)) {
beanDefinition.setDestroyMethodName(destroyMethodName);
}
// 获取是否配置成单例
if (element.hasAttribute(SINGLETON_ATTRIBUTE)) {
beanDefinition.setSingleton(TRUE_VALUE.equals(element.getAttribute(SINGLETON_ATTRIBUTE)));
}
// 获取是否配置成懒加载
String lazyInit = element.getAttribute(LAZY_INIT_ATTRIBUTE);
if (beanDefinition.isSingleton()) { // 此属性对单例的bean才有效
beanDefinition.setLazyInit(TRUE_VALUE.equals(lazyInit));
}
return beanDefinition;
} catch (ClassNotFoundException e) {
throw new BeansException("找不到[" + beanClassName + "]对应的类", e);
}
}
和
因为是
的子标签而不是属性,因此需要单独处理。
和
标签的解析过程基本是一致的,这里就以
来作为说明。首先也是要遍历出
下的所有
标签,转换成对应的PropertyValue
,然后存入MutablePropertyValues
,最后归入BeanDefinition
。在tiny-spring中,
下可能存在
、
或中的一个来表示要注入属性的值,具体是怎么处理的呢?
/**
* 解析带有属性值的标签,提取值
*/
private Object parsePropertySubElement(String beanName, Element element) {
// 标签下有/
对于标签,我们用
RuntimeBeanReference
来标识它是一个对其它bean的引用,后续通过RuntimeBeanReference
中保存的bean name
,使用BeanFactory
的getBean(...)
就能获取到对应的bean并设置进去;对于
标签,我们同样用一个标记类ManagedList
来标识它,至于
,在xml中只是普通的字符串,直接提取出来保存即可,后续根据属性的实际类型来进行转换,当然,这是后面的故事了。
总结
至此我们完成了配置文件的抽象和读取过程,接下来就是BeanFactory
的戏份了。下一篇将会详细介绍BeanFactory
如何利用这些配置信息来帮我们管理对象,码字不易,感觉有帮助的话,点个star吧~~