初识Spring的DI及其基本用法

作为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

(暂略)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容