Spring实战2:装配bean—依赖注入的本质

主要内容

  • Spring的配置方法概览
  • 自动装配bean
  • 基于Java配置文件装配bean
  • 控制bean的创建和销毁

任何一个成功的应用都是由多个为了实现某个业务目标而相互协作的组件构成的,这些组件必须相互了解、能够相互协作完成工作。例如,在一个在线购物系统中,订单管理组件需要与产品管理组件以及信用卡认证组件协作;这些组件还需要跟数据库组件协作从而进行数据库读写操作。

在Spring应用中,对象无需自己负责查找或者创建与其关联的其他对象,由容器负责将创建各个对象,并创建各个对象之间的依赖关系。例如,一个订单管理组件需要使用信用卡认证组件,它不需要自己创建信用卡认证组件,只需要定义它需要使用信用卡认证组件即可,容器会创建信用卡认证组件然后将该组件的引用注入给订单管理组件。

创建各个对象之间协作关系的行为通常被称为装配(wiring),这就是依赖注入(DI)的本质。

2.1 Spring的配置方法概览

正如在Spring初探一文中提到的,Spring容器负责创建应用中的bean,并通过DI维护这些bean之间的协作关系。作为开发人员,你应该负责告诉Spring容器需要创建哪些bean以及如何将各个bean装配到一起。Spring提供三种装配bean的方式:

  • 基于XML文件的显式装配
  • 基于Java文件的显式装配
  • 隐式bean发现机制和自动装配

绝大多数情况下,开发人员可以根据个人品味选择这三种装配方式中的一种。Spring也支持在同一个项目中混合使用不同的装配方式。

我的建议是:尽可能使用自动装配,越少写显式的配置文件越好;当你必须使用显式配置时(例如,你要配置一个bean,但是该bean的源码不是由你维护),尽可能使用类型安全、功能更强大的基于Java文件的装配方式;最后,在某些情况下只有XML文件中才又你需要使用的名字空间时,再选择使用基于XML文件的装配方式。

2.2 自动装配bean

Spring通过两个特性实现自动装配:

  • Component scanning——Spring自动扫描和创建应用上下文中的beans;
  • Autowiring——Spring自动建立bean之间的依赖关系;

这里用一个例子来说明:假设你需要实现一个音响系统,该系统中包含CDPlayer和CompactDisc两个组件,Spring将自动发现这两个bean,并将CompactDisc的引用注入到CDPlayer中。

2.2.1 创建可发现的beans

首先创建CD的概念——CompactDisc接口,如下所示:

package com.spring.sample.soundsystem;

public interface CompactDisc {
    void play();
}

CompactDisc接口的作用是将CDPlayer与具体的CD实现解耦合,即面向接口编程。这里还需定义一个具体的CD实现,如下所示:

package com.spring.sample.soundsystem;

import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {
    private String title = "Sgt. Perppers' Lonely Hearts Club Band";
    private String artist = "The Beatles";

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

这里最重要的是@Component注解,它告诉Spring需要创建SgtPeppers bean。除此之外,还需要启动自动扫描机制,有两种方法:基于XML配置文件;基于Java配置文件,代码如下(二选一):

  • 创建soundsystem.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="com.spring.sample.soundsystem" />
</beans>

在这个XML配置文件中,使用<context:component-scan>标签启动Component扫描功能,并可设置base-package属性。

  • 创建Java配置文件
package com.spring.sample.config;

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

@Configuration
@ComponentScan(basePackages = "com.spring.sample.soundsystem")
public class SoundSystemConfig {
}

在这个Java配置文件中有两个注解值得注意:@Configuration表示这个.java文件是一个配置文件;@ComponentScan表示开启Component扫描,并且可以设置basePackages属性——Spring将会设置该目录以及子目录下所有被@Component注解修饰的类。

  • 自动配置的另一个关键注解是@Autowired,基于之前的两个类和一个Java配置文件,可以写个测试
package com.spring.sample.soundsystem;

import com.spring.sample.config.SoundSystemConfig;
import org.junit.Assert;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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SoundSystemConfig.class)
public class SoundSystemTest {
    @Autowired
    private CompactDisc cd;

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

运行测试,测试通过,说明@Autowired注解起作用了:自动将扫描机制创建的CompactDisc类型的bean注入到SoundSystemTest这个bean中。

2.2.2 给被扫描的bean命名

在Spring上下文中,每个bean都有自己的ID。在上一个小节的例子中并没有提到这一点,但Spring在扫描到SgtPeppers这个组件并创建对应的bean时,默认给它设置的ID为sgtPeppers——是的,这个ID就是将类名称的首字母小写。

如果你需要给某个类对应的bean一个特别的名字,则可以给@Component注解传入指定的参数,例如:

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

2.2.3 设置需要扫描的目标basepackage

在之前的例子中,我们通过给@Component注解传入字符串形式的包路径,来设置需要扫描指定目录下的类并为之创建bean。

可以看出,basePackages是复数,意味着你可以设置多个目标目录,例如:

@Configuration
@ComponentScan(basePackages = {"com.spring.sample.soundsystem", "com.spring.sample.video"})
public class SoundSystemConfig {
}

这种字符串形式的表示虽然可以,但是不具备“类型安全”,因此Spring也提供了更加类型安全的机制,即通过类或者接口来设置扫描机制的目标目录,例如:

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

通过如上设置,会将CDPlayer和DVDPlayer各自所在的目录作为扫描机制的目标根目录。

如果应用中的对象是孤立的,并且互相之间没有依赖关系,例如SgtPeppersbean,那么这就够了。

2.2.4 自动装配bean

简单得说,自动装配的意思是让Spring从应用上下文中找到对应的bean的引用,并将它们注入到指定的bean。通过@Autowired注解可以完成自动装配。

例如,考虑下面代码中的CDPlayer类,它的构造函数被@Autowired修饰,表明当Spring创建CDPlayer的bean时,会给这个构造函数传入一个CompactDisc的bean对应的引用。

package com.spring.sample.soundsystem;

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

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

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

还有别的实现方法,例如将@Autowired注解作用在setCompactDisc()方法上:

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

或者是其他名字的方法上,例如:

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

更简单的用法是,可以将@Autowired注解直接作用在成员变量之上,例如:

@Autowired
private CompactDisc cd;

只要对应类型的bean有且只有一个,则会自动装配到该属性上。如果没有找到对应的bean,应用会抛出对应的异常,如果想避免抛出这个异常,则需要设置@Autowired(required=false)。不过,在应用程序设计中,应该谨慎设置这个属性,因为这会使得你必须面对NullPointerException的问题。

如果存在多个同一类型的bean,则Spring会抛出异常,表示装配有歧义,解决办法有两个:(1)通过@Qualifier注解指定需要的bean的ID;(2)通过@Resource注解指定注入特定ID的bean;

2.2.5 验证自动配置

通过下列代码,可以验证:CompactDisc的bean已经注入到CDPlayer的bean中,同时在测试用例中是将CDPlayer的bean注入到当前测试用例。

package com.spring.sample.soundsystem;

import com.spring.sample.config.SoundSystemConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SoundSystemConfig.class)
public class CDPlayerTest {
    public final Logger log = LoggerFactory.getLogger(CDPlayerTest.class);
    @Autowired
    private MediaPlayer player;

