Spring 实战 第二章 组装Bean

这一章包含下面几个内容:

  • 声明Bean
  • 构造器注入和Setter函数注入
  • 组装Bean
  • 控制Bean的创建和销毁

任何不平凡的应用程序都是由若干个必须统统工作来满足某个业务目标的对象组成的。这些对象必须彼此了
解并与其他的对象进行沟通以完成他们的工作。比如在一个在线购物应用中,一个订单管理组件需要与一个
产品管理组件和信用卡授权组件进行工作,所有的这些组件都需要一个数据访问组件来进行数据库的读取和
写入工作。

但是,如第一章看到的一样,传统的应用程序处理这个关系是复杂的,并且对象是不利于重用和难于测试
的。最好的情况下,这些对象可以做比她们应该做的更多的工作,最坏的情况是,它们相互耦合,导致难以
理解和测试。

在Spring中,对象不负责去寻找和创建需要她们工作的对象。替代的是,容器会管理她们与他们合作的对象
的引用。例如,一个订单管理组件需要一个信用卡授权,但是不需要去创建信用卡授权,它只需要一个能进
行信用卡授权的引用就可以了。

创建两个应用程序对象的管理是通过Spring 的DI实现,通常叫着装配(wiring),在本章中,我们会学习Spring的Bean装配。你将会看到的这些技术中,DI是这些技术的基础。

在Spring中,有很多中方式去装配Bean,接下来,让我们看看最常见的三种配置Spring容器的方式。

2.1 探索Spring的配置选项

像第一章描述的那样,Spring容器负责通过DI创建Bean并且组织这些Bean之间的关系。但是作为开发人员
的你的职责是告诉Spring哪些Bean需要创建,怎么样将这些Bean组装起来。当涉及到表达一个Bean装配的
规范时,Spring是令人难以置信的灵活,它提供了三种装配方式:

  • 使用XML显式配置
  • 使用Java显式配置
  • 隐式Bean发现和自动配置

咋一看,它似乎提供给了三种配置方案,导致了Spring的复杂。每一个方案提供了技术方面有一些重复的,
并且决定哪种技术适用于某一情况是必须的。但是别担心,许多情况下,选择都是根据个人爱好来的。你可
以选择一种最符合你口味的配置方法。

在Spring中,这个是比较伟大的,你可以选择很多方式去装配你的Bean,但是有些时候,你必须选择一种方
案。

这里没有一个绝对正确的答案。你可以选择任何方案,只要它适合你和你的项目。也没有人必须让你做出只
能选择一个的已选?Spring的配置方案可以混合起来使用,所以,你可以让某些Bean使用XML配置,让某
些Bean使用Java配置并且其他的Bean使用自动发现来创建。

即使如此,我们的建议是尽可能多的使用自动配置。最好的方式是你有最少的显示配置。当你必须显示配置
Bean的时候(比如,你配置Bean,但是源代码不是你维护),我更加喜欢安全和更加强大的基于Java的配
置而不是XML的配置。最好,需要使用XML配置是在有一个方面的XML命名空间,但是这个时候没有对应的
Java配置项等价的时候。

所以,配置的考虑优先级为:自动配置>Java配置>XML配置

在本章中,我们会详细学习者三种配置方法,并在本书其他部分也会使用。在这一点上,让我们先试试,每个人都可以得到自己喜欢的方式,首先,让我们看看Spring的自动配置。

2.2 自动配置

在本章后面有一小部分,介绍了XML和JAVA的混合配置。即使你会发现很多显式的配置,但是没有什么比
Spring提供的自动配置更加方面的。在Spring中,能够自动配置的时候,为什么还要显式的声明这些装配
呢?

Spring的自动配置有两个方面:

  • 组件扫描(Component Scan)–在应用上下文中,Spring可以自动发现Bean并创建它
  • 自动装配(AutoWiring)–Spring自动满足Bean依赖

这两个一起工作,组件扫描和自动装配是相当强大的武器,它使得Spring的显示配置能够减到最少。为了描述组件扫描和自动装配,你必须再系统中创建一些Bean.你会创建一个CompactDisc类,Spring会发现并创建它。然后你创建一个CDPlayer类,Spring会发现它并将它注入进CompactDisc Bean里面。

创建可发现的Bean

在这个时代,MP3文件、流媒体音乐和光盘可能看起来有点怪异和陈旧。当然,没有那么多磁带、八个轨道或者其他记录。但光盘是越来越稀缺的物理音乐交付模式。

尽管如此,对于DI,CD提供了一个很好的说明。除非你插入一张CD进CD播放器,否则CD播放器是没有用的,你可以说,一个CD播放器依赖于一个CD,为了实现它的功能。在Java中,CompactDisc是一个CD的接口,如下:

package soundsystem;

