上一篇:Note3
前言:在 Note1 的 4.3 中简单的描述了什么是装配,以及为什么需要装配。在这一节将详细解析 Spring 是如何装配 Bean 的。创建应用对象之间协作关系的行为通常称为装配(wiring) , 这也是依赖注入(DI) 的本质。
Spring容器会负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。 但是,我们首先需要告诉Spring要创建哪些bean并且如何将其装配在一起。
Spring 中装配 bean 的三种常见方式
Spring有三种常见的装配方式:
1. 在XML中进行显式配置(XML)
2. 在Java中进行显式配置(JavaConfig)
3. 隐式的bean发现机制和自动装配
尽管可以任意选择或搭配使用这三种方式,但还是建议优先使用自动装配机制,其次可以选择使用JavaConfig,最后才是XML的方式。
1. 隐式的bean发现机制和自动装配
1.1 先使用注解声明Bean
@Component
public calss Dog{
public void say(){
System.out.println("I'm a dog");
}
}
Bean声明注解:
@Component 、@Controller、@Service、@Repositpry、(SpringBoot还有一个 @RestController 注解,这里先不提)
这些注解的作用是一样的,只是为了区分代码中的结构分层(Controller/Service/Dao)而做出的区分,@Component注解则通常用作通用的地方,这四个注解所在的代码分层即使混用也没关系(但不推荐)。
Bean声明注解表明该类会作为组件类, 并告知Spring要为这个类创建bean(但不是在添加完注解后马上就创建)。
1.2 Spring从两个角度来实现自动化装配
> 组件扫描(component scanning) : Spring会自动发现应用上下文中所创建的bean。
> 自动装配(autowiring) : Spring自动满足bean之间的依赖。
Spring的组件自动扫描默认是不启用的,可以通过两种方式(JavaConfig 或 XML)来启用组件自动扫描。
【###】 XML的方式
//test.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:compontent-scan base-package="com.test">
<!--
1.多个包以逗号分开:base-package="com.test,com.test2,..."
2.可以使用通配符:base-package="com.test.*"
-->
</beans>
按照上述配置后,默认会扫描 base-package 属性所指定的包及所有子包中带有@Component注解的类,并在Spring中自动为这些类创建一个bean。
【###】JavaConfig的方式
@Configuration
@ComponentScan
public calss TestConfig{
}
//也可以设置扫描指定的包:
//@ComponentScan("com.test")
//@ComponentScan( basePackages = {"com.test" , "..."} )
如果没有其他配置的话, @ComponentScan默认会扫描当前配置类所在的包及所有子包中带有@Component注解的类,并在Spring中自动为这些类创建一个bean。
1.3 不通过指定包名来扫描
上面这两种方式可以实现扫描指定包,但如果代码重构时,上层包结构可能会改变,这样写死包名就会出问题。所以,@ComponentScan还提供了另一种方法:
> 先在包中创建一个空标记接口(或类)(例如:MarkerTest),
> 使用 basePackageClasses 属性 @ComponentScan( basePackageClasses = {MarkerTest.class , ...} ) 标记接口(或类)所在的包将会作为组件扫描的基础包,基础包及其下的所有子包都会被扫描。
1.4 创建一个 JUnit 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestConfig.class)
public class MyJunitTest{
@Autowired
private Dog dog;
@Test
public void testAaIfNull(){
assertNotNull(dog);
}
}
Spring 的 SpringJUnit4ClassRunner 会在测试开始的时候自动创建Spring的应用上下文。 注解@ContextConfiguration会告诉它需要在 TestConfig 中加载配置。 因为 TestConfig 类中包含了@ComponentScan, 因此最终的应用上下文中会包含 dog。
为了测试上述结论是否正确,在类中使用 @Autowired 注入了该 bean,并且使用了一个简单的测试方法断言 dog 不为null。
1.5 为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID,默认将类名的首字母小写作为Bean的ID。当然,也可以自定义Bean的 ID 的值,例如:
@Component("litterDog")
public calss Dog{
public void say(){
System.out.println("I'm a dog");
}
}
还有另外一种方式指定 ID 的值 —— 使用Java依赖注入规范(Java Dependency Injection) 中所提供的@Named注解:
@Named("litterDog")
public calss Dog{
public void say(){
System.out.println("I'm a dog");
}
}
虽然在大多数场景中, @Named 和 @Component 是可以互相替换,但还是推荐使用 @Component 注解。
1.6 Bean的自动装配
自动装配就是让Spring自动满足bean依赖的一种方法, 在满足依赖的过程中, 会在Spring应用上下文中寻找匹配某个bean需求的其他bean。
使用 @Resource 或 @Autowired 注解来自动满足依赖,详见@Autowired 和 @Resource 注解。
【###】@Inject 注解
如果不想使用Spring注解,也可以用 @Inject 注解来替换。@Inject注解来源于Java依赖注入规范,@Inject注解和@Autowired注解之间有一些细微的差别,但大多数场景下还是可以相互替换的。
2. 使用 JavaConfig 进行显式配置
尽管推荐使用组件扫描和自动装配来实现自动化配置,但有时候这种方案是行不通的。例如,想要将第三方库的组件装配到自己的应用中,在这种情况下是没有办法在类上添加 @Component 等注解。这是就需要显示配置了,显示配置有两种:Java和XML,先来看看JavaConfig显示配置。
2.1 POJO没有依赖其他POJO的情况
普通Java类
public calss Dog{
public void say(){
System.out.println("I'm a dog");
}
}
JavaConfig显示装配
@Configuration
public calss TestConfig{
//所装配Bean的ID为和方法名相同
@Bean
public Dog dog(){
return new Dog();
}
//如果想要指定Bean的ID,可以指定 name 属性值
@Bean(name="litterDog")
public Dog dog(){
return new Dog();
}
}
2.2 POJO依赖其他POJO的情况
【###】方式一
普通Java类
public calss Human{
private Dog dog;
public Human(Dog dog){
this.dog = dog;
}
public void play(){
dog.say();
}
}
JavaConfig显示装配
@Configuration
public calss TestConfig{
@Bean
public Human human(){
return new Human(dog());
}
@Bean
public Dog dog(){
return new Dog();
}
}
并不是每次调用 dog() 都返回一个新的Bean实例。因为默认情况下,Spring是单例的。所以,Spring会拦截 dog() 的调用并确保返回的是 Spring 所创建的 Bean。所以,无论调用多少次 dog(),返回的都是同一个 Dog 实例。
【###】方式二
针对 POJO 依赖其他 POJO 的情况下,还有另外一种配置方式。
@Configuration
public calss TestConfig{
@Bean
public Human human(Dog dog){
return new Human(dog);
}
@Bean
public Dog dog(){
return new Dog();
}
}
这种方式相对于方式一来说是一个更好的选择,因为他不会要求将 Dog 和 Human 声明到同一个配置类中。你可以将 Dog 放在其他 JavaConfig、XML配置或自动扫描装配Bean,都可以。
3. 使用 XML 进行显式配置
在此再强调一遍,只有到了万不得已的时候才推荐使用XML进行配置。所以,在这一节学习XML配置是为了能理解并维护项目中已有的XML配置,而不是为了在新的工作任务中使用XML配置。
3.1 声明一个简单的<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
http://www.springframework.org/schema/context">
<!-- ID为:com.test.XmlTest#0 -->
<bean class="com.test.XmlTest">
<!-- ID为:com.test.XmlTest#1 -->
<bean class="com.test.XmlTest">
<!-- ID为:xmlTest -->
<bean id="xmlTest" class="com.test.XmlTest">
</beans>
如果在<bean>中没有指定id,则默认为:全限定类名 + #n。
3.2.1 构造器注入之(注入bean引用)
有两种方案:
方案一:<constructor-arg>元素
方案二:使用 Spring3.0 引入的 c- 命名空间
两者的区别很大程序上在于是否冗长烦琐,<constructor-arg>元素比 c- 会更加冗长,但有些 <constructor-arg>元素可以实现的事情是 c- 无法实现的。
【###】方案一(此处省略xml声明,和上面相同)
<bean id="human" class="com.test.Human">
<constructor-arg ref="hand"/>
<constructor-arg ref="eye"/>
</bean>
<bean id="hand" class="com.test.Hand"/>
<bean id="hand" class="com.test.Eye"/>
【###】方案二
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
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="human" class="com.test.Human" c:hand-ref="hand" c:eye-ref="eye"/>
<bean id="hand" class="com.test.Hand"/>
<bean id="eye" class="com.test.Eye"/>
</beans>
解释(c:hand-ref):
[c:] 表示命名空间前缀
[hand] 表示构造器参数名
[-ref] 表示命名约定,它会告诉Spring,后面的值是一个Bean的ID引用,而不是一个字面量“hand”
【###】如果觉得直接写构造器参数名不好,可以按照下面这样进行替换:
<bean id="human" class="com.test.Human" c:_0-ref="hand"/>
<bean id="human" class="com.test.Human" c:_1-ref="eye"/>
<!-- 如果构造器只有一个参数,则可以直接省略参数索引 -->
<bean id="human" class="com.test.Human" c:_-ref="hand"/>
将构造器参数名换成了“0”,也就是参数的索引。因为在XML中不允许数字作为属性的第一个字符,因此必须要添加一个下划线作为前缀。
3.2.2 构造器注入之(注入字面量)
【说明】在下面的字面量注入示例中,字面量都是硬编码的。这里使用硬编码是为了方便,在 Note5 中会讲如何使用属性占位符或Spring表达式语言来实现在运行时动态注入字面量的值。
public class Cat{
private String name;
public Cat(String name){
this.name = name;
}
}
【###】方案一
<bean id="cat" class="com.test.Cat">
<constructor-arg value="hello kitty"/>
</bean>
【###】方案二
<bean id="cat" class="com.test.Cat" c:_name="hello kitty"/>
<!-- 或 -->
<bean id="cat" class="com.test.Cat" c:_0="hello kitty"/>
<!-- 如果构造器只有一个参数 -->
<bean id="cat" class="com.test.Cat" c:_="hello kitty"/>
对比于注入Bean的ID引用,去掉了 “-ref” 后缀。
3.2.3 构造器注入之(注入集合)
注入集合到构造器中只能使用 <constructor-arg> 元素,因为 c- 命名空间没有这个功能。
public class Pig{
private List<Stirng> sons;
public Pig(List<String> sons){
this.sons = sons;
}
}
【###】给集合一个null值
<bean id="pig" class="com.test.Pig">
<constructor-arg><null/></constructor-arg>
</bean>
【###】给集合赋非空值
<bean id="pig" class="com.test.Pig">
<constructor-arg>
<list>
<value>balckPig</value>
<value>pinkPig</value>
<value>whitePig</value>
</list>
</constructor-arg>
</bean>
同理,如果集合不是字面量结合而是 bean 引用集合:
<bean id="pig" class="com.test.Pig">
<constructor-arg>
<list>
<ref bean="blackPig"/>
<ref bean="pinkPig"/>
<ref bean="whitePig"/>
</list>
</constructor-arg>
</bean>
如果构造器参数类型不是List,而是Set。则直接将 <list> 换成 <set> 就可以了,其他不变。
3.3 属性注入
上面的XML注入示例(Bean引用、字面量、集合)都是通过构造器来注入的,没有使用属性的 Setter 方法,接下来就看看如何使用XML实现属性注入。
public class Human{
private Hand hand;
public void setHand(Hand hand){
this.hand = hand;
}
}
3.3.1 属性注入之(注入bean引用)
【###】方案一
<bean id="human" class="com.test.Human">
<property name="hand" ref="hand"/>
</bean>
<bean id="hand" class="com.test.Hand"/>
<property>元素为属性的Setter方法提供的功能与<constructor-arg>元素为构造器所提供的功能是一样的。
【###】方案二
Spring 为 <constructor-arg> 元素提供了 c- 命名空间作为替代方案。类似的,Spring 也为 <property> 提供了替代方案:p- 命名空间。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
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="human" class="com.test.Human" p:hand-ref="hand"/>
<bean id="hand" class="com.test.Hand"/>
</beans>
解释(p:hand-ref):
[p:] 表示命名空间前缀
[hand] 表示属性名
[-ref] 表示命名约定,它会告诉Spring,后面的值是一个Bean的ID引用,而不是一个字面量“hand”
3.3.2 属性注入之(注入字面量)
【说明】在下面的字面量注入示例中,字面量都是硬编码的。这里使用硬编码是为了方便,在 Note5 中会讲如何使用属性占位符或Spring表达式语言来实现在运行时动态注入字面量的值。
public class Cat{
private String name;
public void setName(String name){
this.name = name;
}
}
【###】方案一
<bean id="cat" class="com.test.Cat">
<property name="name" value="hello kitty"/>
</bean>
【###】方案二
<bean id="cat" class="com.test.Cat" p:name="hello kitty"/>
对比于注入Bean的ID引用,去掉了 “-ref” 后缀。
3.3.3 属性注入之(注入集合)
不能使用 p- 命名空间来装配集合,但可以还是用Spring 的 util- 命名空间来进行简化。
public class Pig{
private List<Stirng> sons;
public void setSons(List<String> sons){
this.sons = sons;
}
}
【###】方案一
<bean id="pig" class="com.test.Pig">
<property name="sons">
<list>
<value>balckPig</value>
<value>pinkPig</value>
<value>whitePig</value>
</list>
</property>
</bean>
同理,如果集合不是字面量结合而是 bean 引用集合:
<bean id="pig" class="com.test.Pig">
<property name="sons">
<list>
<ref bean="blackPig"/>
<ref bean="pinkPig"/>
<ref bean="whitePig"/>
</list>
</property>
</bean>
如果属性的参数类型不是List,而是Set。则直接将 <list> 换成 <set> 就可以了,其他不变。
【###】方案二(使用 util- 命名空间简化)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
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"
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<util:list id="sonList">
<value>blackPig</value>
<value>pinkPig</value>
<value>whitePig</value>
</util:list>
<bean id="com.test.Pig" p:sons-ref="sonList"/>
</beans>
3.3.4 util- 命名空间中的元素
util:list:创建一个 java.util.List 类型的 bean,其中包含值或引用
util:set:创建一个 java.util.Set 类型的 bean,其中包含值或引用
util:map:创建一个 java.util.Map 类型的 bean,其中包含值或引用
util:properties:创建一个 java.util.Properties 类型的 bean
util:property-path:引用一个 bean 的属性(或内嵌属性),并将其暴露为 bean
<util:constant>:引用某个类型的 public static 域,并将其暴露为 bean
3.4 使用构造器注入还是属性注入?
一个通用的规则是:对于强依赖使用构造器注入,对于可选依赖使用属性注入。
什么是强依赖?例如 Human 对 Hand (即人对于手)即使强依赖关系,而人对鞋子就是可选依赖。
4. 导入和混合配置
在 Spring 应用中,组件扫描、JavaConfig 和 XM 配置三者可以混合使用。且在自动装配Bean时,并不在意Bean来自哪里,不管是它是在XML或JavaConfig中声明的还是通过自动扫描得到的。
4.1 组合多个JavaConfig
定义了两个JavaConfig
//CatConfig.java
@Configuration
public class CatConfig{
@Bean
public Cat cat(){
return new Cat();
}
}
//DogConfig.java
@Configuration
public class DogConfig{
@Bean
public Dog dog(){
return new Dog();
}
}
将这两个 JavaConfig 组合在一起
@Configuration
@Import(CatConfig.class)
public class DogConfig{
@Bean
public Dog dog(){
return new Dog();
}
}
//或
@Configuration
@Import({CatConfig.class,DogConfig.class})
public class AnimalConfig{
}
4.2 组合 JavaConfig 和 XML配置
定义一个 JavaConfig
@Configuration
public class DogConfig{
@Bean
public Dog dog(){
return new Dog();
}
}
定义一个 XML 配置
//cat-config.xml
...
<bean id="cat" class="com.test.Cat" p:name="hello kitty"/>
...
将两个配置组合在一起
@Configuration
@Import(DogConfig.class)
@ImportResource("classpath:cat-config.xml")
public class AnimalConfig{
}
4.3 组合多个 XML 配置
定义两个 XML 配置
//cat-config.xml
...
<bean id="cat" class="com.test.Cat" p:name="hello kitty"/>
...
//dog-config.xml
...
<bean id="dog" class="com.test.Dog" p:name="hello Tony"/>
...
将两个配置组合在一起
//dog-config.xml
...
<bean id="dog" class="com.test.Dog" p:name="hello Tony"/>
<import resource="dog-config.xml"/>
...
4.4 最后
不管使用 JavaConfig 还是 XML 进行配置,我们通常都会创建一个根配置(JavaConfig 或 XML)。在根配置中将多个 JavaConfig 和 XML 配置组合在一起。最后,通常会在根配置中开启应用组件扫描(通过 <context:component-scan> 或 @ComponentScan)。
下一篇:Note5