Spring 中的 DI 注解

2020/10/28 58

Spring

我正在学习 Spring,Spring 提供了很多注解,在学习阶段先收集注解的作用,将来再来回看。

@Component

表明被标记的类会作为组件类,并告知 Spring 要为这个类创建 bean。没有必要显示配置 bean,因为被标记类使用了 @Component 注解,所以 Spring 会为我们把事情处理妥当。不过,组件扫描默认是不启用的,我们还需要显示配置一下 Spring,从而命令它去寻找带有 @Component 注解的类,并为其创建 bean。

为组件扫描的 bean 命名

Spring 应用上下文中所有的 bean 都会给顶一个 ID。相对于使用 xml 的方案,我更喜欢基于 Java 的配置,在基于 Java 的配置中,尽管我们没有明确地为 bean 设置 ID,但 Spring 会根据类名指定一个 ID。具体来讲,这个 bean 所给定的 ID 是将类名的第一个字母变为小写。SgtPeppers -> sgtPeppers。

如果你想为这个 bean 设置不同的 ID,那么仅仅只需要把期望的 ID 作为值传递给 @Component 注解:

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

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

package soundsystem;
import javax.inject.Named;

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

Spring 支持将 @Named 作为 @Component 注解的替代方案。两者之间有一些细微的差异,但是大多数场景中,他们是可以互相替换的。

话虽如此,我更加强烈地喜欢 @Component 注解,而对 @Named... 怎么说呢,我感觉它的名字起得很不好,它并没有像 @Component 那样清楚地表明它是做什么的。

@Configuration

创建 JavaConfig 类的关键在于为其添加 @Configuration 注解,@Configuration 注解表明这个类是一个配置类,通过此类,Spring 应用将知道如何创建 bean 的细节。

@ComponentScan

在 Spring 中启用组件扫描。如果没有其它配置的话,@ComponentScan 默认会扫描与配置类相同的包以及子包,查找带有 @Component 注解的类。

但是,如果你想扫描不同的包,那么该怎么办呢?或者,如果你想扫描多个基础包,那又该怎么办呢?

有一个原因会促使我们明确地设置基础包,那就是我们想要将配置类放在单独的包中,使其与其他的应用代码区分开来。如果是这样的话,那默认的基础包就不能满足要求了。

要满足这样的需求其实是完全没有问题的!为了指定不同的基础包,你所需要做的就是在 @ComponentScan 的 value 属性中指明包的名称:

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

如果你想更加清晰地表明你所设置的是基础包,那么你可以通过 basePackages 属性进行配置:

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

可能你已经注意到 basePackages 属性使用的是复数形式,如果你想要设置多个基础包,只需要设置一个数组即可:

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

上面的例子中,是通过 String 类型表示基础包的,这没有什么错,但是这种方式不是类型安全的,如果一旦更改命名空间,那么就会出现错误了。

@ComponentScan 还提供了另外一种方法,那就是将其指定为包中所包含的类或接口:

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

我们可以进一步优化它,在包中创建一个用来进行扫描的空标记接口(marker interface)。通过标记接口的方式,你依然能够保持对重构友好的接口引用。

@Autowired

在你的应用程序中,如果所有的对象都是相互独立的,没有任何依赖,那么你所需要的可能就是组件扫描而已。但是,很多对象会依赖其它对象才能完成任务。这样的话,我们就需要了解一下 Spring 自动化配置的另外一方面的内容,那就是自动装配。

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

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

@Component
public class CDPlayer implements MediaPlayer {
    private CompactDisc cd;

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

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

@Autowired 注解不仅能够用在构造器上,还能在属性的 Setter 方法上

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

实际上,Setter 方法并没有什么特殊之处。@Autowired 可以用在类的任何方法上

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

如果没有匹配的 bean,那么在应用上下文创建的时候,Spring 会抛出一个异常。为了避免异常的出现,你可以将 @Autowired 的 required 属性设置为 false。但是把 reuqired 设置为 false 时,你需要谨慎对待。如果在你的代码中没有进行 null 检查的话,你可能会得到一个 NullPointerException。

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

@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 注解来源于 Java 依赖注入规范,该规范同时还为我们定义了 @Named 注解。在自动装配中,Spring 同时还支持 @Inject 和 @Autowired。尽管它们之间有着一些细微的差别,但是在大多数场景下,它们都是可以互相替换的。

@Bean

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

在这种情况下,你必须显式装配。有两种方案可供选择:JavaConfig 和 XML。JavaConfig 是更好的方案,因为它强大、类型安全且对重构友好。同时 JavaConfig 与其他 Java 代码又有所区别,它不应该包含任何业务逻辑,JavaConfig 也不应该侵入到业务逻辑代码中。通常会将 JavaConfig 放到单独的包中,与其它应用程序分离,这样对它的意图就不会产生困惑了。

声明简单的 bean

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

@Bean 注解告诉 Spring,这个方法将会返回一个对象,该对象要注册为 Spring 应用上下文中的 bean。

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

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

请发挥你的想象力,我们就可以做出疯狂的事情,比如,在一组 CD 中随机选择一个 CompactDisc 来播放:

@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 == 3)
        return new HardDaysNight();
    else
        return new Revolver();
}