/**
 * Created by Robert.wang on 2016-3-10-0010.
 */
public interface CompactDisc {
    void play();
}

CompactDis具体是什么是不重要的,重要的是你已经将它定义为了一个接口。作为一个接口,他定义了一个约束,使得在CD 播放器上可以播放它。并且它保持了任何CD播放器的实现和CD本身的耦合降到最低。

实际上,你可以有很多CompactDis的实现,在本例中,你可以有一个实现类,SgtPeppers,如下:

package soundsystem;

import org.springframework.stereotype.Component;

/**
 * Created by Robert.wang on 2016-3-10-0010.
 */
@Component
public class SgtPeppers implements CompactDisc {
    private String title = "Sgt. Pepper's Lonely Hearts Club Band";
    private String artist = "The Beatles";

    @Override
    public void play() {
        System.out.println("Playing " + title + " by " + artist);
    }
}

像CompactDisc一样,SgtPeppers的细节不是本讨论的重点。你需要注意的是SgtPeppers上有一个@Component的注解。这个简单的组件将该类标识为一个组件类,并且在Spring中,它可以被发现并被创建,所以不需要进行显示地配置SgtPeppers的Bean,Spring会自动装配它。

然而,在Spring中,Component Scan不是默认的,你始终需要去显示的配置,告诉Spring去扫描这些具有Component组件的类并为他们创建一个Bean。这个尽可能最小化的配置类如下:

package soundsystem;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * Created by Robert.wang on 2016-3-10-0010.
 */
@Configuration
@ComponentScan
public class CDPlayerConfig {
}

CDPlayerConfig定义了Spring的装配规范,我们将会看到,基于Java的配置比2.3节讲的更加简单,但是,
现在,观察CDPlayerConfig,它没有显式定义任何Bean,而是通过ComponentScan注解是的Spring的自
动配置起了作用。没有了更多的配置,@ComponentScan注解会默认扫描与配置类相同的包下面的类,因
为CDPlayerConfig在包soundsystem下面,所以Spring会去扫描soundsystem包下面的类以及
soundsystem子包下面的类,去发现具有@Component注解的类。它应该可以发现 CompactDisc类并为
其创建一个Bean。

如果你使用XML配置,你可以使用在context命名空间下的元素,最小的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="soundsystem"/>
</beans>

尽管使用XML是一个可选的方案,但是在本节,我会将重点放在基于Java的配置上。不管相信与否,有两个
类已经创建了,接下来你可以测试测试。为了测试,我们使用JUnit。如下:

package soundsystem;

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 static org.junit.Assert.*;

