一、依赖注入

考虑这样一个例子,CustomerDAO 类使用了 DBUtil 类中的方法连接到 MySQL 数据库

public class CustomerDAO
{
    private DBUtil databaseUtil = new DBUtil();

    public Customer findCustomerByName(String name)
    {
        databaseUtil.doSomething()
    }
}

假设现在想要改为使用 Oracle 数据库,需要怎么做?可以使用 OracleDBUtil 继承 DBUtil,并重写其相关方法。但是由于 CustomerDAO 中将 DBUtil 实例添加到其初始化过程中。因此想要修改的话还是需要对 CustomerDAO 本身进行修改。这违反了开-闭原则。

public class CustomerDAO
{
    private DBUtil databaseUtil = new OracleDBUtil();

    public Customer findCustomerByName(String name)
    {
        databaseUtil.doSomething()
    }
}

为了使对 dbUtil 的修改不影响 CustomerDAO 本身,我们可以将实例的创建移到 CustomerDAO 之外。

public class CustomerDAO
{
    private DBUtil databaseUtil;

    public CustomerDAO(DBUtil databaseUtil)
    {
        this.databaseUtil = databaseUtil;
    }

    public Customer findCustomerByName(String name)
    {
        databaseUtil.doSomething()
    }
}

这样,我们就可以自主选择 DBUtil 的具体实现,同时不会造成对 CustomerDAO 的修改。保证了开闭原则,降低了代码的耦合程度,有助于更有效的代码复用。

DBUtil databaseUtil = new OracleDBUtil();
var customerDAO = new CustomerDAO(databaseUtil) 

这样的方法称为依赖注入。也就是,一个类对其他类的依赖,并不存在于这个类中,而是通过构造函数或其他方法传入这个类中。这样就使得编程时只需要关注抽象的依赖,而不需要考虑具体的实现。

二、spring的依赖注入

依赖注入是 spring 的核心之一。spring 提供了通过 java 注释或 xml 进行依赖注入配置的方法。这里只介绍通过 java 注释实现的手动装配和自动装配。

接下来本文会举一个经典例子来比较手动装配和自动装配的不同。考虑我们需要一个 CDPlayer,其中包含 CD。CD 具有 play() 方法,需要 CDPlayer 调用才能播放。

三、自动化装配

不管自动或手动装配,都需要创建配置类,并为其添加注释 @Configuration。这一配置类将提供可供 spring 维护的用于装配的类。这些类称为 Bean(爪哇咖啡豆!!)。

@Configuration
public class CDPlayerConfig

自动化装配方式中,用于指示类与类的依赖关系的标志分散在各个类中。给类添加注释 @Component 表示该类为一个 Bean。或者可以认为这表示配置类中会自动生成一个创建该类型 Bean 的方法。

@Component
public class SgtPeppers implements CompactDisc

同时在需要被注入的位置,需要添加 @Autowired 方法。表示该属性是自动装配的。

@Autowired
private CompactDisc cd;

最后,在配置类中再添加一条注释 @ComponentScan,表示该配置类会扫描对应路径下所有带 @Component 注释的类,为这些类创建生成 Bean 的方法。

@Configuration
@ComponentScan
public class CDPlayerConfig

@ComponentScan 默认扫描与配置类在同一包或在子包下的所有 Component,可以通过 @ComponentScan("包名")@ComponentScan({"包名1", "包名2"}) 来设置扫描路径。

最终各文件代码如下(去除导入语句,各文件在同一包目录下):

// CompactDisc.java
public interface CompactDisc {
    void play();
}

// CDPlayerConfig.java
@Configuration
@ComponentScan
public class CDPlayerConfig {
}

// SgtPeppers.java
@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);
    }
}

// CDPlayer.java
@Component
public class CDPlayer {
    @Autowired
    private CompactDisc cd;

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

注意此时若直接通过 new 创建 CDPlayer 实例,cd 属性不会被装配。正确的使用方法是让 spring 托管对象的创建。这将在第五节介绍。

四、注释手动装配

手动装配 Bean 需要自行编写配置类。但是不需要在其他类中添加注释。

配置类只需要添加注释 @Configuration,而不需要 @ComponentScan

@Configuration
public class CDPlayerConfig

接着在配置类中编写方法,返回值为要创建 Bean 的类型。在例子中,也就是要 SgtPeppersCDPlayer。同时要对方法添加 @Bean 注释。

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

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

注意在 cdPlayer() 方法中使用了 CDPlayer 的一个参数的构造函数。为了通过java 代码实现配置,实现对希望注入依赖的属性的访问(如构造函数,或 setter)是必要的。

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

通过 new CDPlayer(sgtPeppers()),我们就建立了 CDPlayer 和 SgtPeppers 的依赖关系。另外一种写法更加简洁,也更能体现组件之间的依赖关系。

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

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

最终各文件代码如下:

// CompactDisc.java
public interface CompactDisc {
    void play();
}

// CDPlayerConfig.java
@Configuration
public class CDPlayerConfig {
    @Bean
    public CompactDisc sgtPeppers() {
        return new SgtPeppers();
    }

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

// SgtPeppers.java
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);
    }
}

// CDPlayer.java
public class CDPlayer {
    private CompactDisc cd;

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

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

五、Bean 的使用

终于到了 Bean 的使用部分。被 @Bean@Component 注释,作为 Bean 的类,并不能直接通过 new 来创建实例,而只能通过被 Spring 托管的方式创建,这似乎类似于 jvm。

具体来说,要使用 Bean,需要创建一个 Spring 应用上下文(Application Context)的实例,并设定需要用到的 Bean 所在的配置类或 xml。通过这个实例获取各 Bean 对象实例,才能进行操作。

以 java 配置类为例,我们需要使用 CDPlayer 类,不能直接通过 new 创建实例。而是先创建上下文,并将配置类传入。

ApplicationContext context = new AnnotationConfigApplicationContext(CDPlayerConfig.class);

接着才能获取 CDPlayer 实例,并调用其方法。

CDPlayer cdPlayer = context.getBean(CDPlayer.class);
cdPlayer.playCD();