Spring系列(一)Spring装配bean详解

装配(wiring): 创建应用对象之间协作关系的行为。

依赖注入的本质是装配。

1 Spring配置的可选方案

Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。

Spring提供了三种主要的装配机制:

  • 在XML中进行显示配置
  • 在Java中进行显示配置
  • 隐式的bean发现机制和自动装配

这些装配机制可以同时使用,也可以单独使用

2 自动化装配bean

Spring从两个角度来实现自动化装配:

  • 组件扫描(component):Spring会自动发现应用上下文中所创建的bean。
  • 自动装配(autowiring):Spring自动满足bean之间的依赖。
2.1 创建可被发现的bean
  • @Component 表明该类作为组件类,并告知Spring要为这个类创建bean。
  • @ComponentScan: 启用组件扫描

以CD播放器为例创建一个可被发现的bean。
1. 创建CompactDisc接口定义CD概念

public interface CompactDisk {
    /**
     * 播放
     */
    void play();
}
  1. 定义一个CompactDisc的实现类,将该类通过@Component注解为组件类
@Component
public class SgtPeppers implements CompactDisk{

    private String title = "Sgt. Pepper's Lonely Hearts Club Band";
    private String artist = "The Beatles";

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

}
  1. 不过组件扫描默认是不开启的,因此还需要显式配置一下Spring,从而,命令它去寻找带有@Component注解的类,并为其创建bean。有两种方式:
    • 第一种是使用Java config的方式启用扫描:
@Configuration
@ComponentScan
public class CDPlayerConfig {
}

使用了@ComponentScan注解来启用扫描,默认会扫描与配置类相同的包。在这个包中查找带有@Component注解的类,自动为其创建bean。

  • 第二章是使用xml配置方式,使用Spring context命名空间的元素,如下所示:
<context:component-scan base-package="com.zjx.compactdisk"></context:component-scan>

到此为止,已经创建了可被发现的bean了,可以使用Junit进行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
public class CDPlayerTest {
    @Autowired
    private CompactDisk compactDisk;

    @Test
    public void test(){
        assertNotNull(compactDisk);
    }
}

如果使用xml启用扫描,则:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:soundsSystem.xml")
public class CDPlayerXmlTest {

    @Autowired
    private CompactDisk compactDisk;

    @Test
    public void test(){
        assertNotNull(compactDisk);
    }
}

解析:

  • 使用SpringJUnit4ClassRunner以便在测试开始的时候自动创建Spring的应用上下文。
  • @ContextConfiguration会告诉需要在哪里加载配置
2.2 为组件扫描的bean命名

Spring应用上下文中所有bean都会给一个ID,如果没有明确指定ID,Spring会根据类名为其指定一个ID,也就是将类名第一个字母变成小写字母。

如果想自行指定ID,可以在使用@component注解的时候指定,例如:

@Component("myname")
public class SgtPeppers implements CompactDisk{
    ...
}

还有另外一种为bean命名的方式,这种方式不使用@Component注解,而是使用Java依赖注入规范(Java Dependency Injection)中提供的@Named注解为bean设置ID:

@Named("myname")
public class SgtPeppers implements CompactDisk{
    ...
}

Spring支持将@Named作为@Component注解的替代方案。两者之前有细微差别,在大多数场景中是可以互相替换的。

2.3 设置组件扫描的基础包

按照默认规则,@ComponentScan会以配置类所在的包作为基础包(base package)来扫描组件。但是如果你想扫描不同的包,甚至扫描多个包,这时需要在@ComponentScan的value属性中指定包的名称:

  • 扫描指定的包
@Configuration
@ComponentScan("soundsystem")
public class CDPlayerConfig {
}

如果想清晰表明你所设置的是基础包,可以通过basePackages属性指定:

@Configuration
@ComponentScan(basePackages="soundsystem")
public class CDPlayerConfig {
}
  • 指定扫描多个包
@Configuration
@ComponentScan(basePackages={"soundsystem","video"})
public class CDPlayerConfig {
}
  • 还可以指定扫描包中所包含的类或者接口所在包
@Configuration
@ComponentScan(basePackageClasss={CDPlayer.class,video.class})
public class CDPlayerConfig {
}

basePackageClasss指定的类所在的包会作为组件包扫描

2.4 通过为bean添加注解实现自动装配

自动装配就是让Spring自动满足bean依赖的一种方法。在满足依赖的过程中,会再Spring应用上下文中寻找匹配某个bean需求的其他bean。为了声明要进行自动装配,可以借助Spring的 @Autowired 注解。

@Autowired 注解可以用在构造器、属性的setter方法以及类的任何方法上。

Spring会尝试满足方法参数上所声明的依赖,假如有且只有一个bean匹配依赖需求的话,那么这个bean就会被装配进来。