/**
 * Created by Robert.wang on 2016-3-10-0010.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
public class CompactDiscTest {

    @Autowired
    private CompactDisc cd;

    @Test
    public void cdShouldNotBeNull() {
        assertNotNull(cd);
    }

    @Test
    public void testPlay() throws Exception {
        cd.play();
    }
}

CompactDiscTest利用Spring的SpringJUnit4ClassRunner,它在测试开始的时候自动创建Spring的上下文
环境,@ContextConfiguration告诉它从CDPlayerConfig类中加载配置文件,因为CDPlayerConfig中包含
了@ComponentScan的注解,所以上下文可以发现CompactDisc并且创建一个Bean。

为了验证,CompactDisc通过使用AutoWired注解自动注入Bean到测试中(等会儿会深入讨论
@AutoWired),最后,一个简单的测试方法测试cd属性是否为null,如果不为null,则说明,Spring可以
自动发现CompactDisc类并自动创建Bean,并将Bean自动注入到测试中来。

测试应该会通过,你的第一个简单的ComponentScan的联系是成功的!尽管你只是使用它来创建一个简单
的Bean,只要在soundsystem包下面的使用@Component注解的类都可以被发现并且创建Bean。一个
@ComponentScan注解对于创建许多Bean来说是一个更好的选择。

现在让我们深入理解@ComponentScan和@Component,看看在Component Scan过程中你可以做什
么。

2.2.2 命名一个component-scan的Bean

在所有Spring application context中的Bean都有一个ID,那在上面的例子中SgtPeppers的Bean的ID是什
么呢?它来源于SgtPeppers的类名,其实,他是sgtPeppers,他是类名的第一个字母小心得来的。如果你
需要给予一个不同的ID,你可以在@Component注解中显式声明,比如你需要使用lonelyHeartsClub作为
ID,那么你可以使用下面的语法。

@Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
...
}

给Bean命名的另外一种方式就是使用@Named注解(该注解是在JDI标准 JSR-330中)而不是使用
@Component注解,如下:

@Named("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
...
}

Spring支持使用@Named注解作为@Component注解的另外一种方式,他们之间有一些不同,但是在大多
数情况下,他们是可以互换的。

在大多数情况下,我强烈建议使用@Component注解而不使用@Named注解,主要是因为它的命名不太好,它没有像@Component注解一样说明它描述的是什么,因此在本书其余部分,我们不会使用它。

2.2.3 为ComponentScan设置一个BasePackage

到目前为止,你可以使用一个无属性的@ComponentScan。那就是说,默认去扫描配置文件包下的类,但
是,如果你想要去扫描不同的包,或者扫描许多包呢?

显示设置包的一个常见原因是,你可以将所有的配置代码保留在一个包中,从而与源代码进行分离,在这种
情况下,默认包就不能满足要求了。

没有问题,为了满足需要,你只需要在@ComponentScan注解中填写上它的value属性就可以了。如下:

@Configuration
@ComponentScan("soundsystem")
public class CDPlayerConfig {}

或者,你如果想要标识更加清楚,指明其实basepackage,你可以设置basepackages属性,如下:

@Configuration
@ComponentScan(basePackages="soundsystem")
public class CDPlayerConfig {}

你可能会注意到,basePackages是复数,那是否意味着你可以标识多个basePackage呢?没错,只需要将
其值设置成数组就可以了,如下:

@Configuration
@ComponentScan(basePackages={"soundsystem", "video"})
public class CDPlayerConfig {}

如上,basePackages只有一个问题就是他的值是String,也就是说它不具有类型安全特性,如果你重构软甲
包的名称,指定的值就错了。

代替basePackage用String作为值,@ComponentScan也提供了另外一种属性,即baseClasses,它是通
过类或者接口来标明的。

@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
public class CDPlayerConfig {}

像你看到的一样,这些类的包将作为ComponentScan的基包。

使用basePackageClasses指定组件的类,你可以考虑在包下面创建一个空的包接口用于Component
Scan,使用这种方式,当重构的时候就会很方便。

如果所有的对象都是独立的,像上面例子中一样,那么 ComponentScan就可以满足你的需要。但是很多应
用程序中,对象是依靠其他对象才能实现他们的功能的,你需要去装配你通过ComponentScan发现的
Bean。为了实现这个功能,我们接下来学习autowiring,即自动装配。这个是Spring自动配置的另一个方
面。

2.2.4 通过注解(@AutoWired)自动连接Bean

简单地说,就是让Spring自动发现Bean的依赖项,并且在ApplicationContext中自动寻找依赖项并且注入
进Bean去的一种手段。为了实现这个功能,可以使用Spring的@AutoWired注解。

比如下面,它的构造函数被AutoWired注解,意味着当Spring创建CDPlayer的时候,它需要一个
CompactDisc的Bean传递进来,那就需要Spring去创建CompactDisc的实例。

package soundsystem;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Created by Robert.wang on 2016-3-10-0010.
 */
@Component
public class CDPlayer implements MediaPlayer {
    private CompactDisc cd;

    @Autowired
    public CDPlayer(CompactDisc cd) {
        this.cd = cd;
    }

    @Override
    public void play() {
        cd.play();
    }
}

@AutoWired的使用不限制为构造函数,它也可以使用在属性设置器中,比如,如果CDPlayer有一个
setCompactDisc的函数,你可以使用@AutoWired如下:

@Autowired
public void setCompactDisc(CompactDisc cd) {
this.cd = cd;
}

当Spring初始化Bean之后,它会尽量满足通过@AutoWired注解的方法setCompactDisc得到调用。其实,
没有什么特殊的设置函数,@AutoWired可以使用到类的任何方法上面,加入CDPlayer有一个
insertCompactDisc的方法,@AutoWired也可以应用上去。如:

@Autowired
public void insertDisc(CompactDisc cd) {
this.cd = cd;
}

不管是构造函数、设置器或者其他方法,Spring都会尝试去满足方法参数中所表示的依赖关系。加入只有一
个Bean满足条件,那么就会用这个Bean。如果没有Bean满足,当ApplicationContext创建的时候,Spring
会抛出异常。为了避免该异常,你可以设置@AutoWired主机的required属性为false。如:

@Autowired(required=false)
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}

当required属性为false时,Spring会尝试autowiring,但是如果没有Bean满足,它会不管该wiring。然
而,使用required属性时候必须特别小心,在代码中必须检查属性是否为null,如果不检查,会抛出
NullPointerExceptions异常。

如果有多个Bean满足,Spring也会抛出异常,指示选择歧义。在第三章中会详细介绍这种情况。

@AutoWired是Spring规范的注解,如果你烦Spring的注解,你可以考虑使用@Inject注解。如:

package soundsystem;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class CDPlayer {
...
@Inject
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
...
}

@Inject与@Named一样,来自于JDI规范。Spring支持@Inject注解,它跟@Autowired差不多。尽管他们
有一些小小的不同,但是多数情况下,他们是可以互换的。

