第二章 装配Bean

创建应用对象之间协作关系的行为通常称为装配,这也是依赖注入(DI)的本质。

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

① 在XML中进行显示配置;

② 在Java中进行显示配置;

③ 隐式的bean发现机制和自动装配。

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

① 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean;

② 自动装配(autowiring):Spring自动满足bean之间的依赖。

组件扫描和自动装配组合在一起就能发挥出强大的威力,它们能够将你的显式配置降低到最少。

 以下以CD播放器和CD的例子为例,进行讲解Spring如何实现自动化装配的

先建立一个磁盘的接口,如下:

package com.chenjl.autowire;

public interface CompactDisc {
    void play ();
}

再建立一个它的实现,该类添加了@Component注解,如下:

package com.chenjl.autowire;

import org.springframework.stereotype.Component;

@Component
public class SgtPepers implements CompactDisc{

    private static String title = "最炫名族风";

    private static String artist = "凤凰传奇";

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

添加了@Component注解后,Spring会为你把事情处理妥当。不过,组件扫描默认是不启用的。我们还需要显示配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。如下所示:

package com.chenjl.autowire;

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

@Configuration
@ComponentScan
public class CDPlayerConfig {
}

类CDPlayerConfig通过java代码定义了Spring的装配规则。它使用了@ComponentScan注解,这个注解能够在Spring中启用组件扫描。

如果没有其它配置的话,@ComponentScan注解默认会扫描与配置类相同的包。

也可以使用XML启用组件扫描,可以使用Spring context命名空间的<context:component-scan>元素,如下所示:

<?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="com.chenjl.autowire"/>

</beans>

为了测试组件扫描,我们创建了一个简单的JUnit测试:

package com.chenjl.test;

import com.chenjl.autowire.CDPlayerConfig;
import com.chenjl.autowire.CompactDisc;
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.assertNotNull;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayTest {

    @Autowired
    CompactDisc compactDisc;

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

}

CDPlayTest使用了Spring的SpringJUnit4ClassRunner,以便在测试开始的时候自动创建Spring上下文。注解@ContextConfiguration会告诉它需要在CDPlayerConfig中加载配置。

为了证明会生成对应的bean,在测试代码中有一个@Autowired注解标注的属性,并断言其不为空。

为组件扫描的bean 命名:

Spring应用上下文中所有的bean都会给定一个ID,如果没有人为的指定其ID,那么就将类名的第一个字母变为小写作为其ID。

要想设置其ID,则如下所示:

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

 

还有另外一种bean的命名方式,如下所示:

package com.chenjl.autowire;

import javax.inject.Named;

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

其实@Named注解并不是jdk自带的,而是要导入jar包的。

设置组件扫描的基础包

为了指定不同的基础包,在@ComponentScan的value属性中指明包的名称,如下所示:

package com.chenjl.autowire;

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

@Configuration
@ComponentScan(basePackages = "com.chenjl")
public class CDPlayerConfig {
}
可能你已经注意到了,basePackages使用的是复数的形式,意味着可以设置多个基础包,如下所示:
package com.chenjl.autowire;

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

@Configuration
@ComponentScan(basePackages = {"com.chenjl.autowire","com.chenjl.test"})
public class CDPlayerConfig {
}

还可以指定类,如下所示:

package com.chenjl.autowire;

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

@Configuration
@ComponentScan(basePackageClasses = {CompactDisc.class,CDDisc.class})
public class CDPlayerConfig {
}

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

简单来说,自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。如下代码,通过自动装配,将一个CompactDisc注入到CDPlayer之中。

package com.chenjl.autowire;

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

public class CDPlayer implements MediaPlayer{

    private CompactDisc cd;

    // 通过构造器注入就可以播放任何的CD唱片
    @Autowired
    public CDPlayer (CompactDisc cd) {
        this.cd = cd;
    }

    // 也可以通过set方法注入依赖
    @Autowired
    public void setCd(CompactDisc cd) {
        this.cd = cd;
    }

