Spring揭秘-BeanFactory的对象注册与依赖绑定方式

BeanFactory的对象注册与依赖绑定方式

BeanFactory作为一个IoC Service Provider,为了能够明确管理各个业务对象以及业务对象之间的依赖绑定关系,同样需要某种途径来记录和管理这些信息。上一章在介绍IoC Service Provider时,我们提到通常会有三种方式来管理这些信息。而BeanFactory几乎支持所有这些方式,很令人兴奋,不是吗?

直接编码方式

其实,把编码方式单独提出来称作一种方式并不十分恰当。因为不管什么方式,最终都需要编码才能“落实”所有信息并付诸使用。不过,通过这些代码,起码可以让我们更加清楚BeanFactory在底层是如何运作的。
下面来看一下我们的FX新闻系统相关类是如何注册并绑定的。

通过编码方式使用BeanFactory实现FX新闻相关类的注册及绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String[] args)
{
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaCode(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaCode(BeanDefinitionRegistry registry)
{
AbstractBeanDefinition newsProvider =
new RootBeanDefinition(FXNewsProvider.class,true);
AbstractBeanDefinition newsListener =
new RootBeanDefinition(DowJonesNewsListener.class,true);
AbstractBeanDefinition newsPersister =
new RootBeanDefinition(DowJonesNewsPersister.class,true);
// 将bean定义注册到容器中
registry.registerBeanDefinition("djNewsProvider", newsProvider);
registry.registerBeanDefinition("djListener", newsListener);
registry.registerBeanDefinition("djPersister", newsPersister);
// 指定依赖关系
// 1. 可以通过构造方法注入方式
ConstructorArgumentValues argValues = new ConstructorArgumentValues();
argValues.addIndexedArgumentValue(0, newsListener);
argValues.addIndexedArgumentValue(1, newsPersister);
newsProvider.setConstructorArgumentValues(argValues);
// 2. 或者通过setter方法注入方式
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.addPropertyValue(new ropertyValue("newsListener",newsListener));
propertyValues.addPropertyValue(new PropertyValue("newPersistener",newsPersister));
newsProvider.setPropertyValues(propertyValues);
// 绑定完成
return (BeanFactory)registry;
}
JAVA

BeanFactory只是一个接口,我们最终需要一个该接口的实现来进行实际的Bean的管理, DefaultListableBeanFactory就是这么一个比较通用的BeanFactory实现类。 DefaultListableBeanFactory除了间接地实现了BeanFactory接口,还实现了BeanDefinitionRegistry接口,该接口才是在BeanFactory的实现中担当Bean注册管理的角色。基本上, BeanFactory接口只定义如何访问容器内管理的Bean的方法,各个BeanFactory的具体实现类负责具体Bean的注册以及管理工作。BeanDefinitionRegistry接口定义抽象了Bean的注册逻辑。通常情况下,具体的BeanFactory实现类会实现这个接口来管理Bean的注册。它们之间的关系如下图所示。

打个比方说, BeanDefinitionRegistry就像图书馆的书架,所有的书是放在书架上的。虽然你还书或者借书都是跟图书馆(也就是BeanFactory,或许BookFactory可能更好些)打交道,但书架才是图书馆存放各类图书的地方。所以,书架相对于图书馆来说,就是它的“ BookDefinitionRegistry”。

每一个受管的对象,在容器中都会有一个BeanDefinition的实例(instance)与之相对应,该BeanDefinition的实例负责保存对象的所有必要信息,包括其对应的对象的class类型、是否是抽象类、构造方法参数以及其他属性等。当客户端向BeanFactory请求相应对象的时候,BeanFactory会通过这些信息为客户端返回一个完备可用的对象实例。 RootBeanDefinition 和 ChildBeanDefinition是BeanDefinition的两个主要实现类。

现在,我们再来看这段绑定代码,应该就有“柳暗花明”的感觉了。

在main方法中,首先构造一个DefaultListableBeanFactory作为BeanDefinitionRegistry,然后将其交给bindViaCode方法进行具体的对象注册和相关依赖管理,然后通过bindViaCode返回的BeanFactory取得需要的对象,最后执行相应逻辑。在我们的实例里,当然就是取得FXNewsProvider进行新闻的处理。

在bindViaCode方法中,首先针对相应的业务对象构造与其相对应的BeanDefinition,使用了RootBeanDefinition作为BeanDefinition的实现类。构造完成后,将这些BeanDefinition注册到通过方法参数传进来的BeanDefinitionRegistry中。之后,因为我们的FXNewsProvider是采用的构造方法注入,所以,需要通过ConstructorArgumentValues为其注入相关依赖。在这里为了同时说明setter方法注入,也同时展示了在Spring中如何使用代码实现setter方法注入。如果要运行这段代码,需要把setter方法注入部分的4行代码注释掉。最后,以BeanFactory的形式返回已经注册并绑定了所有相关业务对象的BeanDefinitionRegistry实例。

注意:最后一行的强制类型转换是有特定场景的。因为传入的DefaultListableBeanFactory同时实现了BeanFactory和BeanDefinitionRegistry接口,所以,这样做强制类型转换不会出现问题。但需要注意的是,单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的!

外部配置文件方式

Spring的IoC容器支持两种配置文件格式: Properties文件格式和XML文件格式。当然,如果你愿意也可以引入自己的文件格式,前提是真的需要。

采用外部配置文件时, Spring的IoC容器有一个统一的处理方式。通常情况下,需要根据不同的外部配置文件格式,给出相应的BeanDefinitionReader实现类,由BeanDefinitionReader的相应实现类负责将相应的配置文件内容读取并映射到BeanDefinition,然后将映射后的BeanDefinition注册到一个BeanDefinitionRegistry,之后, BeanDefinitionRegistry即完成Bean的注册和加载。

当然,大部分工作,包括解析文件格式、装配BeanDefinition之类的工作,都是由BeanDefinitionReader的相应实现类来做的, BeanDefinitionRegistry只不过负责保管而已。整个过程类似于如下代码:

1
2
3
4
BeanDefinitionRegistry beanRegistry = <某个BeanDefinitionRegistry实现类,通常为DefaultListableBeanFactory>;
BeanDefinitionReader beanDefinitionReader = new BeanDefinitionReaderImpl(beanRegistry);
beanDefinitionReader.loadBeanDefinitions("配置文件路径");
// 现在我们就取得了一个可用的BeanDefinitionRegistry实例
JAVA

Properties配置格式的加载

Spring提供了org.springframework.beans.factory.support.PropertiesBeanDefinitionReader类用于Properties格式配置文件的加载,所以,我们不用自己去实现BeanDefinitionReader,只要根据该类的读取规则,提供相应的配置文件即可。
对于FXNews系统的业务对象,我们采用如下文件内容(见代码清单4-5)进行配置加载。

1
2
3
4
5
6
7
8
9
djNewsProvider.(class)=..FXNewsProvider
# ----------通过构造方法注入的时候-------------
djNewsProvider.$0(ref)=djListener
djNewsProvider.$1(ref)=djPersister
# ----------通过setter方法注入的时候---------
# djNewsProvider.newsListener(ref)=djListener
# djNewsProvider.newPersistener(ref)=djPersister
djListener.(class)=..impl.DowJonesNewsListener
djPersister.(class)=..impl.DowJonesNewsPersister
PROPERTIES

这些内容是特定于Spring的PropertiesBeanDefinitionReader的,要了解更多内容,请参照Spring的API参考文档。我们可以很容易地看明白上述代码中的配置内容所要表达的意思。

djNewsProvider作为beanName,后面通过.(class)表明对应的实现类是什么,实际上使用djNewsProvider.class=…的形式也是可以的,但Spring 1.2.6之后不再提倡使用,而提倡使用.(class)的形式。其他两个类的注册, djListener和djPersister,也是相同的道理。

通过在表示beanName的名称后添加.$[number]后缀的形式,来表示当前beanName对应的对象需要通过构造方法注入的方式注入相应依赖对象。在这里,我们分别将构造方法的第一个参数和第二个参数对应到djListener和djPersister。需要注意的一点,就是$0和$1后面的(ref), (ref)用来表示所依赖的是引用对象,而不是普通的类型。如果不加(ref),PropertiesBeanDefinitionReader会将djListener和djPersister作为简单的String类型进行注入,异常自然不可避免啦。

FXNewsProvider采用的是构造方法注入,而为了演示setter方法注入在Properties配置文件中又是一个什么样子,以便于你更全面地了解基于Properties文件的配置方式,我们在下面增加了setter方法注入的例子,不过进行了注释。实际上,与构造方法注入最大的区别就是,它不使用数字顺序来指定注入的位置,而使用相应的属性名称来指定注入。 newsListener和newPersistener恰好就是我们的FXNewsProvider类中所声明的属性名称。这印证了之前在比较构造方法注入和setter方法注入方式不同时提到的差异,即构造方法注入无法通过参数名称来标识注入的确切位置,而setter方法注入则可以通过属性名称来明确标识注入。与在Properties中表达构造方法注入一样,同样需要注意,如果属性名称所依赖的是引用对象,那么一定不要忘了(ref)。

当这些对象之间的注册和依赖注入信息都表达清楚之后,就可以将其加载到BeanFactory而付诸使用了。而这个加载过程实际上也就像我们之前总体上所阐述的那样,如下代码内容再次演示了类似的加载过程。
加载Properties配置的BeanFactory的使用演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args)
{
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaPropertiesFile(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}

public static BeanFactory bindViaPropertiesFile(BeanDefinitionRegistry registry)
{
PropertiesBeanDefinitionReader reader =
new PropertiesBeanDefinitionReader(registry);
reader.loadBeanDefinitions("classpath:../../binding-config.properties");
return (BeanFactory)registry;
}
JAVA

基于Properties的加载方式就是这么简单,所有的信息配置到Properties文件即可,不用再通过冗长的代码来完成对象的注册和依赖绑定。这些工作就交给相应的BeanDefinitionReader来做吧!哦,我的意思是,让给PropertiesBeanDefinitionReader来做。

注:Spring提供的PropertiesBeanDefinitionReader是按照Spring自己的文件配置规则进行加载的,而同样的道理,你也可以按照自己的规则① 来提供相应的Properties配置文件。只不过,现在需要实现你自己的“ PropertiesBeanDefinitionReader”来读取并解析。这当然有“重新发明轮子”之嫌,不过,如果你只是想试验一下,也可以尝试哦。无非就是按照自己的规则把各个业务对象信息读取后,将编码方式的代码改造一下放到你自己的“ PropertiesBeanDefinitionReader”而已。

XML配置格式的加载

XML配置格式是Spring支持最完整,功能最强大的表达方式。当然,一方面这得益于XML良好的语意表达能力;另一方面, 就是Spring框架从开始就自始至终保持XML配置加载的统一性。同Properties配置加载类似,现在只不过是转而使用XML而已。 Spring 2.x之前,XML配置文件采用DTD( DocumentType Definition)实现文档的格式约束。 2.x之后,引入了基于XSD( XML Schema Definition)的约束方式。不过,原来的基于DTD的方式依然有效,因为从DTD转向XSD只是“形式”上的转变,所以,后面的大部分讲解还会沿用DTD的方式,只有必要时才会给出特殊说明。

如果FX新闻系统对象按照XML配置方式进行加载的话,配置文件内容如以下代码所示。

FX新闻系统相关类对应XML格式的配置内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>
<bean id="djNewsListener" class="..impl.DowJonesNewsListener">
</bean>
<bean id="djNewsPersister" class="..impl.DowJonesNewsPersister">
</bean>
</beans>
JAVA

我想这段内容不需要特殊说明吧,应该比Properties文件的内容要更容易理解。如果想知道这些内容背后的更多玄机,往后看吧!
有了XML配置文件,我们需要将其内容加载到相应的BeanFactory实现中,以供使用,如以下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args)
{
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaXMLFile(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaXMLFile(BeanDefinitionRegistry registry)
{
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
reader.loadBeanDefinitions("classpath:../news-config.xml");
return (BeanFactory)registry;
// 或者直接
//return new XmlBeanFactory(new ClassPathResource("../news-config.xml"));
}
JAVA

与为Properties配置文件格式提供PropertiesBeanDefinitionReader相对应, Spring同样为XML格 式 的 配 置 文 件 提 供 了 现 成 的 BeanDefinitionReader实 现 , 即 XmlBeanDefinitionReader。XmlBeanDefinitionReader负责读取Spring指定格式的XML配置文件并解析,之后将解析后的文件内容映射到相应的BeanDefinition,并加载到相应的BeanDefinitionRegistry中(在这里是DefaultListableBeanFactory)。这时,整个BeanFactory就可以放给客户端使用了。

除了提供XmlBeanDefinitionReader用于XML格式配置文件的加载, Spring还在DefaultListableBeanFactory的基础上构建了简化XML格式配置加载的XmlBeanFactory实现。从以上代码最后注释掉的一行,你可以看到使用了XmlBeanFactory之后,完成XML的加载和BeanFactory的初始化是多么简单。

注:当然,如果你愿意,就像Properties方式可以扩展一样, XML方式的加载同样可以扩展。虽然XmlBeanFactory基本上已经十分完备了,但如果出于某种目的, XmlBeanFactory或者默认的XmlBeanDefinitionReader所使用的XML格式无法满足需要的话,你同样可以通过扩展XmlBeanDefinitionReader或者直接实现自己的BeanDefinitionReader来达到自定义XML配置文件加载的目的。 Spring的可扩展性为你服务!

注解方式

可能没有注意到,在提到BeanFactory所支持的对象注册与依赖绑定方式的时候,说的是BeanFactory“几乎”支持IoC Service Provider可能使用的所有方式。之所以这么说,有两个原因。

在Spring 2.5发布之前, Spring框架并没有正式支持基于注解方式的依赖注入;
Spring 2.5发布的基于注解的依赖注入方式,如果不使用classpath-scanning功能的话,仍然部分依赖于“基于XML配置文件”的依赖注入方式。

另外,注解是Java 5之后才引入的,所以,以下内容只适用于应用程序使用了Spring 2.5以及Java 5或者更高版本的情况之下。

如果要通过注解标注的方式为FXNewsProvider注入所需要的依赖,现在可以使用@Autowired以及@Component对相关类进行标记。

如下代码演示了FXNews相关类使用指定注解标注后的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class FXNewsProvider
{
@Autowired
private IFXNewsListener newsListener;
@Autowired
private IFXNewsPersister newPersistener;
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister)
{
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
...
}
@Component
public class DowJonesNewsListener implements IFXNewsListener
{
...
}
@Component
public class DowJonesNewsPersister implements IFXNewsPersister
{
...
}
JAVA

@Autowired是这里的主角,它的存在将告知Spring容器需要为当前对象注入哪些依赖对象。而@Component则是配合Spring 2.5中新的classpath-scanning功能使用的。现在我们只要再向Spring的配置文件中增加一个“触发器”,使用@Autowired和@Component标注的类就能获得依赖对象的注入了。

如下代码给出的正是针对这部分功能的配置内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:component-scan base-package="cn.spring21.project.base.package"/>
</beans>

XML

<context:component-scan/>会到指定的包( package)下面扫描标注有@Component的类,如果找到,则将它们添加到容器进行管理,并根据它们所标注的@Autowired为这些类注入符合条件的依赖对象。

在以上所有这些工作都完成之后,我们就可以像通常那样加载配置并执行当前应用程序了,如以下代码所示:

1
2
3
4
5
6
public static void main(String[] args)
{
ApplicationContext ctx = new ClassPathXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("FXNewsProvider");
newsProvider.getAndPersistNews();
}
JAVA

本章最后将详细讲解Spring 2.5新引入的“基于注解的依赖注入”。当前的内容只是让我们先从总体上有一个大概的印象,所以,不必强求自己现在就完全理解它们。

注:Google Guice是一个完全基于注解方式、提供依赖注入服务的轻量级依赖注入框架,可以从Google Guice的站点获取有关这个框架的更多信息。


Spring揭秘-BeanFactory的对象注册与依赖绑定方式
https://leehoward.cn/2019/10/28/Spring揭秘-BeanFactory的对象注册与依赖绑定方式/
作者
lihao
发布于
2019年10月28日
许可协议