    @Test
    public void playTest() {
        player.play();
    }
}

2.3 基于Java配置文件装配bean

Java配置文件不同于其他用于实现业务逻辑的Java代码,因此不能将Java配置文件业务逻辑代码混在一起。一般都会给Java配置文件新建一个单独的package。

2.3.1 创建配置类

实际上在之前的例子中我们已经实践过基于Java的配置文件,看如下代码:

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

@Configuration注解表示这个类是配置类,之前我们是通过@ComponentScan注解实现bean的自动扫描和创建,这里我们重点是学习如何显式创建bean,因此首先将@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})这行代码去掉。

2.3.2 定义bean

通过@Bean注解创建一个Spring bean,该bean的默认ID和函数的方法名相同,即sgtPeppers。例如:

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

同样,可以指定bean的ID,例如:

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

可以利用Java语言的表达能力,实现类似工厂模式的代码如下:

@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 if (choice == 3) {
        return new Revolover();
    }
}

2.3.3 JavaConfig中的属性注入

最简单的办法是将被引用的bean的生成函数传入到构造函数或者set函数中,例如:

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

看起来是函数调用,实际上不是:由于sgtPeppers()方法被@Bean注解修饰,所以Spring会拦截这个函数调用,并返回之前已经创建好的bean——确保该SgtPeppers bean为单例。

假如有下列代码:

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

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

如果把sgtPeppers()方法当作普通Java方法对待,则cdPlayerbean和anotherCDPlayerbean会持有不同的SgtPeppers实例——结合CDPlayer的业务场景看:就相当于将一片CD同时装入两个CD播放机中,显然这不可能。

默认情况下,Spring中所有的bean都是单例模式,因此cdPlayeranotherCDPlayer这俩bean持有相同的SgtPeppers实例。

当然,还有一种更清楚的写法:

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

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

这种情况下,cdPlayeranotherCDPlayer这俩bean持有相同的SgtPeppers实例,该实例的ID为lonelyHeartsClub。这种方法最值得使用,因为它不要求CompactDisc bean在同一个配置文件中定义——只要在应用上下文容器中即可(不管是基于自动扫描发现还是基于XML配置文件定义)。

2.4 基于XML配置文件装配bean

这种是Spring中最原始的定义方式,在此不再详述。

2.5 混合使用多种配置方法

通常,可能在一个Spring项目中同时使用自动配置和显式配置,而且,即使你更喜欢JavaConfig,也有很多场景下更适合使用XML配置。幸运的是,这些配置方法可以混合使用。

首先明确一点:对于自动配置,它从整个容器上下文中查找合适的bean,无论这个bean是来自JavaConfig还是XML配置。

2.5.1 在JavaConfig中解析XML配置

  • 通过@Import注解导入其他的JavaConfig,并且支持同时导入多个配置文件;
@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}
  • 通过@ImportResource注解导入XML配置文件;
@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath: cd-config.xml")
public class SoundSystemConfig {
}

2.5.2 在XML配置文件中应用JavaConfig

  • 通过<import>标签引入其他的XML配置文件;
  • 通过<bean>标签导入Java配置文件到XML配置文件,例如
<bean class="soundsystem.CDConfig" />

通常的做法是:无论使用JavaConfig或者XML装配,都要创建一个root configuration,即模块化配置定义;并且在这个配置文件中开启自动扫描机制:<context:component-scan>或者@ComponentScan

2.6 总结

这一章中学习了Spring 装配bean的三种方式:自动装配、基于Java文件装配和基于XML文件装配。

由于自动装配几乎不需要手动定义bean,建议优先选择自动装配;如何必须使用显式配置,则优先选择基于Java文件装配这种方式,因为相比于XML文件,Java文件具备更多的能力、类型安全等特点;但是也有一种情况必须使用XML配置文件,即你需要使用某个名字空间(name space),该名字空间只在XML文件中可以使用。

参考资料

  1. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html

本号专注于后端技术、JVM问题排查和优化、Java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。


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

推荐阅读更多精彩内容