    // 也可以用在一个普通的方法之上
    @Autowired
    public void insertDisc (CompactDisc cd) {
        this.cd = cd;
    }

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

}

不管是构造器、Setter方法还是其它方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么这个bean将会被装配起来。

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

如下所示:

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

默认情况下是必须要注入依赖的。而且要注意的是:把required属性设置为false时,你需要谨慎对待,如果在你的代码中没有进行Null检查的话,这个处于未装配状态的属性有可能会出现NullPointException。

如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。

@Autowired是Spring特有的注解。可以将@Inject注解和其相互替换。

验证自动装配

代码如下:

package com.chenjl.test;

import com.chenjl.autowire.CDPlayerConfig;
import com.chenjl.autowire.CompactDisc;
import com.chenjl.autowire.MediaPlayer;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.*;
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.assertEquals;
import static org.junit.Assert.assertNotNull;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayTest {

    @Rule
    public final StandardOutputStreamLog log = new StandardOutputStreamLog();

    @Autowired
    CompactDisc compactDisc;

    @Autowired
    private MediaPlayer mediaPlayer;

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

    @Test
    public void play () {
        mediaPlayer.play();
        assertEquals("最炫名族风 played by 凤凰传奇", log.getLog());
    }

}

在测试代码中使用System.out.println()是稍微有点棘手的事情,因此,在该样例中使用了StandardOutputStreamLog,这是来源于System Rules库的一个JUnit规则,该规则能够基于控制台的输出编写断言,在这里,我们断言SgtPepers的play方法的输出被发送到了控制台上。

通过Java代码装配bean

尽管很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。

在这种情况下,必须要采用显示装配的方式。在进行显示装配的时候,有两种可选方案:Java和XML。

在进行显示配置时,JavaConfig是更好的方案,因为它更强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其它Java代码一样。

同时,JavaConfig与其它的java代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其它的组件一样都使用相同的语言进行表述,但JavaConfig是配置代码。这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。尽管不是必须的,但通常会将JavaConfig放到单独的包中,使它与其它的应用逻辑分离开来,这样对它的意图就不会产生困惑了。

创建配置类

声明简单的bean

要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解,比方说,下面的代码声明了CompactDisc bean 注解:

package com.chenjl.config;

import com.chenjl.autowire.CDDisc;
import com.chenjl.autowire.CompactDisc;
import com.chenjl.autowire.SgtPepers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = {CompactDisc.class,CDDisc.class})
public class CDPlayerConfig {

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

}

@Bean注解会告诉Spring这个方法会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。

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

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

不管你采用什么方法来为bean命名,bean声明都是非常简单的。方法体返回了一个新的SgtPeppers实例。这里是使用Java来进行描述的,因此我们可以发挥Java提供的所有功能,只要最终生成一个CompactDisc实例即可。

请稍微发挥下你的想象力,我们可能希望做点疯狂的事情,比如说,在一组CD中随机选择一个CompactDisc来播放:

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

借助JavaConfig实现注入

我们前面所声明的CompactDisc bean是非常简单的,它自身没有其它的依赖。但现在,我们需要声明CDPlayerbean,它依赖于CompactDisc。在JavaConfig中,要如何将他们装配在一起呢?

在JavaConfig中装配bean的最简单方式就是引用创建bean的方法。例如下面就是声明CDPlayer的可行方案:

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

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

创建XML配置规范

在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。在XML配置中,要创建一个XML配置文件,并且要以<beans>元素为根。

最为简单的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>

声明一个简单的bean

要在基于XML的Spring配置中声明一个bean,我们要使用spring-beans模式中的另外一个元素:<bean>。<bean>元素类似于JavaConfig中的@Bean注解。我们可以按照如下的方式声明CompactDiscbean:

<bean class="com.chenjl.autowire.SgtPepers"/>

这里声明了一个很简单的bean,创建这个bean的类通过class属性来指定的,并且要使用全限定的类名。

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

尽管自动化的bean命名方式非常方便,但如果你要稍后引用它的话,那自动产生的名字就没有多大用处了。因此,通常来讲更好的办法是借助id属性,为每个bean设置一个你自己选择的名字:

<bean id="sgtPepers" class="com.chenjl.autowire.SgtPepers"/>

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

当Spring发现这个bean元素时,它将会调用SgtPepers默认的构造器来创建bean。在XML配置中,bean的创建显得更加被动,不过,它并没有JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。

借助构造器注入初始化bean

在Spring XML配置中,只有一种声明bean的方式:使用<bean>元素并指定class属性。Spring会从这里获取必要的信息来创建bean。但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:

<Constructor-arg>元素

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

两者的区别在很大程度就是是否冗长繁琐。可以看到,<constructor-arg>元素比c-命名空间更加冗长,从而导致XML更加难以读懂。另外,有些事情<constructor-arg>可以做到,但是使用c-命名空间却无法实现。

构造器注入bean引用

<bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer">
        <constructor-arg ref="sgtPepers"/>
    </bean>

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

作为替代的方案,你也可以使用Spring的c-命名空间。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:context="http://www.springframework.org/schema/context"
       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
         http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context.xsd">

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

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

在编写前面的样例时,关于c-命名空间,引用参数的名称看起来有点怪异,因为这需要在编译代码的时候,将调试标志保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能就无法正常执行了。替换的方案是我们使用参数在整个参数列表中的位置信息。

<bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer" c:_0-ref="sgtPepers"/>