这里,使用@Inject和@AutoWired,我没有什么特别的建议,根据自己的爱好选择,最好保持一致性。

2.2.5 验证autowiring

现在,你使用@AutoWired注解了CDPlayer的构造函数,你可以假定Spring可以自动注入。接下来我们编
写一个测试来验证。

package soundsystem;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
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 static org.junit.Assert.*;

/**
 * Created by Robert.wang on 2016-3-10-0010.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
public class CDPlayerTest {
    @Rule
    public final StandardOutputStreamLog log = new StandardOutputStreamLog();

    @Autowired
    private MediaPlayer mediaPlayer;

    @Autowired
    private CompactDisc compactDisc;

    @Test
    public void cdShouldNotBeNull() {
        assertNotNull(compactDisc);
    }

    @Test
    public void play() {
        mediaPlayer.play();
        assertEquals(
                "Playing Sgt. Pepper's Lonely Hearts Club Band" +
                        " by The Beatles\r\n",
                log.getLog());
    }
}

上面的测试可以通过,现在你已经了解了ComponentScan和AutoWired,接下来我们讨论基于Java的配
置。

2.3 通过java配置Spring

尽管自动配置在很多情况下是很好的。但是有的时候,自动配置不能满足要求,这个时候就需要进行显式的
配置。比如,你需要从第三库里面装配Bean,因为你没有权利修改源码,所以你就没有机会在类里面添加
@AutoWired注解或者@Component注解,在这种情况下,你必须使用显式配置。显式配置有两种方式,
Java和XML,这一节,我们主要学习Java的配置。

就像我们之前提到的,基于Java的配置是由于基于XML的配置,以为它功能更加强大, 而且是类型安全和对
重构友好的。

2.3.1 创建一个Java的配置类

像前面的配置一样,下面为一个最简单的Java配置类。

package soundsystem;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CDPlayerConfig {
}

该配置类的关键是@Configuration注解,它表明该类是一个Spring配置类,里面是对Bean进行详细配置的
地方。

由于我们需要学习Bean的配置,我们将@ComponentScan注解去掉了,也就是让自动发现功能失效。这样
当运行之前的测试的时候,就会报BeanCreationException异常,因为CDPlayer的构造函数需要一个
CompactDisc的Bean作为参数,但是该Bean在Spring上下文中找不到,所以为抛出异常。

2.3.2 声明简单的Bean

在JavaConfig中,为了声明一个Bean,你需要创建一个方法,该方法返回你需要的Bean的类型。然后使用
@Bean来注解该方法。如下:

@Bean
public CompactDisc sgtPeppers() {
    return new SgtPeppers();
}

@Bean注解告诉Spring,该方法是用来产生Bean的方法,并且将该Bean注入到Spring上下文中去。方法的
主体就是返回你希望的Bean类型的实体。默认情况下,Bean的ID跟@Component得到的ID是一样的。如果
需要重新定义,指明@Bean注解的name属性就可以了,如下:

@Bean(name="lonelyHeartsClubBand")
public CompactDisc sgtPeppers() {
    return new SgtPeppers();
}

发挥你的想象,在这个函数的实体中,你可以做任何事情,比如像下面这样:

@Bean
public CompactDisc randomBeatlesCD() {
    int choice = (int) Math.floor(Math.random() * 4);
    if (choice == 0) {
        return new SgtPeppers();
    } else if (choice == 1) {
        return new WhiteAlbum();
    } else if (choice == 2) {
        return new HardDaysNight();
    } else {
        return new Revolver();
    }
}

2.3.3 使用JavaConfig进行注入

使用Java注入,只需要调用使用@Bean注解的方法即可,如下:

@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}

看起来好像是调用了sgtPeppers()方法,但是其实不是。它只是调用了sgtPeppers方法产生的Bean,并没
有重新调用sgtPeppers方法,比如:

@Bean
public CDPlayer cdPlayer() {
    return new CDPlayer(sgtPeppers());
}
@Bean
public CDPlayer anotherCDPlayer() {
    return new CDPlayer(sgtPeppers());
}

以上的两端代码,产生的CompactDisc是一样的,也就是说明了sgtPeppers被Spring处理过了,Spring将
对该函数的所有调用都直接返回了该函数产生的Bean,并不会提供重新调用该函数的方法。这个是有点迷惑
人的。Spring也提供了另外一种模式来进行注入,像下面这样:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
    return new CDPlayer(compactDisc);
}

这样,直接使用参数提供,Spring会获取该参数需要的Bean,然后注入进去。当然,通过这种方法注入之
后,在函数体中,你可以随便使用这个注入的Bean,比如:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
    CDPlayer cdPlayer = new CDPlayer(compactDisc);
    cdPlayer.setCompactDisc(compactDisc);
    return cdPlayer;
}

这里使用了设置器注入的模式。再一次说明,@Bean注解的方法体中可以使用任意有效的java表达式,并不
是只有构造器注入和设置器注入这两种方法,也可以使用其他的方法。

JavaConfig的能力没有限制,唯一的限制就是Java语言的限制。

2.4 使用XML进行配置

到目前为止,你已经知道了自动配置和基于JavaConfig的配置,但是还有一种方式,它是伴随着Spring的成
长而成长的。从Spring开始起,XMLConfig就是Spring配置的主要方式,在Spring中,有无数的
XMLConfig文件被创建,对于很多人来说,Spring跟XmlConfig基本上是可以等价的。

尽管这个是事实,但是我们要清楚,Spring不是只有XMLConfig这一种方式。现在Spring强烈推荐使用自动
配置和基于Java的配置,XMLConfig应该不是你的第一选择。

然而,有相当多的XML配置文件已经存在,所以去理解Spring中的XMLConfig是相当重要的,但是我希望,
这一节只是去帮你怎么理解已经存在的XMLConfig,你新开发的项目中,尽量使用自动发现和基于Java的配
置。

2.4.1 创建一个基本的XMLConfig

创建一个基本的XMLConfig如下:

<?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">
<!-- configuration details go here -->
</beans>

从上面可以看出,基于XML的配置比基于Java的配置要复杂得多,只是一个基本的配置,JavaConfig只需要
一个@Configuration注解,而XMLConfig需要声明一堆的命名空间与命名空间地址。这就是我为什么希望
JavaConfig的原因之一。

上面,XMLConfig的根目录是元素,然后在根目录里面声明命名空间,Spring的默认命名空间是
spring-beans。上面的代码不需要记忆太多,只需要在用的地方拷贝这段代码即可。

2.4.2 声明一个简单的Bean

在XMLConfig中声明一个简单的Bean的方法是使用一个带有class属性的元素,像下面这样:

<bean class="soundsystem.SgtPeppers" />

在这个例子中,由于缺少明确的身份认证,Spring会根据该类的完全名称设定该Bean的名称,为
soundsystem.SgtPeppers#0,#0代表这是该类的第一个Bean,如果在其他地方声明了该类的另外一个
Bean,则名称为soundsystem.SgtPeppers#1,后面的数据为枚举,一次枚举下去。

虽然方便,但是这种自动产生的名称并没有多大的用途,所以一般情况我们都需要为Bean指定一个显式的名
称,像下面这样:

<bean id="compactDisc" class="soundsystem.SgtPeppers" />

当你在后面将该Bean注入到CDPlayer中去的时候,你会使用这个名称。

在我们继续深入下去之前,让我们花一点时间看看这个简单的Bean申明的特点。

第一件事情就是你不会像在JavaConfig一样直接创建SgtPeppers的实例,当Spring看到这个元素
时,他会调用SgtPeppers的默认构造函数去创建这个Bean,Bean的创建是较为被动的。而且它也没有
JavaConfig那样,对Bean进行各种操作的能力。

另外一件事情就是,这个Bean声明使用的class是使用的字符串形式,但是谁能保证这个字符串就是一个类的
完整路径呢?Spring的XML配置并没有提供编译时检验,即使它是一个真正的类型,但是当重命名该类的时
候会发生什么呢?

这些只是我更喜欢JavaConfig的一些原因,我建议你在选择应用程序的配置方式时要注意它的不足之处。接
下来我们继续学习。

2.4.3 构造器注入的方式初始化一个Bean

在Spring的XMLConfig中,只有一种方式申明一个Bean,也就是使用带有class属性的元素。但是当
我们使用DI在Bean声明中注入的时候,这里有下面两种方式可以选择:

  • 元素
  • c-namespace

这两种方式之间的不同是较多的。就像你看到的一样,比使用c-namespace是更加不可
读并且更不清洗的。另一方面,能做一些c-namespace不能做的事情

使用Bean引用注入

下面是使用Bean引用注入的例子:

<bean id="cdPlayer" class="soundsystem.CDPlayer">
    <constructor-arg ref="compactDisc" />
</bean>

当Spring遇到这个元素时,它会创建一个CDPlayer的实例,元素告诉它,该构造
函数需要一个CompactDisc类型的参数,所以Spring需要注入该参数。

另外,你也可以使用Spring 3.0中引用的c-namespace方法,为了使用c-namespace,你必须引用c的命名
空间,如下:

<?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">
...
</beans>

必须带上c的命名空间以及其Schema,然后你可以像下面这样使用:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
c:cd-ref="compactDisc" />

你可以使用c-namespace去定义一个构造函数的参数作为bean元素的属性。并且它是一个比较奇怪的参
数,下图说明了c-namespace属性的名字的各个部分的含义:

这里写图片描述

这个属性名字以命名空间c:开始,接下来就是需要注入的参数的名字,-ref告诉Spring,后面是一个Bean的
引用而不是一个String的字面值。使用c-namespace比使用元素更加简洁,这是我希望它
的一个原因,除了易读,当我写一本书的代码时,c-namespace是特别有帮助。

但是,我认为c-namespace有一个烦恼,那就是直接使用的是参数的名字,当调试的时候,参数名字变化
了,会导致有问题。但是Spring提供了一种解决方案,通过使用参数的索引来命名属性的名字,如:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
c:_0-ref="compactDisc" />

这个c-namespace的属性名字看起来更加离奇,我将参数名字使用了一个0代替,由于xml的属性名称不能
使用数字作为起始,所以在前面加了一个下划线_,使用一个索引标识名称比使用名字要更加方面,而且不会
受参数名字变化的困扰,而且当有多个参数的时候,这中方法是相当有用的,你不需要去记那么多的参数名
称。

如果只有一个参数,Spring还提供了一种选择,如下所示:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
c:_-ref="compactDisc" />

这是目前为止最奇怪的属性名字了,这里没有参数索引,也没有参数名称,只有一个_下划线的占位符。它代
表的就是该构造函数只有一个参数。

现在,你知道了怎么注入一个引用到构造函数中去,但是怎么注入一个字符串字面值到构造函数中去呢?

向构造函数中注入字符串字面值

为了描述这个问题,我们看下面的类:

package soundsystem;
public class BlankDisc implements CompactDisc {
    private String title;
    private String artist;
    public BlankDisc(String title, String artist) {
        this.title = title;
        this.artist = artist;
    }
    public void play() {
        System.out.println("Playing " + title + " by " + artist);
    }
}

不像SgtPeppers,BlankDisc中,title与artist不是硬编码的,它是可以通过构造函数传入的。该类会更加的
灵活,很像一个真实世界中的空白光盘,你可以在上面设置你想要的任何艺术家和标题。现在,你可以像如
下方式一样声明:

<bean id="compactDisc"
class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
</bean>

如上所示,我们使用元素声明构造函数的注入,但是这里元素的值不再使用ref了,而是使
用的value,也就是说,ref代表的是其他Bean的引用,而使用value,代表后面跟的是一个值,而不是引用
了。

如果使用c-namespace的方式,通过名字的方式如下:

<bean id="compactDisc" class="soundsystem.BlankDis" c:_title="Sgt. Pepper's Lonely Hearts Club Band" c:_artist="The Beatles"">
</bean>

可以看出,使用字面值与引用的差别就是后面不需要加-ref了。如果使用索引的方式如下:

<bean id="compactDisc"
class="soundsystem.BlankDisc"
c:_0="Sgt. Pepper's Lonely Hearts Club Band"
c:_1="The Beatles" />

XML不允许有两个属性的名字一样,所以当有两个或者两个以上的构造函数参数的时候,不能直接使用一个
下划线(_)代替,这样就会有超过一个属性名字一样了。但是,当构造函数只有一个参数的时候,可以使用
一个下划线表示。

当组装Bean引用或者字面值的时候,与c-namespace是等价的,但是,有些时
候,能做,而c-namespace不能做,看下面。

组装集合

直到现在为止,我们假定CD仅仅是由一个标题和一个艺术家的名字组成。但是,它跟真实世界中的CD是不
一样的。大多数的CD都不只是只有一首歌曲,而是上面有十几首歌曲或者更多。

如果CD是跟真事中的CD一样,那么它还必须有一个轨道的概念,考虑下面的BlankDisc:

package soundsystem;

import java.util.List;

/**
 * Created by LQ-ZHANGHUAI on 2016/3/14.
 */