引用其它 bean

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

注意,上面的代码调用了需要传入了 CompactDisc 对象的构造器来创建实例。看起来,CompactDisc 是通过调用 sgtPeppers() 得到的,但情况并非完全如此。因此 sgtPeppers() 方法上添加了 @Bean,Spring 但会拦截所有对它的调用,并确保直接返回该方法所创建的 bean,而不是每次都对其进行实际调用。默认情况下,Spring 中的 bean 都是单例的。

其实还有一种理解起来更为简单的方式:

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

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

@Primary

自动装配也有它的短处,仅有一个 bean 匹配时,自动装配才是有效的。如果有多个匹配的 bean 存在,就会产生歧义。

@Autowired
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

@Component
public class Cake implements Dessert { ... }
@Component
public class Cookies implements Dessert { ... }
@Component
public class IceCream implements Dessert { ... }

这三个类都是用了 @Component 注解,在组件扫描时,能够发现它们并将其创建为 Spring 应用上下文里面的 bean。然后,当 Spring 试图自动装配 setDessert() 中的 Dessert 参数是,发现有多个值可供选择,Spring 会抛出 NoUniqueBeanDefinitionException。

在 Scpring 中,可以通过 @Primary 来表达最主要的方案,如果你使用 XML 配置的话,可以用 primary="true" 来设置首选 bean。

但是,如果你标记了多个首选项,那么它就无法正常工作了。

@Qualifier

@Primary 虽然能在一定程度上消除歧义,但是也可能会产生新的歧义。Spring 的限定符能够在所有可选的 bean 上进行缩小范围操作,最终能够达到只有一个 beam 满足要求。如果将来所有的限定符都用上后,依然存在歧义,那么可以添加更多的限定符来缩小选择范围。

@Qualifier 注解是使用限定符的主要方式。它可以与 @Autowired 和 @Inject 协同使用,在注入的时候指定想要注入进去的是哪个 bean。

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

@Qualifier("iceCream") 指向的是组件扫描时所创建的 bean,并且这个 bean 是 IceCream 的实例。

使用默认的 bean ID 作为限定符是非常简单的,但是可能会引入一些问题。如果你重构了 IceCream 类,将其重命名为 Gelato 的话,自动装配将会失败。

我们可以为 bean 设置自己的限定符,而不是依赖于 beanID,所需要做的就是在 bean 声明上添加 @Qualifier 注解:

@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }

把 cold 限定符分配给 IceCream bean,因为它没有耦合类名,因此可以随便重构 IceCream 类名,在诸如的地方,只要引用 cold 限定符就可以了:

@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

面向特性的限定符要比基于 beanID 的限定符更好一些。但是,如果多个 bean 都具备相同特性的话,也会出现问题,例如,如果引入了这个新的 Dessert bean,会发生什么呢:

@Component
@Qualifier("cold")
public class Popsicle implements Dessert { ... }

在自动装配时,再次遇到歧义性的问题,需要使用更多的限定符来缩小可选范围

@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert { ... }

@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }

@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

这里有个小问题,Java 不允许在同一个条目上重复出现相同类型的多个注解。在这里,使用 @Qualifier 注解并没有办法将自动装配的可选范围缩小至仅有一个 bean。

但是,我们可以创建自定义注解,借助这样的注解来表达 bean 所希望限定的特性。

@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }

同样,你可以创建一个新的 @Creamy 来代替 @Qualifier("creamy")

@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy { }

现在更改以下之前的代码:

@Component
@Cold
@Creamy
public class IceCream implements Dessert { ... }

@Component
@Cold
@Fruity
public class Popsicle implements Dessert { ... }

@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有 Java 编译器的限制或错误。与此同时,相对于使用原始的 @Qualifier 并借助 String 类型来指定限定符,自定义的注解也更为类型安全。

@RunWith

虽然 @RunWith 不是 Spring 提供的,但我还是要记录一下它。在 Spring 单元测试中,使用 @RunWith(SpringJUnit4ClassRunner.class) 和测试允许于 Spring 测试环境,以便在测试开始的时候自动创建 Spring 应用上下文,便于你测试。

@ContextConfiguration

告诉它需要在哪个类中加载配置。