如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,可以将@Autowired的required属性设置为false:

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

注意:将required设置为false,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。如果代码中没有进行null检查的话,这个处于未装配的属性可能会抛出NullPointerException。

可以使用@Inject替换@Autowired。这个注解来源于Java依赖注入规范。

3 通过Java代码装配bean

在进行显式配置的时候有两种可选方案:Java和XML。JavaConfig是更好的方案,因为它更强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其他代码一样。

JavaConfig与其他的Java代码又有区别。JavaConfig是配置代码,这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码中。

3.1 创建配置类

创建配置类的关键在于为其添加 @Configuration注解 ,在@Configuratiion注解表明这个类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节。

@Configuration
public class CDPlayerConfig {

}
3.2 声明简单的bean

要在JavaConfig中声明bean,需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解。比如:

    @Bean
    public CompactDisc sgtPepers(){
        return new SgtPeppers();
    }
  • @Bean注解会告诉Spring这个方法将返回一个对象,该对象注册为Spring应用上下文中bean。方法体中包含了最终产生bean实例的逻辑。

默认情况下,bean的ID与带有@Bean注解的方法名是一样的。在本例中,bean的名字将会是sgtPepers。如果想为其设置成一个不同的名字,那么可以重命名该方法,也可以通过name属性指定一个不同的名字:

    @Bean(name="newName")
    public CompactDisc sgtPepers(){
        return new SgtPeppers();
    }

在JavaConfig中装配bean的最简单的方式就是引用创建bean的方法,例如:

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

解析:该方法同样使用了@Bean注解,这表明这个方法会创建一个bean实例并将其注册到Spring应用上下文中,所创建的bean ID为cdplayer,与方法名字相同。

看起来CompactDisc是通过调用SgtPeppers()得到的,但情况并非如此。因为SgtPeppers()方法上有@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。

另一种更为简单的方式来注入bean:

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

通过这种方式引用其他的bean是更好的选择。因为它不会要求将bean声明到同一个配置类中,甚至不要求必须在JavaConfig中声明,实际上可以通过组件扫描功能自动发现或者通过XML来进行配置。你可以将配置分散到多个配置类、XML文件以及自动扫描和装配bean中,只要功能健全即可。

带有@Bean注解的方法可以采用任何必要的Java功能产生bean实例。构造器和Setter方法只是@Bean方法的两个简单样例。

4 通过XML装配bean

4.1 创建XML配置规范

在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。在使用JavaConfig的时候,这意味着要创建一个带有@Configuration注解的类,而在XML配置中,这意味着要创建一个XML文件,并且要以元素为根。

最简单的Spring 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

使用XML时需要在配置文件的顶部声明XML模式(XSD)文件,这些文件定义了配置Spring的XML元素。

用来装配bean的最基本的XML元素包含在spring-beans模式之中,在上面这个XML文件中,它被定义为根命名空间。是该模式的一个元素,它是所有Spring配置文件。

4.2 声明一个简单的

要在基于XML的Spring配置一个bean,我们使用spring-beans模式中的另一个元素。元素类似于JavaConfig中的@Bean注解。如下声明一个CompactDisc bean:

<bean class="com.zjx.xml.SgtPeppers"/>
  • 这里创建了一个很简单的bean,创建这个bean的类通过class属性来指定的,并且使用类的权限定的类名。

因为没有明确指定ID,所以这个bean将会根据全限定类名来进行命名。在本例中,bean的ID将会是”com.zjx.xml.SgtPeppers#0”。其中,“#0”是一个计数形式,用来区分相同类型的其他bean。如果你声明了另外一个SgtPeppers,并且没有明确进行标识,那么它将自动得到的ID是”com.zjx.xml.SgtPeppers#1”。

  • 可以借助id属性,为每个bean设置一个你自己选的名字:
 <bean id="peppers" class="com.zjx.xml.SgtPeppers"/>

为了减少XML中繁琐的配置,只对那些需要按名字引用的bean进行明确的命名。

JavaConfig配置优于XML的部分原因:
- JavaConfig可以用任何可以想到的方式创建bean实例,而XML只能通过默认构造器创建bean
- xml将bean的类型以字符串的形式设置在class属性中,无法保证类型正确

4.3 借助构造器注入初始化bean

在Spring配置中,只有一种声明bean的方式:使用元素并指定class属性。Spring会从这里获取必要的信息来创建bean。

但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:

  • 元素
  • 使用Spring 3.0所引入的c-命名空间

两者的区别在很大程度上就是是否冗长烦琐。比c-命名空间更加烦琐,但是可以做到一些c-命名空间无法实现的事。

  • 构造器注入bean引用