public class BlankDisc implements CompactDisc {
    private String title;
    private String artist;
    private List<String> tracks;

    public BlankDisc(String title, String artist, List<String> tracks) {
        this.title = title;
        this.artist = artist;
        this.tracks = tracks;
    }

    @Override
    public void play() {
        System.out.println("Playing " + title + " by " + artist);
        for (String track : tracks) {
            System.out.println("-Track: " + track);
        }
    }
}

这个变化意味中,在Spring中声明Bean的时候,你必须指定一个轨道列表。最简单的方法就是直接传递一个
null。如下:

<bean id="compactDisc" class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg><null/></constructor-arg>
</bean>

传递null元素并不是一个好的注意,虽然在运行中得不到错误,但是当运行play函数的时候,会抛出
NullPointerException异常。

一个更好的注意是在声明的时候提供一个列表。这里可以使用元素作为的子元素,
在其里面声明需要的列表,如下:

<bean id="compactDisc" class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg>
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
            <value>Lucy in the Sky with Diamonds</value>
            <value>Getting Better</value>
            <value>Fixing a Hole</value>
            <!-- ...other tracks omitted for brevity... -->
        </list>
    </constructor-arg>
</bean>

其中value元素代表的是一个值,如果你需要使用一个Bean引用,直接使用ref元素代替value元素就可以
了,如下所示:

