Spring:依赖注入DI

依赖注入有点像设计模式,不过主要还是对于耦合的处理,spring in action的一个例子讲得比较清晰

首先看一个骑士的实现类

package com.springinaction.knights;

import java.util.ResourceBundle;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: DamselRescuingKnight
* Author: lihui
* Date: 2018/6/15 上午0:14
*/

public class DamselRescuingKnight implements Knight {

private RescueDamselQuest quest;

public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}

public void embarkOnQuest() {
quest.embark();
}
}

这里在构造函数里new了一个RescueDamselQuest,这样DamselRescuingKnight就和RescueDamselQuest耦合在一起了,从而限制了这个骑士的能力,只能完成解救少女这个任务;而如果有其他任务,就无能为力了

使用DI,对象的依赖关系就由系统中负责协调各个对象的第三方组件在创建对象的时候来设定,对象本身不需要自行创建或者管理它们的依赖关系

NewImage

看下面一个骑士的实现类

package com.springinaction.knights;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: BraveKnight
* Author: lihui
* Date: 2018/6/14 上午8:22
*/

public class BraveKnight implements Knight {

private Quest quest;

//Quest被注入进来
public BraveKnight(Quest quest) {
this.quest = quest;
}

public void embarkOnQuest() {
quest.embark();
}
}

此时BraveKnight没有自己创建探险任务,而是在将探险任务作为构造函数的参数传入,这种就是构造器注入constructor injection,依赖注入的其中一种方式

骑士接口

package com.springinaction.knights;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Knight
* Author: lihui
* Date: 2018/6/14 上午8:27
*/

public interface Knight {
public void embarkOnQuest();
}

这里传入的探险类型Quest,这是一个接口,所有探险任务都要实现这个接口,因此BraveKnight能够响应RescueDamselQuest,SlayDragonQuest等任何Quest的实现

package com.springinaction.knights;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: Quest
* Author: lihui
* Date: 2018/6/14 上午8:31
*/

public interface Quest {

public void embark();
}

此时BraveKnight没有和任何Quest的实现耦合,探险任务只要是实现了Quest接口,具体是哪种类型的探险就都能够完成

这里DI的最大改变就是松耦合,如果一个对象只通过接口而不是具体实现或者初始化过程来表明依赖关系,那么这种依赖就能在对象本身毫不知情的情况下,用不同的具体实现进行替换

这里可以用mock来进行测试,只需要给一个Quest的mock实现

Maven里添加相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>spring-di</groupId>
<artifactId>spring-di</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
</dependency>
</dependencies>
</project>

这里用TestNG测试

package com.springinaction.knights;

import org.mockito.Mockito;
import org.testng.annotations.Test;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: BraveKnightTest
* Author: lihui
* Date: 2018/6/14 下午9:06
*/

public class BraveKnightTest {

@Test
public void testEmbarkOnQuest() {
Quest mockQuest = Mockito.mock(Quest.class);//创建mock Quest
BraveKnight knight = new BraveKnight(mockQuest);//注入mock Quest
knight.embarkOnQuest();
Mockito.verify(mockQuest, Mockito.times(1)).embark();
}
}

这里通过Mockito创建一个Quest接口的mock实现,通过该mock对象,新建一个BraveKnight实例,并通过构造函数注入这个mock Quest;当调用embarkOnQuest()方法的时候,就可以要求Mockito框架验证Quest的mock实现的embark()方法只被调用了一次,测试结果

==============================================
Default Suite
Total tests run: 1, Failures: 0, Skips: 0
===============================================


Process finished with exit code 0

回到上面例子,此时BraveKnight类可以接受任意传递给它的一种Quest实现,但需要知道如何把特定的Query实现传递给它

比如此时BraveKnight要完成的探险任务是杀掉一只喷火龙

下面SlayDragonQuest就是要注入到BraveKnight中的Quest具体实现

package com.springinaction.knights;

import java.io.PrintStream;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: SlayDragonQuest
* Author: lihui
* Date: 2018/6/14 上午8:30
*/

public class SlayDragonQuest implements Quest {
private PrintStream stream;

public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}

public void embark() {
stream.println("spring-di: slay the dragon");
}
}

这里SlayDragonQuest实现了Quest接口,就可以注入到BraveKnight中,这里没有用System.out.println(),而是请求一个比较通用的PringStream

现在的问题是,如何将SlayDragonQuest交给BraveKnight,以及如何将PrintStream交给SlayDragonQuest

创建应用组件之间协作称为装配wiring,Spring有多种装配bean的方式,一般使用XML描述

在resources/META-INF.spring下面创建一个XML配置文件knights-di.xml,它能够将BraveKnight,SlayDragonQuest和PrintStream装配在一起

<?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">

<bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest"></constructor-arg>
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}"></constructor-arg>
</bean>
</beans>

这里BraveKnight和SlayDragonQuest声明为Spring的bean

BraveKnight bean,在构造时传入了对SlayDragonQuest bean的引用,将它作为构造函数的参数,同时SlayDragonQuest bean的声明使用了Spring表达式,将System.out(是个PrintStream)传入到了SlayDragonQuest构造器当中

这样Spring通过应用上下文Application Context装载bean的定义并把它们组装起来,Application Context负责对象的创建和组装;当然Spring自带多种应用上下文的实现,主要区别是如何加载配置

这里knights-di.xml中的bean是使用XML文件进行配置,所以选择ClassPathXmlApplicationContext作为Application Context比较合适;该类加载位于应用程序类路径下的一个或者多个XML配置文件

package com.springinaction.knights;

import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* Copyright (C), 2014-2018, maoxiaomeng.com
* FileName: KnightMain
* Author: lihui
* Date: 2018/6/14 上午8:38
*/

public class KnightMain {

public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("META-INF/spring/knights-di.xml"); //加载Spring上下文
Knight knight = context.getBean(Knight.class); //获取knight bean
knight.embarkOnQuest(); //使用knight
context.close();
}
}

main()方法调用ClassPathXmlApplicationContext加载knights-di.xml,并获取Knight对象的引用

使用knights-di.xml文件创建Spring Application Context,接着调用该Application Context获取一个ID为knight的bean,得到Knight对象引用后,直接调用embarkOnQuest()方法就可以执行各种具体探险任务了;这个类完全不知道骑士接受了什么探险任务,而且完全不知道是由BraveKnight来执行的,只有knights-di.xml才知道是哪个骑士执行了哪个探险任务

发表评论