CDPlayer bean有一个接受CompactDisc类型的构造器,现在已经声明了SgtPeppers bean,并且SgtPeppers类实现了CompactDisc接口,所以实际上我们已经有了一个可注入到CDPlayer bean中的bean,我们只需要在XML中声明CDPlayer并通过ID引用SgtPeppers就可以了。

<bean class="com.zjx.xml.CDPlayer">
        <constructor-arg ref="peppers"></constructor-arg>
    </bean>

当Spring遇到这个元素时,它会创建一个CDPlayer实例。元素会告知Spring要将一个ID为peppers的bean引用传递给CDPlayer的构造器中。

  • c命名空间注入bean引用
    c-命名空间是在Spring 3.0中引入的,它是在XML更简洁地描述构造器参数的方式。要使用的话必须在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:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

在c-命名空间和模式声明之后,就可以使用它来注入构造器参数了,如下所示:

 <bean class="com.zjx.xml.CDPlayer" 
          c:cd-ref="peppers"/>

属性名以“c:”开头,也就是命名空间的前缀。接下来就是要装配的构造器参数名,在此之后是“-ref”,这是一个命名的约定,它会告诉Spring,正在装配的是一个bean的引用,这个bean的名字是peppers,而不是字面量“peppers”。

也可以使用参数在整个参数列表中的位置信息:

 <bean class="com.zjx.xml.CDPlayer" 
          c:_0-ref="peppers"/>
  • 将字面量注入到构造器
    使用value属性可以将给定的值以字面量的形式注入到构造器中。

假设有一个CompactDisc的新实现:

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);
  }

}

现在我们可以将已有的SgtPepers替换成这个类:

 <bean id="blankDisc" class="com.zjx.xml.BlankDisc">
        <constructor-arg value="发如雪"></constructor-arg>
        <constructor-arg value="周杰伦"></constructor-arg>
    </bean>

使用c-命名空间的方式:

 <bean id="peppers" class="com.zjx.xml.BlankDisc" 
          c:title="发如雪" 
          c:artist="周杰伦"/>
  • 装配集合

装配集合只能以构造器注入的方式进行装配,无法使用c命名空间

可以使用<list>和<set>

比如一张唱片上有很多首歌,新的ConmpactDisc实现类如下:

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;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public void setArtist(String artist) {
    this.artist = artist;
  }

  public void setTracks(List<String> tracks) {
    this.tracks = tracks;
  }

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

因为构造器里面有一个List参数,在声明bean的时候必须提供一个列表。最简单的做法是将列表设置为null:

<bean id="peppers" class="com.zjx.xml.properties.BlankDisc">
            <constructor-arg value="发如雪"></constructor-arg>
            <constructor-arg value="周杰伦"></constructor-arg>
            <constructor-arg ><null></null></constructor-arg>
    </bean>

但这样做在运行时会报空指针异常,因此并不是理想的方案。最好的做法是使用<list>元素将其声明为一个列表:

 <bean id="peppers" class="com.zjx.xml.properties.BlankDisc">
            <constructor-arg value="十一月的萧邦"></constructor-arg>
            <constructor-arg value="周杰伦"></constructor-arg>
            <constructor-arg >
                <list>
                    <value>发如雪</value>
                    <value>夜曲</value>
                    <value>黑色毛衣</value>
                </list>
            </constructor-arg>
    </bean>
  • <list>元素是<constructor-arg>的子元素,这表明一个包含值的列表将会传递到构造器中。其中<value>元素用来指定列表中的每个元素。
  • 也可以使用<ref>元素替代<value>元素,实现bean引用列表的装配。
...
<list>
    <ref bean="sgtPepers"/>
</list>
...
  • <set>和<list>区别不大,其中最重要的不同在于当Spring创建要装配的集合时,所创建的是Java.util.List还是java.util.Set。如果是set的话,所有重复的值都会被忽略掉,而且顺序也无法保证。无论何种情况,<set>、<list>都可以用来装配List、Set甚至数组。
4.4 设置属性

也可以通过属性注入的方式注入bean。假设属性注入的CDPlayer如下所示:

public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;

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

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

}
  • 对强依赖使用构造器注入,对可选性的依赖使用属性注入

使用 <property>元素 在XML文件中为Setter方法注入bean:

    <bean id="compactDisc" class="com.zjx.xml.BlankDisc">
        <constructor-arg value="发如雪"></constructor-arg>
        <constructor-arg value="周杰伦"></constructor-arg>
    </bean>

    <bean class="com.zjx.xml.properties.CDPlayer">
        <property name="cd" ref="compactDisc"></property>
    </bean>

在本例中引用了ID为compactDisc的bean(通过ref指定)。name指参数名。

  • Spring提供了更简洁地p-命名空间,作为<property>的替代方案,同样的,为了启用p-命名空间,必须要在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:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