<list>
    <ref bean="sgtPeppers" />
    <ref bean="whiteAlbum" />
    <ref bean="hardDaysNight" />
    <ref bean="revolver" />
    ...
</list>

这个意味着,list元素对应的是java.util.List的集合,同样,也可以使用set元素来定义,如下所示:

<bean id="compactDisc" class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg>
        <set>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
            <value>Lucy in the Sky with Diamonds</value>
            <value>Getting Better</value>
            <value>Fixing a Hole</value>
            <!-- ...other tracks omitted for brevity... -->
        </set>
    </constructor-arg>
</bean>

但是,list元素与set元素有一点不同,当Spring去创建集合的时候,它要么会创建List,要么会创建Set,但
是,如果是Set的时候,重复的元素会被抛弃而且不保证顺序。但是无论何种情况下,都可以使用或
者去装配List或者Set,甚至Array。

装配集合是相对于c-namespace的优势,这里没有明显的方式去使用c-namespace装配
集合。

和c-namespace还有一些细微的差别。但是,我们在这里考虑的比你远,就像前面说的
一样,我们建议使用java配置而不是xml配置。因此,关于在xml中如果使用构造器不是一个很好的话题。我
们下面看看在xml中怎么注入属性。

2.4.4 设置属性

直到这里,我们看到CDPlayer和BlankDisc全部都是使用的构造器注入而没有使用属性的设置方法。相反,
让我们看看Spring中的属性注入是怎么工作的。假设你的CDPlayer像下面这样声明。

