基于前面介绍的用spring改造后的示例应用,我们将做一些实验来了解bean定义的beanName、id、name属性以及别名映射的用法,并做相关总结。
参考练习examples/spring-02-id-name-alias
打印beanName
我们在BookStoreAppTest.saveBook
单元测试方法中输出加载到的所有bean定义的名称,也就是beanName
,bean定义在注册时就是用beanName
作为唯一标识来进行bean定义的注册的。
package com.xiaoma.spring.example.bookstore;
import ...
@Slf4j
class BookStoreAppTest {
@Test
void saveBook() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
...
AbstractApplicationContext aac = (AbstractApplicationContext) context;
String[] beanDefinitionNames = aac.getBeanDefinitionNames();
log.info("-------------------容器中包含的bean定义的名称列表------------------");
for (String name : beanDefinitionNames) {
log.info("{}", name);
}
}
}
看到输出的结果:
21:47:20.453 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - -------------------容器中包含的bean定义的名称列表------------------
21:47:20.453 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - funnyBook
21:47:20.453 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - boringBook
21:47:20.454 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - dabao
21:47:20.454 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - erbao
21:47:20.454 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - bookStoreApp
21:47:20.454 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - bookDao
21:47:20.455 [Test worker] INFO c.x.s.e.b.BookStoreAppTest - bookService
省略id的情况
在前面我们声明的bean定义中,只指定了id
属性,则spring容器在注册bean定义时就会取id作为beanName
。在同一个xml配置文件中id
是不能重复的,这是xml本身定义中的约束。当然,id
不是必须的,在声明一个bean时不指定id,这里区分两种情况:
-
内部bean
内部bean只有当前类的实例自己会引用,不会被外部环境引用到,通常直接用内部bean作为构造器参数或者属性值进行注入。比如:
<bean id="jack" class="com.xiaoma.spring.example.reading.Child"> <property name="name" value="jack" /> <property name="book"> <bean class="com.xiaoma.spring.example.reading.Book" /> </property> </bean>
这里的
Child
实例化后注入的book
依赖就是一个内部bean,它只与当前的实例绑定。它是没有名称的,因为外部没有对它的引用,也不需要对其进行bean定义的注册。 -
外部bean
相对于内部bean来说,如果不指定任何名称,容器会为外部bean自行分配。如果容器中只有该类型的bean的唯一实例,则我们无需显式为其指定名称;否则在按照名称进行注入时,不指定
id
或name
,会很不方便。比如:<!-- com.xiaoma.spring.example.reading.Book#0 --> <bean class="com.xiaoma.spring.example.reading.Book" /> <!-- com.xiaoma.spring.example.reading.Book#1 --> <bean class="com.xiaoma.spring.example.reading.Book" />
这里虽然我们定义了两个一样的bean(class一样),它们也是作为两个bean被注册的,注册用的
beanName
是自动生成的,虽然这种情况在我们后面介绍通过组件注解来声明bean时不会出现,但是在使用传统xml方式声明bean时更推荐显式的指定id
或name
属性,尤其是涉及多个同类型的bean的定义。
name属性与别名映射
再来看name
属性,它可以用来为bean起多个名字,它比起id
属性的好处是可以设置多个名称,通过逗号分隔(中间可以有空格)来设置。name
属性可以单独指定,也可以和id
一起指定,但是要注意,在同一个配置文件中,用id
和name
指定的任何一个名称都必须唯一(在单个bean定义中有重复会去重),不能和其他bean定义中的名称冲突,否则会报错。看下面的bean定义:
<!-- 命名规则没有严格限制,名称中也可以出现空格、点号、中划线等 -->
<bean id="myBook" name="bookForChildren, myBook, childrenBook, bookForChildren, children.book"
class="com.xiaoma.spring.example.reading.Book" />
在当前bean定义中有名称重复没关系,会去重,实际为:
<bean id="myBook" name="bookForChildren, childrenBook, children.book" ... />
这里myBook
作为注册bean定义的beanName
,其他的名称作为其别名,通过调试源码,可以看到别名的注册信息:
通过测试输出,会发现这4个名称是互为别名的关系
// AbstractApplicationContext aac
log.info("myBook alias: {}", Arrays.toString(aac.getAliases("myBook")));
log.info("bookForChildren alias: {}", Arrays.toString(aac.getAliases("bookForChildren")));
log.info("childrenBook alias: {}", Arrays.toString(aac.getAliases("childrenBook")));
log.info("children.book alias: {}", Arrays.toString(aac.getAliases("children.book")));
这种别名的映射等同于下面的显式指定别名映射的配置形式:
<alias name="myBook" alias="bookForChildren" />
<alias name="bookForChildren" alias="childrenBook" />
<alias name="childrenBook" alias="children.book" />
<bean id="myBook" class="com.xiaoma.spring.example.reading.Book" />
存储的映射关系可以看到:
会形成一个别名链,和前面多对一的映射,在获取每个名称的alias时,得到的结果都是一样的,都是互为别名的关系。
但是在注册别名链时需要注意不能存在循环注册的情况,比如:
<alias name="myBook" alias="bookForChildren" />
<alias name="bookForChildren" alias="childrenBook" />
<alias name="childrenBook" alias="children.book" />
<!-- 和第一个形成了循环注册 -->
<alias name="children.book" alias="myBook" />
启动容器提示的错误:
Configuration problem: Failed to register alias 'myBook' for bean with name 'children.book'
Offending resource: class path resource [application-context.xml]; nested exception is java.lang.IllegalStateException: Cannot register alias 'myBook' for name 'children.book': Circular reference - 'children.book' is a direct or indirect alias for 'myBook' already
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Failed to register alias 'myBook' for bean with name 'children.book'
以上对别名的映射关系也支持仅定义name
属性的情况,beanName
会使用name
中指定的第一个值作为注册bean定义的唯一标识,其他值会作为其别名。可以由读者自行实验。
在实际获取bean时,可以使用任何一个名称,下面的断言是ok的,容器会根据建立的别名映射找到注册bean的beanName
来获取bean:
assertSame(context.getBean("bookForChildren"), context.getBean("myBook"));
文件覆盖的情况
在同一个配置文件中进行如下配置:
<alias name="myBook" alias="bookForChildren" />
<alias name="bookForChildren" alias="childrenBook" />
<alias name="childrenBook" alias="children.book" />
<bean id="myBook" class="com.xiaoma.spring.example.reading.Book" />
<bean name="childrenBook" class="com.xiaoma.spring.example.reading.Book" />
下面的断言是成立的:
assertSame(context.getBean("children.book"), context.getBean("myBook"));
说明在使用一个名称获取bean时,会判断别名链,找到最原始的beanName
,即id
为myBook
的bean。断开中间的别名映射:
<!-- <alias name="bookForChildren" alias="childrenBook" />-->
现在获取的才是两个不同的bean。
对于跨多个文件的配置,Spring容器加载多个xml文件或者使用<import />
包含子配置文件时,如果定义的bean的标识字段有冲突,后面加载的会覆盖前面的。比如,这里有两个xml文件:
<!-- 位于application-context.xml文件中 -->
<bean id="myBook" name="bookForChildren, childrenBook, children.book" class="com.xiaoma.spring.example.reading.Book">
<property name="name" value="海贼王" />
<property name="type" value="漫画" />
</bean>
<!-- 位于other-context.xml文件中 -->
<bean name="myBook" class="com.xiaoma.spring.example.reading.Book">
<property name="name" value="火影" />
<property name="type" value="漫画" />
</bean>
加载顺序:
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml", "other-context.xml");
或者在application-context.xml
中导入other-context.xml
:
<beans ...>
<bean id="myBook" name="bookForChildren, childrenBook, children.book" class="com.xiaoma.spring.example.reading.Book">
<property name="name" value="海贼王" />
<property name="type" value="漫画" />
</bean>
<import resource="other-context.xml" />
</beans>
会发现根据myBook
获取的bean是被覆盖了注册定义的火影漫画。
现在调整下other-context.xml
种的定义:
<bean id="yourBook" name="childrenBook" class="...">
...
</bean>
从输出的日志可以看到现在各个名称的别名:
myBook alias: [bookForChildren, children.book]
bookForChildren alias: [myBook, children.book]
childrenBook alias: [yourBook]
children.book alias: [myBook, bookForChildren]
很显然现在拆成了两个别名组合了,也就是存在别名映射的覆盖。自然下面的断言不会成立:
assertSame(context.getBean("childrenBook"), context.getBean("myBook"));
最佳实践
推荐的做法是,如果某类型的bean只有一个,指定id为类名首字母小写,如果存在多个指定时后缀加以区分,另外bean的命名和变量不一样,没那么多限制,比如bookDao-mock
、bookDao.mock
都是合法的。