本章内容:声明bean、构造器注入和Setter方法注入、装配bean、控制bean的创建和销毁
2.1 Spring配置的可选方案
当描述bean如何进行装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制:
(1)在XML中进行显式配置。
(2)在Java中进行显式配置。
(3)隐式的bean发现机制和自动装配。
建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),我推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。
2.2 自动化装配bean
Spring从两个角度来实现自动化装配:
组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
自动装配(autowiring):Spring自动满足bean之间的依赖。
组件扫描和自动装配组合在一起就能发挥出强大的威力,它们能够将你的显式配置降低到最少。
2.2.1 创建可被发现的bean
如果你不将CD插入(注入)到CD播放器中,那么CD播放器其实是没有太大用处的。所以,可以这样说,CD播放器依赖于CD才能完成它的使命。为了在Spring中阐述这个例子,让我们首先在Java中建立CD的概念。
代码清单1:CompactDisc接口在Java中定义了CD的概念
package spring.soundsystem; public interface CompactDisc { void play(); }
CompactDisc的具体内容并不重要,重要的是你将其定义为一个接口。作为接口,它定义了CD播放器对一盘CD所能进行的操作。它将CD播放器的任意实现与CD本身的耦合降低到了最小的程度。
代码清单2:带有@Component注解的CompactDisc实现类SgtPeppers
package spring.soundsystem; import org.springframework.stereotype.Component; @Component() public class SgtPeppers implements CompactDisc { private String title="《天下》"; private String artist="张杰"; public void play() { System.out.println("Playing "+title+" by "+artist); } }
@Component注解表明该类会作为组件类,不过,组件扫描默认是不启用的。我们还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。程序清单3的配置类展现了完成这项任务的最简洁配置。
代码清单3:@ComponentScan注解启用了组件扫描
package spring.soundsystem;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class CDPlayerConfig { }
@ComponentScan默认会扫描与配置类相同的包以及这个包下的所有子包,查找带有@Component注解的类,这样的话,就能发现CompactDisc,并且会在Spring中自动为其创建一个bean。
如果你更倾向于使用XML来启用组件扫描的话,那么可以使用Spring context命名空间的<context:component-scan>元素。代码清单4展示了启用组件扫描的最简洁XML配置。
代码清单4:通过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="spring.soundsystem"/> </beans>
代码清单5:测试组件扫描能够发现CompactDisc
package spring.soundsystem.test; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import spring.soundsystem.CDPlayerConfig; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=CDPlayerConfig.class) //@ContextConfiguration(locations="classpath:applicationcontext.xml") public class CDPlayerTest { @Autowired private CompactDisc cd; @Test public void play(){ cd.play(); } }
CDPlayerTest使用了Spring的SpringJUnit4ClassRunner,以便在测试开始的时候自动创建Spring的应用上下文。
2.2.2 为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID。具体来讲,这个bean所给定的ID为sgtPeppers,也就是将类名的第一个字母变为小写。
如果想为这个bean设置不同的ID,你所要做的就是将期望的ID作为值传递给@Component注解。例如命名为lonelyHeartsClub:
@Component("lonelyHeartsClub") public class SgtPeppers implements CompactDisc { }
2.2.3 设置组件扫描的基础包
我们没有为@ComponentScan设置任何属性。这意味着,按照默认规则,它会以配置类所在的包作为基础包(base package)来扫描组件.。以下方式可指定不同的包:
@Configuration @ComponentScan("spring.soundsystem") public class CDPlayerConfig { }
或者通过basePackages熟悉进行设置,这种方式可以设置多个基础包:
@Configuration @ComponentScan(basePackages="spring.soundsystem") public class CDPlayerConfig { }
除了将包设置为简单的String类型之外,@ComponentScan还提供了另外一种方法,那就是将其指定为包中所包含的类或接口:
@Configuration @ComponentScan(basePackageClasses= {SgtPeppers.class}) public class CDPlayerConfig { }
在你的应用程序中,如果所有的对象都是独立的,彼此之间没有任何依赖,就像SgtPeppersbean这样,那么你所需要的可能就是组件扫描而已。但是,很多对象会依赖其他的对象才能完成任务。这样的话,我们就需要有一种方法能够将组件扫描得到的bean和它们的依赖装配在一起。要完成这项任务,我们需要了解一下Spring自动化配置的另外一方面内容,那就是自动装配。
2.2.4 通过为bean添加注解实现自动装配
为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。
代码清单6:通过自动装配,将一个CompactDisc注入到CDPlayer之中
package spring.soundsystem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class CDPlayer implements MediaPlayer { private CompactDisc cd; @Autowired() public CDPlayer(CompactDisc cd) { this.cd=cd; } public void play() { cd.play(); } }
@Autowired注解不仅能够用在构造器上,还能用在属性的Setter方法上。比如说,如果CDPlayer有一个setCompactDisc()方法,那么可以采用如下的注解形式进行自动装配:
@Autowired() public void setCompactDisc(CompactDisc cd){ this.cd=cd; }
实际上,Setter方法并没有什么特殊之处。@Autowired注解可以用在类的任何方法上。假设CDPlayer类有一个insertDisc()方法,那么@Autowired能够像在setCompactDisc()上那样,发挥完全相同的作用:
@Autowired() public void InsertDisc(CompactDisc cd){ this.cd=cd; }
如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,你可以将@Autowired的required属性设置为false:
@Autowired(required=false) public CDPlayer(CompactDisc cd) { this.cd=cd; }
将required属性设置为false时,如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。但是,如果在你的代码中没有进行null检查的话,这个处于未装配状态的属性有可能会出现NullPointerException。
如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。
2.2.5 验证自动装配
现在,我们已经在CDPlayer的构造器中添加了@Autowired注解,Spring将把一个可分配给CompactDisc类型的bean自动注入进来。为了验证这一点,让我们修改一下CDPlayerTest,使其能够借助CDPlayer bean播放CD:
package spring.soundsystem.test; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import spring.soundsystem.CDPlayerConfig; import spring.soundsystem.MediaPlayer; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = CDPlayerConfig.class) // @ContextConfiguration(locations="classpath:applicationcontext.xml") public class CDPlayerTest { @Autowired private MediaPlayer player; @Test public void play() { player.play(); } }
2.3 通过Java代码装配bean
尽管在很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。
在进行显式配置的时候,有两种可选方案:Java和XML。就像我之前所说的,在进行显式配置时,JavaConfig是更好的方案,因为它更为强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其他Java代码一样。
2.3.1 创建配置类
package spring.soundsystem; import org.springframework.context.annotation.Configuration; @Configuration public class CDPlayerConfig { }
创建JavaConfig类的关键在于为其添加@Configuration注解,@Configuration注解表明这个类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节。到此为止,我们都是依赖组件扫描来发现Spring应该创建的bean。尽管我们可以同时使用组件扫描和显式配置,但是在本节中,我们更加关注于显式配置,因此我将CDPlayerConfig的@ComponentScan注解移除掉了
移除了@ComponentScan注解,此时的CDPlayerConfig类就没有任何作用了。
2.3.2 声明简单的bean
要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解。比方说,下面的代码声明了CompactDisc bean:
@Bean public CompactDisc sgtPeppers(){ return new SgtPeppers(); }
@Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。
请稍微发挥一下你的想象力,我们可能希望做一点稍微疯狂的事情,比如说,在一组CD中随机选择一个CompactDisc来播放:
现在,你可以自己想象一下,借助@Bean注解方法的形式,我们该如何发挥出Java的全部威力来产生bean。当你想完之后,我们要回过头来看一下在JavaConfig中,如何将CompactDisc注入到CDPlayer之中。
2.3.3 借助JavaConfig实现注入
在JavaConfig中装配bean的最简单方式就是引用创建bean的方法。例如,下面就是一种声明CDPlayer的可行方案:
@Bean public CDPlayer cdPlayer(){ return new CDPlayer(sgtPeppers()); }
cdPlayer()方法像sgtPeppers()方法一样,同样使用了@Bean注解,这表明这个方法会创建一个bean实例并将其注册到Spring应用上下文中。所创建的bean ID为cdPlayer,与方法的名字相同。
默认情况下,Spring中的bean都是单例的,因此,两个CDPlayer bean会得到相同的SgtPeppers实例。如下:
@Bean public CDPlayer cdPlayer(){ return new CDPlayer(sgtPeppers()); } @Bean public CDPlayer anothercdPlayer(){ return new CDPlayer(sgtPeppers()); }
假如对sgtPeppers()的调用就像其他的Java方法调用一样的话,那么每个CDPlayer实例都会有一个自己特有的SgtPeppers实例。如果我们讨论的是实际的CD播放器和CD光盘的话,这么做是有意义的。如果你有两台CD播放器,在物理上并没有办法将同一张CD光盘放到两个CD播放器中。
但是,在软件领域中,我们完全可以将同一个SgtPeppers实例注入到任意数量的其他bean之中。默认情况下,Spring中的bean都是单例的,我们并没有必要为第二个CDPlayer bean创建完全相同的SgtPeppers实例。所以,Spring会拦截对sgtPeppers()的调用并确保返回的是Spring所创建的bean,也就是Spring本身在调用sgtPeppers()时所创建的
CompactDiscbean。因此,两个CDPlayer bean会得到相同的SgtPeppers实例。
可以看到,通过调用方法来引用bean的方式有点令人困惑。其实还有一种理解起来更为简单的方式:
在这里,cdPlayer()方法请求一个CompactDisc作为参数。当Spring调用cdPlayer()创建CDPlayerbean的时候,它会自动装配一个CompactDisc到配置方法之中。然后,方法体就可以按照合适的方式来使用它。借助这种技术,cdPlayer()方法也能够将CompactDisc注入到CDPlayer的构造器中,而且不用明确引用CompactDisc的@Bean方法。
通过这种方式引用其他的bean通常是最佳的选择,因为它不会要求将CompactDisc声明到同一个配置类之中。在这里甚至没有要求CompactDisc必须要在JavaConfig中声明,实际上它可以通过组件扫描功能自动发现或者通过XML来进行配置。你可以将配置分散到多个配置类、XML文件以及自动扫描和装配bean之中,只要功能完整健全即可。不管CompactDisc是采用什么方式创建出来的,Spring都会将其传入到配置方法中,并用来创建CDPlayerbean。
再次强调一遍,带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。构造器和Setter方法只是@Bean方法的两个简单样例。这里所存在的可能性仅仅受到Java语言的限制。
2.4 通过XML装配bean
2.4.1 创建XML配置规范
借助Spring Tool Suite创建XML配置文件创建和管理Spring XML配置文件的一种简便方式是使用Spring Tool Suite。在Spring Tool Suite的菜单中,选择File>New>Spring Bean Configuration File,能够创建Spring XML配置文件,并且可以选择可用的配置命名空间。
2.4.2 声明一个简单的<bean>
<bean>元素类似于JavaConfig中的@Bean注解。我们可以按照如下的方式声明CompactDiscbean:
在XML配置中,bean的创建显得更加被动,不过,它并没有JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。
另外一个需要注意到的事情就是,在这个简单的<bean>声明中,我们将bean的类型以字符串的形式设置在了class属性中。谁能保证设置给class属性的值是真正的类呢?Spring的XML配置并不能从编译期的类型检查中受益。即便它所引用的是实际的类型,如果你重命名了类,会发生什么呢?
以上介绍的只是JavaConfig要优于XML配置的部分原因。我建议在为你的应用选择配置风格时,要记住XML配置的这些缺点。
2.4.3 借助构造器注入初始化bean
在Spring XML配置中,只有一种声明bean的方式:使用<bean>元素并指定class属性。Spring会从这里获取必要的信息来创建bean。
但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:
(1)<constructor-arg>元素
(2)使用Spring 3.0所引入的c-命名空间
两者的区别在很大程度就是是否冗长烦琐。可以看到,<constructor-arg>元素比使用c-命名空间会更加冗长,从而导致XML更加难以读懂。另外,有些事情<constructorarg>可以做到,但是使用c-命名空间却无法实现。
构造器注入bean引用
现在已经声明了SgtPeppers bean,并且SgtPeppers类实现了CompactDisc接口,所以实际上我们已经有了一个可以注入到CDPlayerbean中的bean。我们所需要做的就是在XML中声明CDPlayer并通过ID引用SgtPeppers:
在c-命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示:
c-命名空间是在Spring 3.0中引入的,它是在XML中更为简洁地描述构造器参数的方式。要使用它的话,必须要在XML的顶部声明其模式。下图描述了这个属性名是如何组合而成的。
迄今为止,我们所做的DI通常指的都是类型的装配——也就是将对象的引用装配到依赖于它们的其他对象之中——而有时候,我们需要做的只是用一个字面量值来配置对象。为了阐述这一点,假设你要创建CompactDisc的一个新实现,如下所示:
现在,我们可以将已有的SgtPeppers替换为这个类:现在,我们可以将已有的SgtPeppers替换为这个类:
我们再次使用<constructor-arg>元素进行构造器参数的注入。但是这一次我们没有使用“ref”属性来引用其他的bean,而是使用了value属性。
装配集合
到现在为止,我们假设CompactDisc在定义时只包含了唱片名称和艺术家的名字。如果现实世界中的CD也是这样的话,那么在技术上就不会任何的进展。CD之所以值得购买是因为它上面所承载的音乐。大多数的CD都会包含十多个磁道,每个磁道上包含一首歌。如果使用CompactDisc为真正的CD建模,那么它也应该有磁道列表的概念。请考虑下面这个新的BlankDisc:
我们可以有多个可选方案。首先,可以使用<list>元素将其声明为一个列表:我们可以有多个可选方案。首先,可以使用<list>元素将其声明为一个列表:
与之类似,我们也可以使用<ref>元素替代<value>,实现bean引用列表的装配。例如,假设你有一个Discography类,它的构造器如下所示:
那么,你可以采取如下的方式配置Discography bean:
当构造器参数的类型是java.util.List时,使用<list>元素是合情合理的。尽管如此,我们也可以按照同样的方式使用<set>元素:
<set>和<list>元素的区别不大,其中最重要的不同在于当Spring创建要装配的集合时,所创建的是java.util.Set还是java.util.List。如果是Set的话,所有重复的值都会被忽略掉,存放顺序也不会得以保证。不过无论在哪种情况下,<set>或<list>都可以用来装配List、Set甚至数组。
2.4.4 设置属性
到目前为止,CDPlayer和BlankDisc类完全是通过构造器注入的,没有使用属性的Setter方法。接下来,我们就看一下如何使用Spring XML实现属性注入。假设属性注入的CDPlayer如下所示:
该选择构造器注入还是属性注入(set)呢?作为一个通用的规则,我倾向于对强依赖使用构造器注入,而对可选性的依赖使用属性注入。
<property>元素为属性的Setter方法所提供的功能与<constructor-arg>元素为构造器所提供的功能是一样的。在本例中,它引用了ID为compactDisc的bean(通过ref属性),并将其注入到compactDisc属性中(通过setCompactDisc()方法)。
2.5 导入和混合配置
可以使用@Import将两个配置类组合在一起。
2.6 小结
Spring框架的核心是Spring容器。容器负责管理应用中组件的生命周期,它会创建这些组件并保证它们的依赖能够得到满足,这样的话,组件才能完成预定的任务。
装配bean的三种主要方式:自动化配置、基于Java的显式配置以及基于XML的显式配置。不管你采用什么方式,这些技术都描述了Spring应用中的组件以及这些组件之间的关系。
我同时建议尽可能使用自动化配置,以避免显式配置所带来的维护成本。但是,如果你确实需要显式配置Spring的话,应该优先选择基于Java的配置,它比基于XML的配置更加强大、类型安全并且易于重构。
本文代码链接:https://github.com/Gugibv/spring