使用索引来识别构造器参数感觉比使用名字更好一些,即便在构建的时候移除掉了调试标志,参数却依然保持相同的顺序。如果有多个构造器参数的话,这当然是很有用处的。在这里因为只有一个构造器参数,所以我们还会有另外一个方案——根本不用去标识参数:

<!-- 我试了好像报错了 -->
<bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer" c:_-ref="sgtPepers"/>

将字面量值注入到构造器中

首先创建一个CompactDisc的一个新实现,如下所示:

package com.chenjl.autowire;

public class BlankDisc implements CompactDisc{

    private String artist;

    private String title;

    public BlankDisc(String artist, String title) {
        this.artist = artist;
        this.title = title;
    }

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

现在,我们将已有的SgtPeppers替换为这个类:

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

使用c命名空间替换如下:

<bean id="blankDisc" class="com.chenjl.autowire.BlankDisc" c:title="The Beatles" c:artist="Sgt.Pepper's Lonely Hearts Club Band"/>

可以看到,装配字面量与装配引用的区别在于属性名中去掉了“-ref”后缀。与之类似,我们也可以通过参数索引装配相同的字面量值,如下所示:

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

XML不允许某个元素的多个属性具有相同的名字。因此,如果有两个或者更多的构造器参数的话,我们不能简单的使用下划线进行标示。但是如果只有一个构造器参数的话,我们就可以这样做了。

在装配bean引用和字面量值方面,<constructor-arg>和c-命名空间的功能是相同的。但是有一种情况是<constructor-arg>能够实现的,c-命名空间却无法做到的。下面让我们看一下如何将集合装配到构造器参数中。

装配集合

上代码,如下:

package com.chenjl.autowire;

import java.util.List;

public class BlankDisc implements CompactDisc{

    private String artist;

    private String title;

    private List<String> tracks;

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

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

}

使用XML配置bean如下:

<bean id="compactDisc" class="com.chenjl.autowire.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>
            </list>
        </constructor-arg>
    </bean>

与之类似,也可以使用<ref>元素替代<value>,实现bean引用列表的装配,在此不做举例了。如果是Set集合,那元素就用<set>替代<list>。

设置属性

package com.chenjl.autowire;

import com.chenjl.autowire.interfaces.CompactDisc;
import com.chenjl.autowire.interfaces.MediaPlayer;
import org.springframework.beans.factory.annotation.Autowired;

public class CDPlayer implements MediaPlayer {

    private CompactDisc cd;

    // 也可以通过set方法注入依赖
    @Autowired
    public void setCd(CompactDisc cd) {
        this.cd = cd;
    }

}

该使用属性注入还是构造器注入呢?作为一个通用的规则,可以对强依赖使用构造器注入,对可选性的依赖使用属性注入。

如下XML设置属性:

<bean class="com.chenjl.autowire.CDPlayer">
        <property name="cd" ref="compactDisc"/>
    </bean>

Spring提供了更为简洁的p-命名空间,作为<property>元素的替代方案。如下所示:

<bean class="com.chenjl.autowire.CDPlayer" p:cd-ref="compactDisc"/>

当然,也可以将字面量注入到属性中,在此不作示例。

可以使用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:context="http://www.springframework.org/schema/context"
       xmlns:c="http://www.springframework.org/schema/c"
       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/context
          http://www.springframework.org/schema/context/spring-context.xsd">

借助<util-list>可以将磁道列表转移到BlankDisc bean之外,并将其声明到单独的bean之中,如下所示:

<util:list id="trackList">
        <value>Fixing a Hole</value>
        <value>Getting better</value>
    </util:list>

将磁道列表bean注入到BlankDisc bean的tracks属性中:

 <bean id="compactDisc" class="com.chenjl.autowire.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band"/>
        <constructor-arg value="The Beatles"/>
        <constructor-arg ref="trackList"/>
            <!--<list>-->
                <!--<value>Sgt.Pepper's Lonely Hearts Club Band</value>-->
                <!--<value>With a Little Help From My Friends</value>-->
            <!--</list>-->
        <!--</constructor-arg>-->
    </bean>

<util:list>只是util-命名空间中的多个元素之一,如下表列出了util-命名空间提供的所有元素:

导入和混合配置

在JavaConfig中引用JavaConfig:使用注解@Import(CDConfig.class),也可以引入多个:@Import(CDConfig.class,CDPlayerConfig.class)。

在JavaConfig中引用XML配置:使用注解@ImportResource("classpath:cd-config.xml")

在XML中引用XML配置:<import resource="cd-config.xml"/>

在XML中引用JavaConfig配置:直接将配置类作为bean注入<bean class="com.chenjl.config.CDPlayerConfig"/>

 

以上代码整合链接地址:https://github.com/1977288116/SpringAutowiringBean.git

 

posted @ 2020-05-24 22:14  咸鱼,也有梦想!  阅读(108)  评论(0)    收藏  举报