package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import soundsystem.CompactDisc;
import soundsystem.MediaPlayer;
public class CDPlayer implements MediaPlayer {
    private CompactDisc compactDisc;
    @Autowired
    public void setCompactDisc(CompactDisc compactDisc) {
        this.compactDisc = compactDisc;
    }
    public void play() {
        compactDisc.play();
    }
}

注意,CDPlayer没有任何构造函数(也就是说,系统会提供一个默认构造函数)。它也没有任何硬依赖,因
此你可以像下面这样声明它:

<bean id="cdPlayer"
class="soundsystem.CDPlayer" />

Spring创建该Bean完全没有问题,只是在运行play()的时候会抛出NullPointerException,因为你需要注
入compactDisc属性。所以你可以使用下面的配置方法去解决这个问题:

<bean id="cdPlayer"
class="soundsystem.CDPlayer">
    <property name="compactDisc" ref="compactDisc" />
</bean>

元素不像元素,它是使用一个ID为compactDisc的Bean引用去注入
compactDisc属性(通过setCompactDisc函数)。现在就可以运行测试程序了。

像c-namespace作为元素的替代一样,Spring也提供了p-namespace来替代
元素。首先要引入命名空间:

<?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>

然后就可以像下面这样引用了。

<bean id="cdPlayer"
class="soundsystem.CDPlayer"
p:compactDisc-ref="compactDisc" />

p的属性名字说明如下图:

这里写图片描述

方式跟c-namespace一样,这里不做过多说明。

同样,p-namespace不能用于list,ref与value的意义式样的,ref代表对Bean的引用,而value代表的是
值,下面是一个具有list的属性声明:

<bean id="compactDisc"
class="soundsystem.BlankDisc">
    <property name="title"
    value="Sgt. Pepper's Lonely Hearts Club Band" />
    <property name="artist" value="The Beatles" />
    <property name="tracks">
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
            <value>Lucy in the Sky with Diamonds</value>
            <value>Getting Better</value>
            <value>Fixing a Hole</value>
            <!-- ...other tracks omitted for brevity... -->
        </list>
    </property>
</bean>

但是你可以使用Spring提供的util-namespace去简化list的声明,首先引入命名空间:

<?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:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
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">
...
</beans>

现在,可以使用下面的方式声明:

<util:list id="trackList">
    <value>Sgt. Pepper's Lonely Hearts Club Band</value>
    <value>With a Little Help from My Friends</value>
    <value>Lucy in the Sky with Diamonds</value>
    <value>Getting Better</value>
    <value>Fixing a Hole</value>
    <!-- ...other tracks omitted for brevity... -->
</util:list>

然后在属性注入那里直接引用该ID就可以了。

<bean id="compactDisc"
class="soundsystem.BlankDisc"
p:title="Sgt. Pepper's Lonely Hearts Club Band"
p:artist="The Beatles"
p:tracks-ref="trackList" />

util-namespace提供了下面的一些工具:

这里写图片描述