现在可以使用p-命名空间了,按照以下的方式装配compactDisc属性:

<bean id="cdPlayer" class="com.zjx.xml.properties.CDPlayer" p:cd-ref="compactDisc"/>

解析:
- 属性的名字使用”p:”前缀,表明我们所设置的是一个属性。
- 接下来是我们要注入的属性名
- 属性名以“-ref”结尾,这回提示Spring要进行装配的是引用,而不是字面量。

将字面量注入到属性中
在<property>元素中使用value属性即可将字面量注入到属性中:

   <bean id="compactDisc" class="com.zjx.xml.properties.BlankDisc">
        <property name="title" value="十一月的萧邦"></property>
        <property name="artist" value="周杰伦"></property>
        <property name="tracks">
            <list>
                <value>发如雪</value>
                <value>夜曲</value>
                <value>黑色毛衣</value>
            </list>
        </property>
    </bean>

在这里还使用了内嵌的<list>元素来设置集合属性。

还可以使用p-命名空间的方式来完成该功能:

<bean id="compactDisc" class="com.zjx.xml.properties.BlankDisc" p:artist="周杰伦" p:title="十一月的萧邦">
        <property name="tracks">
            <list>
                <value>发如雪</value>
                <value>夜曲</value>
                <value>黑色毛衣</value>
            </list>
        </property>
    </bean>

由于p-命名空间无法装配集合,所以集合还是用property属性来指定。

可以使用util-命名空间来简化该功能:
- 首先,需要在XML中声明util-命名空间及其模式:

<?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-命名空间所提供的功能之一就是<util:list>元素,它会创建一个列表的bean。借助<util:list>,我们可以将list集合转移到bean之外,将其声明到单独的bean中,如下图所示:

    <util:list id="list">
        <value>发如雪</value>
        <value>夜曲</value>
        <value>黑色毛衣</value>
    </util:list>

现在,我们能够像使用其他bean一样,将list注入到BlankDisc bean的tracks属性中:

 <bean id="compactDisc" class="com.zjx.xml.properties.BlankDisc" 
          p:artist="周杰伦" 
          p:title="十一月的萧邦" 
          p:tracks-ref="list"/>

5. 导入和混合配置

在典型的Spring应用中,可能会同时使用自动化和显示配置。

关于混合配置,在自动装配时,它并不在意要装配的bean来自哪里。自动装配的时候会考虑到Spring容器中的所有bean,不管它是在JavaConfig或XML中声明的还是通过组件扫描获取到的。

5.1 在JavaConfig中引用XML配置

假设我们之前定义的CDPlayerConfig已经变得有些笨重了,我们想将其拆分。一种实现方案就是将compactDisc从CDPlayer拆分出来,定义到它自己的CDConfig类中,如下所示:

@Configuration
public class CDConfig {
    @Bean
    public CompactDisc sgtPepers(){
        return new SgtPeppers();
    }
}

sgtPepers()方法已经从CDPlayerConfig中移除掉了,我们需要一种方式将这两个类组合在一起。一种方式是在CDPlayerConfig中使用 @Import 注解导入CDConfig:

@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {

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

}

或者采用更好的办法,创建一个更高级别的配置类SoundSystemConfig,在这个类中使用@Import将两个配置类组合在一起:

@Configuration
@Import({CDConfig.class,CDPlayerConfig.class})
public class SoundSystemConfig {
}

上面两种方式都能将多个JavaConfig结合起来,但是现在假设我们需要通过XML来配置compactDisc,如下所示:

<?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 id="compactDisc" class="sounds.SgtPeppers"/>

</beans>

现在将SgtPeppers配置在了XML之中,要想让Spring同时加载它和其他基于Java的配置需要使用 @ImportResource注解。

假设SgtPeppers定义在cd-config.xml文件中,该文件位于跟路径下,那么可以修改SoundSystemConfig,让它使用@ImportResource注解,如下所示:

@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {
}

此时,两个bean(配置在JavaConfig中的CDPlayer以及配置在XML中的compactDisc)都会被加载到Spring容器中。

5.2 在XML配置中引用JavaConfig

在XML中,可以使用import元素来拆分XML配置。

比如将之前的CDPlayer配置在xml中,将sgtPepers配置在JavaConfig中,这时候可以使用< bean>元素 将Java配置导入到XML配置中,我们可以这样声明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="sounds.config.CDConfig" />

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

</beans>

还可以通过一个更高层的配置文件,这个文件不声明任何bean,只是负责将两个或者多个配置组合起来。比如,可以将CDConfig bean 从之前的XML文件中移除掉,而是使用第三个配置文件将这两个组合在一起:

  <bean class="sounds.config.CDConfig" />

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

猜你喜欢

转载自blog.csdn.net/zjx2016/article/details/80721114