依赖注入DI(dependency injection)

下面通过一个骑士例子来认识依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.knights;
public class DamselRescuingKnight {
private RescueDamselQuest quest;
public DamselRescuingKnight()
{
//与RescueDamselQuest紧耦合
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest()
{
quest.embark();
}
}

可以看到DamselRescuingKnight在它的构造函数中自行创建了RescueDamselQuest,这使得这两个类紧密地耦合在一起,因此极大地限制了这个骑士执行探险的能力,因为他只能执行RescueDamselQuest任务。更糟糕的是为这个骑士类编写单元测试出奇地困难。因为无法对embark函数进行打桩。

通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系。
骑士BraveKnight足够灵活可以接受任何赋予他的探险任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.knights;
public class BraveKnight implements Knight{
private Quest quest;
//Quest被注入进来
public BraveKnight(Quest quest)
{
this.quest = quest;
}
public void embarkOnQuest()
{
quest.embark();
}
}

可以看到BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入。
更重要的是,传入的探险类型是Quest,因此BraveKnight能够执行任何实现了该接口的探险任务。

对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的;但是可以轻松地测试BraveKnight,只需给他一个Quest的mock实现即可。

将Quest注入到Knight中
创建应用组件之间协作的行为通常称为装配。Spring有多种装配bean的方式,如下代码展示了如何采用XML进行装配,将BraveKnight、SlayDragonQuest和PrintStream装配到一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="knight" class="com.knights.BraveKnight">
<constructor-arg ref="quest"/>
</bean>
<bean id="quest" class="com.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}"/>
</bean>
</beans>

SlayDragonQuest类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.knights;
import java.io.PrintStream;
public class SlayDragonQuest implements Quest{
private PrintStream stream;
public SlayDragonQuest(PrintStream stream)
{
this.stream = stream;
}
@Override
public void embark() {
stream.println("embarking on quest to slay the dragon.");
}
}

Spring通过应用上下文装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现。它们之间主要的区别仅仅在于如何装载配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.knights;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args)
{
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aa.xml");
Knight knight = context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}

只有aa.xml文件知道哪个骑士执行了哪种探险任务。

面向切面编程AOP(aspect-oriented programming)

系统由许多不同的组件组成,每一个组件各负责一块特定的功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。
如果将这些关注点分散到多个组件中去,你的代码将会变得异常复杂。

AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。

假设我们使用吟游诗人这个服务类来记载骑士的所有事迹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.knights;
import java.io.PrintStream;
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream)
{
this.stream = stream;
}
//探险之前调用
public void singBeforeQuest()
{
stream.println("fa la la");
}
//探险之后调用
public void singAfterQuest()
{
stream.println("--end quest---");
}
}

如下代码展示了BraveKnight和Minstrel组合起来的第一次尝试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.knights;
public class BraveKnight implements Knight{
private Quest quest;
private Minstrel minstrel;
//Quest被注入进来
public BraveKnight(Quest quest, Minstrel minstrel)
{
this.quest = quest;
this.minstrel = minstrel;
}
@Override
public void embarkOnQuest()
{
minstrel.singBeforeQuest();
quest.embark();
minstrel.singAfterQuest();
}
}

这应该可以达到预期效果。现在,你所需要做的就是回到Spring配置中,声明Minstrel bean并将其注入到BraveKnight的构造器之中。但是,请稍等……
我们似乎感觉有些东西不太对。管理他的吟游诗人真的是骑士职责范围内的工作吗?在我看来,吟游诗人应该做他份内的事,根本不需要骑士命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?
此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到BarveKnight类中。这不仅使BraveKnight的代码复杂化了,而且还让我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果Minstrel为null会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景?

可以将Minstrel声明为一个切面
xml文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
<bean id="knight" class="com.knights.BraveKnight">
<constructor-arg ref="quest"/>
</bean>
<bean id="quest" class="com.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}"/>
</bean>
<bean id="minstrel" class="com.knights.Minstrel">
<constructor-arg value="#{T(System).out}"/>
</bean>
<aop:config>
<aop:aspect ref="minstrel">
<!--定义切点-->
<aop:pointcut id="embark1"
expression="execution(* *.embarkOnQuest(..))"/>
<!--声明前置通知-->
<aop:before pointcut-ref="embark1"
method="singBeforeQuest"/>
<!--声明后置通知-->
<aop:after pointcut-ref="embark1"
method="singAfterQuest"/>
</aop:aspect>
</aop:config>
</beans>

测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.knights;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args)
{
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aa.xml");
Knight knight =context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}

可以看到输出结果为

1
2
3
fa la la
embarking on quest to slay the dragon.
--end quest---

需要引入以下3个jar包,否则程序会运行失败。
aopalliance-1.0.jar
aspectjrt-1.8.6.jar
aspectjweaver-1.8.6.jar

参考
《Spring实战》
环境配置参考极客学院