现在,让我们来综合这一章的内容,你可以混合使用自动装配、XmlConfig、JavaConfig去进行Spring的配
置。下面让我们学习学习。

2.5 导入和混合配置

在一个经典的Spring应用程序中,你可能需要使用自动配置和显式配置。即时你更喜欢JavaConfig,但是有
时你不得不使用XmlConfig。幸运的时候,在Spring中,每个配置之间都不是相互排斥的。你可以自由组合
自动配置、JavaConfig和XmlConfig。实际上,就像你在2.2.1节看到的那样,你必须至少在一个显式配置中
将Component-Scan开启。

了解混合配置的第一件事情就是要知道不管是什么配置,它们来自哪里?自动装配会考虑到Spring容器中的所
有Bean,不管这些Bean是来自JavaConfig、XmlConfig还是Component-Scan。在显示配置中的时候,你
怎么去引用JavaConfig或者XmlConfig。

2.5.1 在JavaConfig中引用XmlConfig

假设到一定时候,CDPlayerConfig变得臃肿,你希望去将它们分开,当然,在这里面就声明了两个Bean,
不是太多了。但是,我们假定,两个Bean就太多了。假设我们将BlankDisc从CDPlayerConfig中分离出来放
到CDConfig类中去。

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import soundsystem.CompactDisc;
import soundsystem.SgtPeppers;

/**
 * Created by LQ-ZHANGHUAI on 2016/3/14.
 */
@Configuration
public class CDConfig {
    @Bean
    public CompactDisc compactDisc() {
        return new SgtPeppers();
    }
}

既然compactDisc方法从CDPlayerConfig中分离出来了,你就需要有一种方式去连接着两个类。一种在
CDPlayerConfig,中导入CDConfig的方式是使用@Import。

或者你可以使用另外一种方式,新建一个顶层的配置类,将这两个类都导入进来。

package config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * Created by LQ-ZHANGHUAI on 2016/3/14.
 */
@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}

不管使用哪一种方式,你都是将CDPlayerConfig分离开了。现在,假设你必须将其分离到一个XML配置文
件中去。XmlConfig如下:

<bean id="compactDisc"
class="soundsystem.BlankDisc"
c:_0="Sgt. Pepper's Lonely Hearts Club Band"
c:_1="The Beatles">
    <constructor-arg>
        <list>
            <value>Sgt. Pepper's Lonely Hearts Club Band</value>
            <value>With a Little Help from My Friends</value>
            <value>Lucy in the Sky with Diamonds</value>
            <value>Getting Better</value>
            <value>Fixing a Hole</value>
            <!-- ...other tracks omitted for brevity... -->
        </list>
    </constructor-arg>
</bean>

那么,怎么样在JavaConfig中导入XmlConfig呢?

答案就是使用@ImportResources,假定BlankDisc定义在一个叫着cd-config.xml文件中,而且可以在类目
录下面找到它,那么你可以像下面这样改变SoundSystemConfig。

package config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

/**
 * Created by LQ-ZHANGHUAI on 2016/3/14.
 */
@Configuration
@Import({CDPlayerConfig.class})
@ImportResource({"classpath:cd-config.xml"})
public class SoundSystemConfig {
}

像上面这样,就可以实现在JavaConfig中导入XmlConfig。

2.5.2 在XmlConfig中引用JavaConfig

与在JavaConfig中引用XmlConfig一样,只不过使用元素代替了@Import注解。在XmlConfig中,
引用其他的XmlConfig,如下所示,使用import resource。

<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
    <import resource="cd-config.xml" />
    <bean id="cdPlayer"
    class="soundsystem.CDPlayer"
    c:cd-ref="compactDisc" />
</beans>

在XmlConfig中引用JavaConfig。使用的是。如下:

<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans

    <bean class="soundsystem.CDConfig" />

    <bean id="cdPlayer"
    class="soundsystem.CDPlayer"
    c:cd-ref="compactDisc" />
</beans>

跟JavaConfig一样,你也可以定义一个高层XmlConfig,它不定义任何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"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="soundsystem.CDConfig" />

    <import resource="cdplayer-config.xml" />

</beans>

不管我们使用的是JavaConfig还是XmlConfig,通常我们都会像上面一样,创建一个顶层的配置文件,在这
个配置文件中,将Component-Scan打开,(在JavaConfig中使用@ComponentScan,在XmlConfig中使
用)。

2.6 总结

在Spring中,Spring容器是核心。该容器管理应用程序中的组件整个生命周期,包含组件创建,管理它们的
依赖关系等。

在这一章中,我们学习了三种Spring的配置方式,ComponentScan、XmlConfig、JavaConfig。不管是哪
一种技术,它都是用来描述组件并且管理这些组件的关系的。

针对这些技术,我的建议是 ComponentScan>JavaConfig>XmlConfig。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值