作为Spring新手,边学《Spring in Action》边总结相关知识。
什么是DI
DI,Dependency Injection,即依赖注入,不是去依赖“注入”这个东东,而是将“依赖”这个东东给注入。
那么什么是依赖?我们都知道,一个稍微大一点的应用程序,它都是由若干个对象组成的,这些对象如果各干各的谁也不理谁,那工作怎么可能完成呢!所以这些对象肯定都是意识到了一些其它对象的存在,并且要和它们交流通信,朝着共同的目标去努力,这样才可能达成目标,正所谓众志成城也。从编程的角度来说,这些对象之间存在着依赖关系。
传统的建立这些依赖关系的方法,是让对象自己去记录、维护自己所依赖的对象,这本是一些本不属于它们自己工作范围的事情。这样也会增加对象之间的耦合度,使得它们难以复用、难以测试。
而在Spring里面,对象自己不需要负责去寻找或是创建它们所依赖的对象,而是由容器(container)来维护对象之间的引用关系。举个栗子,订单管理模块可能会需要一个信用卡授权模块,但是它不需要去创建这个信用卡授权模块——它只需要两手空空地现身,自然会有人给它一个信用卡授权模块。
这种创建应用程序对象之间的依赖关系的行为,就是DI的本质,也经常被称作装配(wiring),被装配到一起的对象,称为bean。有很多装配的方法,首先可以来感受一下配置Spring容器的三种最常见的方法。
配置Spring容器
虽然容器要负责创建beans,并且通过DI来协调这些对象之间的关系,但当然也得靠我们程序员来告诉Spring,要创建哪些beans,怎样把它们装配到一起等等。Spring提供三种机制来让我们做这件事:
- 通过XML显式配置
- 通过Java显式配置
- 隐式进行bean搜索并自动装配
上述三种方法该如何选择呢?书作者Walls的建议是,尽量使用自动配置,需要的显式说明越少就越好。如果必须显式配置(比如当你没有你要配置的beans的源代码时),通过Java配置更理想,因为它类型安全且功能更强。只有当存在方便的XML命名空间可用,而Java配置中又没有可替代者时,才考虑用XML配置。
接下来依次学习这三种机制的使用方法。
一、自动装配beans
Spring从两个方面来实现自动的装配:
- 组件扫描(Component scanning)——Spring自动找寻需要在应用程序上下文中创建的beans。
- 自动装配(Autowiring)——Spring自动满足bean的依赖。
以上两者组合在一起,就可以实现强大的自动装配的功能,将显式的配置说明控制到最少。具体来说,可以通过@Component、@ComponentScan、@Autowired注解来实现自动装配,下面分别介绍它们的作用。
@Component
被@Componet修饰的类,Spring为会其创建一个bean。Spring的应用程序上下文里,所有的beans都有一个ID。当@Component不加参数时,为该类生成的ID就是其类名(首字母小写);也可以加参数,如
@Component("someCoolName")
public class SomeClass {}
生成的bean的ID就是someCoolName了。
但是组件扫描并不是默认开启的,所以还需要写一点显式的配置说明,告诉Spring去找寻被@Component修饰的类,为它们创建beans。
@ComponentScan
如果存在被@ComponentScan修饰的类,那么Spring就会去扫描找寻组件来生成bean(也可以通过XML文件的方式来配置组件扫描)。
当@ComponentScan不加参数时,扫描范围就是该类所在的包。可以加字符串参数,如
@ComponentScan(basePackages = "somepackage")
public class ConfigurationClass {}
这样扫描范围就是somepackage包;参数还可以是字符串数组,如
@ComponentScan(basePackages = {"somepackage", "anotherpackage"})
public class ConfigurationClass {}
扫描范围就变成了多个包。除了字符串,参数还可以是类或接口,如
@ComponentScan(basePackageClasses = {Class1.class, Class2.class})
public class ConfigurationClass {}
这样扫描范围就是这些类所在的包。相比于传字符串形式的参数,传Java类类型的参数更加类型安全。另外,虽然这里用的是一个类来作为@ComponentScan的标记类,但更推荐用一个空的接口来做标记,这样可以更加“重构友好”(refactor-friendly)地引用接口,而不用引用任何实际的程序代码(它们以后可能会被重构到你想要进行组件扫描的包之外)。
如果应用程序中所有对象都没有依赖,那么靠组件扫描就够了。但很多对象都是依赖其它对象的,因此在装配beans的时候,也需要把它们具有的所有依赖都一同装配进来。
@Autowired
简单地说,自动装配就是让Spring自动去满足bean的需求,也就是在应用程序上下文里去寻找这个bean所需要的其它的beans。@Autowired注解就是用来告诉Spring,需要进行自动装配(也可以用Java自带的@Inject注解,两者存在细微区别,但基本可以互换)。如下述的CDPlayer类:
@Component
public class CDPlayer implements MediaPlayer {
private CompactDisc cd;
@Autowired
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
它的构造方法有@Autowired注解,表示当Spring创建CDPlayer的bean时,应该用该构造方法来实例化,并且传入一个CompactDisc的bean。
其实不光是构造方法,任何方法都可以用@Autowired注解,Spring就会去尝试满足该方法的参数所表达的依赖。如果没有bean满足匹配,Spring就会在应用程序上下文被创建的时候扔一个异常。可以通过设置@Autowired的required参数来避免异常:
@Autowired(required=false)
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
当required为false时,Spring仍然会尝试去自动装配,但是如果没有匹配的bean,它就不会装配这个等待被装配的bean。要小心这样设置,因为未装配的属性可以导致空指针异常。
如果存在不只一个bean满足匹配,Spring就会扔一个表示歧义的异常,当然有办法可以管理并避免歧义,这里就不讨论了。
二、通过Java装配beans
尽管大多数情况下,通过组件扫描和自动装配是更好的方法,但有些情况下无法使用自动配置,你将不得不显式地进行配置。比如你想要将第三方库中的组件装配进程序中,但是没有它们的源代码,也就无法给它们打上@Component这些注解,于是自动配置不可行。
通过JavaConfig配置比通过XML配置更为推荐,前者功能更强,更加类型安全且重构友好,因为它就是Java代码。
但同时也要意识到,这些Java代码和程序中其它的Java代码又不一样,因为在概念上,它和程序中的业务逻辑、领域模型这些是分离的,它属于配置代码,因此不应该包含任何业务逻辑,也不应该侵入任何包含业务逻辑的代码。实际上,经常把这些配置代码放置于一个单独的包,这样就不会跟程序的其它逻辑混在一起。
接下来看看具体怎么用JavaConfig进行装配。
@Configuration
要创建一个JavaConfig类,就用@Configuration注解这个类,该注解将它标识为一个配置类,它应该包含需要在Spring应用程序上下文中创建的beans的详细信息。如:
@Configuration
public class CDPlayerConfig {}
它将CDPlayerConfig类标记为配置类。
@Bean
如果某方法被@Bean注解,那么Spring就知道该方法将会返回一个对象,该对象应该在Spring应用程序上下文中被注册为一个bean,方法体内包含着最终生成bean实例的代码逻辑。例如下面的代码声明了CompactDisc的bean:
@Bean
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
方法体返回了一个新的SgtPeppers实例(SgtPeppers继承自CompactDisc),事实上方法里面可以写任何Java代码,只要最后能返回一个CompactDisc实例。
默认情况下,这个bean会被赋予一个与@Bean注解的方法名相同的ID,上述样例中就是sgtPeppers。可以通过name属性来赋一个不同的名字:
@Bean(name="lonelyHeartsClubBand")
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
CompactDisc的bean比较简单,它自己没有依赖的对象。现在如果要声明一个CDPlayer的bean,它是依赖一个CompactDisc对象的,应该怎样来装配呢?
JavaConfig中最简单的做法就是调用所需bean的@Bean方法,还是举例说明,你可以这样声明CDPlayer的bean:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
cdPlayer()方法像sgtPeppers()方法一样,也有@Bean注解,以此来表示它将会产生一个要在Spring应用程序上下文中注册的bean实例,这个bean的ID是cdPlayer,与方法名相同。
cdPlayer()的方法体与sgtPeppers()的有着细微的不同,前者并没有通过默认构造方法来创建实例,而是调用了有一个CompactDisc参数的构造方法来创建CDPlayer的实例。
看上去CompactDisc的实例是通过调用sgtPeppers()方法来提供的,但并不是这样。因为sgtPeppers()方法有@Bean注解,Spring就会拦截任何对它的调用,并确保该方法提供的bean被返回,而不是让它再被调用一次。
继续举栗子,假设你又引进了另一个CDPlayer的bean,和第一个一模一样:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
@Bean
public CDPlayer anotherCDPlayer() {
return new CDPlayer(sgtPeppers());
}
如果对sgtPeppers的调用被当成与其它普通Java方法的调用一样,那么每一个CDPlayer都会被给予一个它自己的SgtPeppers实例。如果我们谈论的是真实的CD播放机和压缩碟片,这倒是有意义的,因为当你有两个CD播放机时,不可能将一张碟片同时插入到两个播放器中。
但在软件中,你可以随意将同一个SgtPeppers的实例注入到任意多个其它的beans里面去。默认情况下,Spring中的所有beans都是单例,也没有什么原因需要你为第二个CDPlayer的bean再创建一个重复的实例,所以Spring会阻止对sgtPeppers()的调用,并确保返回的bean是当Spring自己调用sgtPeppers()时所创建的CompactDisc bean。因此,两个CDPlayer的beans都会被给予同一个SgtPeppers的实例。
如果对通过调用其方法来引用一个bean感到迷惑,另一种方式也许更容易让人理解:
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
return new CDPlayer(compactDisc);
}
这里,cdPlayer()方法需要一个CompactDisc作为参数,当Spring调用cdPlayer()来创造CDPlayer bean的时候,它将一个CompactDisc自动装配进配置方法,然后方法体内可以在任何适当的时候使用该bean。利用这个机制,cdPlayer()方法仍然可以将CompactDisc注入CDPlayer的构造方法中去,而无需显式地引用CompactDisc的@Bean方法。
这一引用其它beans的方法通常是最好的选择,因为它不依赖在同一个配置类中声明的CompactDisc bean。事实上,CompactDisc bean也完成可以不用通过JavaConfig来声明,它可以被组件扫描所发现,也可以在XML中声明。你还可以将你的配置拆成一个健壮的混合体,将配置类、XML文件、自动扫描及装配的beans这三者融合起来。不管CompactDisc是怎样创建的,Spring都乐于将其交给这个配置方法,用来创建CDPlayer的bean。
任何情况下都有必要意识到,尽管你是在通过CDPlayer的构造方法进行DI,在这里你也不是不可以应用其它形式的DI。例如,当你想要通过setter方法来注入一个CompactDisc,cdPlayer()也许就是下面这样了:
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
CDPlayer cdPlayer = new CDPlayer(compactDisc);
cdPlayer.setCompactDisc(compactDisc);
return cdPlayer;
}
现在又要重复提醒一下,一个@Bean方法的主体部分可以使用任何需要的Java代码来生成bean实例。构造方法和setter方法的注入只是其中两个简单的例子,你能在一个@Bean注解的方法里做的事情多了去了,唯一的限制也就只有Java语言本身的能力了。
三、通过XML装配beans
(暂略)