SlideShare uma empresa Scribd logo
1 de 39
- 1 -- 1 -
目次
1. 入门:依赖注入
2. 依赖注入的应用模式
3. 依赖注入对象的 Scope 及其生命周期
4. 依赖注入对象的行为增强( AOP )
- 2 -- 2 -
1. 入门:依赖注入——“依赖”的概念
从现实世界的观点来看,“依赖”即某
个实体对象为了完成某项功能,必须
要依托另外一些实体对象,那么这些
被依托的实体对象即被称为“依赖”
( Dependency ),而依托其它“依
赖”从而完成某项功能的对象,往往
被称作“依赖者”( Dependent )。
某储户类某储户类
public class Depositor {
private Bank bank; // 银行是其依赖
private DepositBook depositBook; // 存折是其依赖
public Cash withDraw(BigDecimal amount) { // 依托上面的依赖完成取款业务
return bank.withDraw(depositBook, amount);
}
}
银行类银行类
存折类存折类
- 3 -- 3 -
1. 入门:依赖注入——传统的组件耦合方式
这种手工 new 的组装方式,虽然是 Java
平台程序开发中最基本的方式,但是其会
带给我们在开发、维护以及测试等方面带
来极大的困扰。
构建依赖对象困难,并且往往依赖对象
与开发者正在开发的逻辑无关。
维护不便。只要有构造方法的改动就会
带来所有调用依赖的地方发生改变并且需
要回归测试。
单元测试时很难将 Mock 对象替入实际
依赖。
某储户类某储户类
银行类
存折类
手工组装模式手工组装模式
new
new
public class Depositor {
private Bank bank;
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
bank = new BankICBC("Tianjin", 100); // 初始化依赖
depositBook = new DepositBookICBC("62202", "current", "CNY"); // 初始化依赖
return bank.withDraw(depositBook, amount);
}
}
- 4 -- 4 -
1. 入门:依赖注入——传统的组件耦合方式
用工厂创建依赖对象,可以将创建对象的
过程隐蔽, Depositor 类的开发者利用这
种透明创建对象的方式则可避免手工构建
复杂依赖关系图所带来的诸多不便。并且
,当依赖的构造方法接口发生变更时,影
响到的也只是工厂类,而不会影响到调用
工厂类的依赖者类本身,从而降低了系统
的维护成本。
某储户类某储户类
工厂模式工厂模式
工厂类工厂类
工厂类工厂类
代码例见下一页。。
。
use
use
- 5 -- 5 -
1. 入门:依赖注入——传统的组件耦合方式
public class Depositor {
private Bank bank;
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
bank = new BankFactory().createBankICBC(); // 工厂创建依赖
depositBook = new DepositBookFactory().createDepositICBC();// 工厂创建依赖
return bank.withDraw(depositBook, amount);
}
}
public class BankFactory {
public Bank createBankICBC() {
return new BankICBC("Tianjin", 100);
}
}
public class DepositBookFactory {
public DepositBook createDepositICBC() {
return new DepositBookICBC("62202", "current", "CNY");
}
}
工厂模式工厂模式
- 6 -- 6 -
1. 入门:依赖注入——传统的组件耦合方式
工厂模式工厂模式
但是随着软件工程的不断复杂化和规模化,工厂模式同样的遇到了很多问题
而被开发人员诟病。围绕着这个话题的讨论有很多,我们不再一一列举,只
举一个比较常见的问题来说明一下。假设我们的银行不只有工商银行,还有
招商银行、中国银行等等,这样就需要我们维护一个越来越庞大的工厂类。
之前的 Bank 工厂就要逐渐这样增加 create 方法:
public class BankFactory {
public Bank createBankICBC() {
return new BankICBC("Tianjin", 100);
}
public Bank createBankCMB() { // …… };
public Bank createBankBOC() { // …… };
// ……
}
- 7 -- 7 -
1. 入门:依赖注入——传统的组件耦合方式
工厂模式工厂模式
这样工厂类自身的维护成本逐渐增大,就成为了工厂模式的一个很大的障碍。
因此很多开发者想办法用参数化的形式,来减少工厂类的维护量。
public class BankFactory {
public Bank createBank(BankVender bankVendor) { // 参数化的工厂方法
switch (bankVendor) {
case ICBC:
return createBankICBC();
case CMB:
return createBankCMB();
default:
return createBankICBC();
}
}
// ……
}
但是这样一来又会回到手工 new 对象的一个问题,即如果工厂方法的参数发
生了变化,可能会引起全部调用到该工厂方法的代码都需要修改及做相应的
回归测试。
- 8 -- 8 -
Service
Locator
容器
Service
Locator
容器
1. 入门:依赖注入——传统的组件耦合方式
ServiceLocatorServiceLocator 模模
式式
某储户类某储户类
get
get
public class Depositor {
private Bank bank;
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
bank = (Bank) new ServiceLocator().get("BankICBC");
depositBook = (DepositBook) new ServiceLocator().get("DepositBookICBC");
// 从 ServiceLocator 容器中获取依赖对象
return bank.withDraw(depositBook, amount);
}
}
“BankICBC”
“DepositBookICBC”
ServiceLocator ,即服务定位器,实质
上是一种改进版的工厂模式。这种模式
的核心思想是,将构建依赖的接口彻底
与依赖者分离,并将此依赖作为“服务”
绑定到一个标识符(通常是一个字符
串),而后依赖者则可通过这个标识符
获取被绑定的依赖。
- 9 -- 9 -
1. 入门:依赖注入——传统的组件耦合方式
这样一个能够管理这些依赖(即“服务”)的、类似于工厂类的一个单独
的类,也常常被称作“ ServiceLocator 容器”,这个容器负责各种依赖的注
册、标识符绑定、提供依赖对象等工作。但是实现这样一个容器对于普通开
发者来说代价就太大了。
因此, JCP 组织联合各 IT 供应商制订了统一的 ServiceLocator 容器
标准,开发者可以只管开发自己的依赖类,然后 IT 供应商提供的
ServiceLocator 容器实现(即各种应用服务器级中间件)通过统一的命名
规则将某个标识符绑定到这些依赖类并注册到容器中,等待开发者的请求命
令。
而 ServiceLocator 模式作为业界标准统一化后,一个最典型的应用便
诞生了: JNDI 。例如获取 RemoteEJB 的时候,我们可以执行这样的
JNDI 查找:
ServiceLocatorServiceLocator 模模
式式
RemoteBean rb = (RemoteBean) initialContext.lookup("java:comp/env/TEST/RemoteBeanImpl");
- 10 -- 10 -
1. 入门:依赖注入——传统的组件耦合方式
但是 ServiceLocator 模式最大的弱
点就是,依赖对象的获取强依赖于
ServiceLocator 容器,除非开发者
愿意花费大量成本自己实现
ServiceLocator 容器,否则就必须
要中间件提供商的支持,这就给我们
的开发和测试带来了很大困扰,因为
往往各个厂商的对标识符的命名方式
都很不一样,使得开发者很不容易获
得到所需要的依赖对象。
ServiceLocatorServiceLocator 模模
式式
Service
Locator
容器
Service
Locator
容器
“ ???”
“ ???”
get
get
Get what?Get what?
- 11 -- 11 -
1. 入门:依赖注入——“依赖注入”登场
于是诸多优秀的 IT 工程师开始想出了更加轻量便利、更加具有可测试性和可
维护性的设计模式—— IoC 模式。 IoC ,即 Inversion of Control 的缩写,
中文里被称作“控制反转”。至于为什么会有这么一个看似古怪的名字,我们
稍后会做解释。 2004 年著名软件工程学者和工程师 Martin Fowler 在其论
文《 Inversion of Control Containers and the Dependency Injection
pattern 》中将 IoC 更名为 Dependency Injection ,从此依赖注入模式成
为实现组件间松耦合的主流设计思想。
- 12 -- 12 -
1. 入门:依赖注入——“依赖注入”登场
依赖注入的原理依赖注入的原理
那么应用了依赖注入,我们的开发模式将会发生什么样的变化呢?我们还是
先来看看刚才的例子。下面的代码就是应用了依赖注入之后的储户
Depositor 类:
public class Depositor {
@In private Bank bank; // @In 标识此处是一个注入点
@In private DepositBook depositBook; // @In 标识此处是一个注入点
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook, amount);
}
}
某储户类某储户类
@In!@In!
@In!@In!
银行类
存折类
可以看到,这个 Depositor
类几乎就是最开始我们定义的
那个最简单的、没有任何初始
化依赖的过程的类!唯一不同
的是在定义依赖成员变量的时
候,多加了一个 JavaSE5 标
准的注解—— @In 。
- 13 -- 13 -
1. 入门:依赖注入——“依赖注入”登场
依赖注入的原理依赖注入的原理
某储户类某储户类
@In!@In!
@In!@In!
注入点!注入点!
依赖注入容器依赖注入容器
“depositBook”
“bank”
这里被 @In 标注的地方被称作“注入点”( injection point ),声明注入点
起到的作用就是,告诉依赖注入容器“在这里请把依赖对象给我!”,也就是
@In 向依赖注入容器发出了一个请求,之后容器会利用 Java 的反射机制将其
保管的对象 set 到注入点,即完成了一次依赖注入。
如果 @In 注入点所请求的依赖对象自身还有依赖,那么依赖注入容器同样
会以同样的方式先去设定好这个“依赖的依赖”,也就是说,依赖注入容器会帮
助开发者构建一套完整的依赖关系图,最后将一个完全初始化好的依赖对象提
供给依赖者。
Java 反射
Java 反射
依赖注入容器非常地区别于
ServiceLocator 容器,它可以:
使标识符及依赖的注册过程轻量可控
。
对受管理的依赖对象进行生命周期管
理。
对受管理的依赖对象进行行为增强
( AOP )。
- 14 -- 14 -
1. 入门:依赖注入——“依赖注入”登场
““ 好莱坞原则”好莱坞原则”
如果说传统的组件间耦合方式,例如 new 、工厂模式等,是一种由开发者主动去
构建依赖对象的话,那么依赖注入模式则是其反向的,即被动地等待别人做好一个依赖
对象提供给我。
在美国好莱坞众多电影工厂在寻找演员的时候通常奉行着这么一个原则:不要找我
,需要的时候我去找你(“ Don’t call us; we’ll call you!” ),把相同的思路运用到我
们的软件工程里通常也被称作“好莱坞原则”( Hollywood Principle ),即告诉开发者
不要主动去构建依赖对象,而是在需要的时候由依赖注入容器把对象提供过来。
这种思路与以前的 new 、工厂创建等方式是正好相反的思想,因此在最开始这样
的思想会被称为“控制反转( IoC )”。从这我们也可以看到,依赖注入的核心思想就是
:由容器提供给我们需要的依赖对象。
某储户类某储户类
@In!@In!
@In!@In!
注入点!注入点!
依赖注入容器依赖注入容器
“depositBook”
“bank”
Java 反射
Java 反射
- 15 -- 15 -
1. 入门:依赖注入——“依赖注入”登场
依赖注入框架简介依赖注入框架简介
Apach
e
Avalo
n
~2003 2003 2005 2007 2010
Pico
Container 、
Nano
Container 、
Apache
Hivemind 、
SmartyPants
-IoC
最早的 DI 框架
随后涌现出了
诸多 DI 框架
Spring
Framework
Spring 的出现标
志着 full-stack
框架开始占据主流
至今仍是使用
最广泛的框架
Jboss Seam
集成了 JPA 和 JSF 的
轻量依赖注入框架,
并提供了诸多强大
的开发功能
Google
Guice
基于 Java 注解和
Java EDSL 的类型
安全的依赖注入
框架
JavaEE6 :
CDI 标准
JSR299 和 JSR330 使得
依赖注入模式统一于
JavaEE 标准
- 16 -- 16 -
2. 依赖注入的应用模式——依赖注入容器及其管理对象
在依赖注入容器中声明管理对象在依赖注入容器中声明管理对象
想要容器在被需要时能够将对象提供给注入点处,那么首先在容器里要有这个对
象(严格来说是有这个对象的 META-DATA ,即基础数据,而非对象本身),这就
需要我们首先向容器中“注册”这样的对象进去,或者也可以称为在容器中“声明一个组
件”。不同的依赖注入框架有不同的声明方法,例如 Spring 中可以利用注解或者
XML 方式向容器声明组件。
Spring 容器Spring 容器
@Component // 在 Spring 容器中声明一个“工商银行”的组件
public class BankICBC implements Bank { // …… }
<!– 在 Spring 容器中声明一个“工商银行存折”的组件 -->
<bean id=" depositBookICBC " class="tutorial.di.ch01.DepositBookICBC "/>
某储户类某储户类
@In!
注入点!注入点!
首先向容器声明
这两个组件
之后容器就可以将
声明好的对象提供
给注入点了
- 17 -- 17 -
2. 依赖注入的应用模式——依赖注入的方式
依赖注入的方式依赖注入的方式
在容器中声明好依赖对象之后,这个对象就会在那里“等待”被请求了。那么在什么
地方会被请求呢?这就是前面提到的“注入点”了,即在被声明为“注入点”的地方,
容器会被通知把其管理的依赖对象提供过来。
某储户类某储户类
依赖注入容器依赖注入容器
@In!
在哪里可以声明
这些注入点呢?
一般地,注入点可以被声明在构造函数处、 Setter 方法处、成员变量处。下面我们
详细介绍这几种声明注入点的方法。注意有些依赖注入框架,例如 Guice ,可以将
注入点声明在方法参数上,在此就不再介绍了。
- 18 -- 18 -
2. 依赖注入的应用模式——依赖注入的方式
ConstructorConstructor 注入注入
利用 Constructor 注入,即在依赖者类的构造函数上声明注入点,而要被注入
的对象则是构造函数的参数。
某储户类某储户类
依赖注入容器依赖注入容器
ConstructorConstructor
@In!
告诉容器利用
构造函数 set 依赖
Spring 框架下的代码例见下一页。。
。
- 19 -- 19 -
2. 依赖注入的应用模式——依赖注入的方式
ConstructorConstructor 注入注入
@Component
public class Depositor {
@Autowired // Constructor 注入点
public Depositor(Bank bank, DepositBook depositBook) {
this.bank = bank;
this.depositBook = depositBook;
}
private Bank bank;
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook, amount);
}
}
@Component // 声明此依赖为 Spring 组件
public class BankICBC implements Bank { // …… }
@Component // 声明此依赖为 Spring 组件
public class DepositBookICBC implements DepositBook { // …… }
- 20 -- 20 -
2. 依赖注入的应用模式——依赖注入的方式
SetterSetter 注入注入
在依赖者类的 Setter 方法上声明注入点,依赖注入框架利用反射机制调用
Setter 方法来完成依赖注入。
某储户类某储户类
依赖注入容器依赖注入容器
SetterSetter
@In!
告诉容器利用
Setter 方法 set 依赖
Seam 框架下的代码例见下一页。。
。
- 21 -- 21 -
2. 依赖注入的应用模式——依赖注入的方式
SetterSetter 注入注入
@Name("bank")
public class BankICBC implements Bank { // …… }
@Name("depositBook")
public class DepositBookICBC implements DepositBook { // …… }
@Name("depositor")
public class Depositor {
private Bank bank;
private DepositBook depositBook;
@In // bank 的 Setter 注入点
public void setBank(Bank bank) {
this.bank = bank;
}
@In // depositBook 的 Setter 注入点
public void setDepositBook(DepositBook depositBook) {
this.depositBook = depositBook;
}
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook, amount);
}
}
- 22 -- 22 -
2. 依赖注入的应用模式——依赖注入的方式
成员变量注入成员变量注入
成员变量注入是指依赖注入框架利用反射机制,直接将依赖者类的成员变量 set
为容器管理的依赖对象。
某储户类某储户类
依赖注入容器依赖注入容器
@In!
告诉容器直接
向成员变量
set 依赖
Seam 框架下的代码例见下一页。。
。
@In!
- 23 -- 23 -
2. 依赖注入的应用模式——依赖注入的方式
成员变量注入成员变量注入
@Name("depositor")
public class Depositor {
@In // bank 的成员变量注入点
private Bank bank;
@In // depositBook 的成员变量注入点
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook, amount);
}
}
@Name("bank")
public class BankICBC implements Bank { // …… }
@Name("depositBook")
public class DepositBookICBC implements DepositBook { // …… }
- 24 -- 24 -
2. 依赖注入的应用模式——依赖注入的方式
注入模式的选择注入模式的选择
再如如果一个类所拥有的依赖数量过多、而开发者又不想构造方法的参数
成为“过长参数列表”(这是一种影响代码的可读性和可维护性的反模
式, Martin Fowler 于 1999 年在其《 Refactoring: Improving the Design
of Existing Code 》一书中提出),则可考虑 Setter 注入或者成员变量注入。
此外,如果遇到了类似“循环依赖”的情况,例如宿主和寄生动物这两个对象就
属于相互依存的“循环依赖”模式,这样的情况下,我们就可能需要 Constructor
和 Setter 两种混合的模式才能解决。总而言之,选择依赖注入模式是一个很大
的话题,我们在此就不再深入讨论了。
对于该怎么使用前面介绍的这几种注入模式,究竟在什么情况下该使用哪
种,这是一个讨论非常广泛的话题。这里只举一个简单的例子来说明,比如要
设计一个不可变( final )依赖的类,则必须使用 Constructor 注入方式。
@Component
public class Depositor {
@Autowired // Constructor 注入点
public Depositor(Bank bank, DepositBook depositBook) {
this.bank = bank;
this.depositBook = depositBook;
}
private final Bank bank; // 不可变的依赖
private final DepositBook depositBook; // 不可变的依赖
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook, amount);
}
}
- 25 -- 25 -
2. 依赖注入的应用模式——依赖注入对象的请求模式
依赖注入对象的请求模式依赖注入对象的请求模式
依赖注入容器中已经有了声明的依赖对象,也知道了向哪个注入点提供这
个对象,现在的问题就是,究竟把容器中的哪个对象提供出去呢?
某储户类某储户类
依赖注入容器依赖注入容器
@In!
要把哪个依赖
对象提供出去?
要把哪个依赖
对象提供出去?
这就需要在依赖对象与依赖对象的请求者之间有一个“统一的语言”,即请
求的标识符。依赖对象在容器中以这个标识符被声明,而在注入点上也来请求
这个标识符,这样两方就可以契合上了。一般地,这个标识符可以用字符串、
全类名( FQCN )、两者混合这几种方式,下面详细介绍。
- 26 -- 26 -
2. 依赖注入的应用模式——依赖注入对象的请求模式
字符串请求模式字符串请求模式
某储户类某储户类
@In!@In!
@In!@In!
依赖注入容器依赖注入容器
“depositBook”
“bank”
“bank”
“depositBook”
顾名思义,字符串请求模式即依赖注入
框架将一个依赖绑定到指定的字符串上
,而后将其装入依赖注入容器。依赖者
通过这个字符串,向容器请求所需要的
依赖。 Seam 和 Spring 都是典型的基
于字符串请求模式的框架。
@Name("bank") // 将 BankICBC 依赖绑定到 "bank"
public class BankICBC implements Bank { // …… }
@Name("depositBook") // 将 DepositBookICBC 依赖绑定到 "depositBook"
public class DepositBookICBC implements DepositBook { // …… }
@In("bank") // 请求标识符为 "bank" 的依赖对象
private Bank bank;
@In("depositBook") // 请求标识符为“ depositBook ” 的依赖对象
private DepositBook depositBook;
- 27 -- 27 -
2. 依赖注入的应用模式——依赖注入对象的请求模式
字符串请求模式字符串请求模式
某储户类某储户类
@In!@In!
@In!@In!
依赖注入容器依赖注入容器
“depositBook”
“bak”
“bak”
“depositBook”
尽管 Seam 和 Spring 都采用了纯字符
串的标识符定义的模式,但这种模式下
有一个很大的弱点:违反了“类型安全
( type-safe )”原则,也就是说可能
会误将一个本不是依赖者类所希望的依
赖对象注入进来。
@In("bak") // 错误地请求了标识符为 "bak" 的依赖对象
private Bank bank;
@Name("bak") // 其他开发者恰好向容器中注册了一个 "bak" 依赖
public class Bak { // …… }
- 28 -- 28 -
2. 依赖注入的应用模式——依赖注入对象的请求模式
FQCNFQCN 请求模式请求模式
某储户类某储户类
@In!@In!
依赖注入容器依赖注入容器
Bank.class
为了弥补纯字符串请求模式中的类型安全问
题,全类名( FQCN )请求模式就应运而
生了。其思想便是,在向容器请求依赖对象
的时候,不是通过字符串的标识符、而是通
过被请求的依赖的全类名来定位依赖。这样
如果开发者误将全类名标识符写错的话,在
编译时立即会提醒“类不存在”。并且,如果
使用 Eclipse 等 IDE 开发工具的话,用其
提供的自动完整代码的功能就会轻松地将依
赖的全类名标识符定义到代码中。
// Spring 的全类名注入的 API
BeanFactory injector = new FileSystemApplicationContext("depositConfiguration.xml")
this.bank = (Bank) injector.getBean(Bank.class);
// Seam 的全类名注入的 API
this.bank = (Bank) Component.getInstance(BankICBC.class);
// Guice 的全类名注入的 API
Injector injector = Guice.createInjector();
this.bank = (Bank) injector.getInstance(Bank.class);
Bank.class
- 29 -- 29 -
2. 依赖注入的应用模式——依赖注入对象的请求模式
FQCNFQCN 请求模式请求模式
某储户类某储户类
@In!@In!
依赖注入容器依赖注入容器
Bank.class
但是如果容器中有多个 Bank 类的实现类,
比如还有一个 BankCMB 的实现类,此时依
赖注入容器就不能正确识别究竟应把哪一个
实现的依赖对象提供给依赖者了,这就是全
类名请求模式的一个缺陷,即其会将依赖对
象的接口限定为只有一个实现。
Bank.class
Bank.class
- 30 -- 30 -
2. 依赖注入的应用模式——依赖注入对象的请求模式
混合请求模式混合请求模式
某储户类某储户类
@In!@In!
依赖注入容器依赖注入容器
“icbc”
Bank.class
FQCN (全类名)请求模式会带来依赖定义
的柔软性较差的问题,因此字符串和全类名
混合的模式又应运而生了。顾名思义,这是
一种综合使用字符串和 FQCN 的模式。
“cmb”
Bank.class
“icbc”
Bank.class
@In!@In!
“cmb”
Bank.class
// Spring 的字符串 + 全类名注入的 API
BeanFactory injector = new FileSystemApplicationContext("depositConfiguration.xml")
this.bank = (Bank) injector.getBean(“icbc", Bank.class);
// 请求名为 "bank" 且类为 Bank 的依赖
this.bank2 = (Bank) injector.getBean(“cmb", Bank.class);
// 请求名为“ cmb" 且类为 Bank 的依赖
- 31 -- 31 -
3. 依赖注入对象的 Scope 及其生命周期——依赖注入对象的 Scope
无状态无状态 ScopeScope
某储户类 A某储户类 A
@In!@In!
依赖注入容器依赖注入容器
“depositBook”
Inject
某储户类 B某储户类 B
@In!@In!
Inject
new
new
无状态 Scope 是最简单、生
存期间最短的一种 Scope ,
我们所有通过 new 出来的非
静态对象都属于这种
Scope ,它随着使用这个对
象的对象(可能是依赖者类,
也可能是依赖者类里的一个方
法)的消亡而消亡。例如在我
们的例子中,不同的储户需要
不同的存折,因此可以将存折
依赖理解为一个无状态
Scope 。当然依赖注入容器
创建这个依赖的时候远不是
new 那么简单,例如 Spring
框架为依赖者创建一个依赖的
时候就要经过大大小小 9 个阶
段。
“depositBook”
“depositBook”
- 32 -- 32 -
3. 依赖注入对象的 Scope 及其生命周期——依赖注入对象的 Scope
单例单例 ScopeScope
某储户类 A某储户类 A
@In!@In!
依赖注入容器依赖注入容器
“bank”
Inject
某储户类 B某储户类 B
@In!@In!
Inject
“bank”
“bank”
create
单例 Scope ,即
Singleton ,是指以依赖注入
容器( Injector )为单位存在
的唯一的实例。例如在我们的
例子中,假设储户 A 和储户 B
都只能去附近的一所银行,那
么这所银行对于储户 A 和储户
B 来讲,应给都是同一个实例。
在依赖注入容器出现之前,我
们常用静态的模式使一个实例
长久存活在 JVM 的 OLD 区域
中,但是这种方式会造成内存
泄漏和不易管理等问题,因此
在依赖注入设计模式下是不推
荐用静态实现单例模式的。
- 33 -- 33 -
3. 依赖注入对象的 Scope 及其生命周期——依赖注入对象的 Scope
WebWeb 开发中常用开发中常用 ScopeScope
简介简介第一个比较常用的就是 Application 级 Scope ,通常我们会将一些贯穿整个 EAR
都会用到的常量定义、通用的服务组件、类似 DBConnection 连接创建等比较消耗
资源的组件等等放到这个 Scope 中去。 Application 级 Scope 是与应用程序的
ServletContext 共存亡的,因此可以理解为所部署应用程序的 ServletContext 中
的“单例 Scope” 。
第二个非常常用的是 Session 级的 Scope ,通常我们会将诸如 User 的认证信息
等对象储存在这个区域,它是与客户端(浏览器等)请求所创建的 Session 共存亡
的。
如果把所有数据都存放在 Session 区域,容易增大 Server 端服务的负荷,并且
Session 区域是线程不安全的,因为所有客户端的请求都可以访问到 Session 的数
据并修改。因此开发者常常会需要一个以单次发送的请求即 HttpRequest 为单位的
Scope 。
- 34 -- 34 -
3. 依赖注入对象的 Scope 及其生命周期——生命周期管理
生命周期管理生命周期管理
某储户类某储户类
@In!@In!
依赖注入容器依赖注入容器
“bank”“bank”
安排营业员
聘请保安
安排营业员
聘请保安
关闭营业点关闭营业点
@Name("bank")
@Scope(ScopeType.APPLICATION)
public class BankICBC implements Bank {
@Create // 标注为初始化的生命周期方法
public void beforeOpenBank() {
assignEmployee();
hireSecurityGuard();
// ……
}
@Destroy // 标注为结束的生命周期方法
public void closeBank {
closeBank();
}
}
所谓生命周期管理,就是一
个对象在它所属的 Scope
中从被容器创建开始、到被
提供给依赖者、再到最后的
消亡这一整个过程中,依赖
注入框架提供了一系列的回
调方法的接口,使框架自身
以及开发者都可以利用这些
接口对各个生存时点的依赖
对象做一些操作和管理等。
假设对于银行这个依赖对象
,在其开业的时候,即开始
进入 Application 级 Scope
的生存期间的时点,一定是
需要很多诸如安排营业员、
聘请保安等初始化的动作的
,只有这些初始化动作完毕
,才能做为一个完整的对象
提供给依赖者。
- 35 -- 35 -
4. 依赖注入对象的行为增强( AOP )——对依赖对象进行行为增强
所谓 AOP ,就是 Aspect Oriented Programming (面向方面的编程),核心思想
是把一个“方面”独立出来,从而实现组件间的松耦合。在我们的银行依赖中,假设有
个需求,即在每一笔取款业务的前后都要输出日志信息。因此我们需要这样修改我们
的代码:
public class BankICBC implements Bank {
private static Logger logger = Logger.getLogger(Bank.class.getName());
@Override
public Cash withDraw(DepositBook depositBook, BigDecimal amount) {
logger.log(Level.INFO, "withdraws starting...");
// ……
logger.log(Level.INFO, "withdraws ended…");
}
}
我们为了实现这个需求需要在其中加入定义 logger 、输出日志等代码。而这些代码
,就是我们所说的独立的“方面”。为什么这么说呢?因为日志的输出工作,是与开发
者真正要做的取款业务完全没有关系的。 AOP 的出现,就可以将这个独立的日志处
理的“方面”从实际的依赖对象里分离开来,而在依赖对象在运行的时候,这个“方面”
又可以加到依赖对象身上得以运行,也就是我们所说的依赖对象的行为被增强了,因
为它的行为不但实现了它本身的逻辑,而且也实现了被增强的其它“方面”的逻辑。而
在 AOP 体系内,用以将其它“方面”的逻辑增强到某对象上的组件往往被称作
Interceptor (拦截器)。
- 36 -- 36 -
4. 依赖注入对象的行为增强( AOP )——对依赖对象进行行为增强
例如在 Spring 框架中,我们可以这样分离出独立的“方面”:
// 此处将日志功能作为“方面”独立出来
public class LogInterceptor {
private static Logger logger = Logger.getLogger(LogInterceptor.class.getName());
public Object log(ProceedingJoinPoint call) throws Throwable {
logger.log(Level.INFO, "withdraws starting...");
try {
return call.proceed();
} finally {
logger.log(Level.INFO, "withdraws ended...");
}
}
}
<beans>
<!-- …… -->
<!-- 这里将“方面”类声明为 Spring 管理的依赖 -->
<bean id="logger" class="tutorial.di.ch01.LogInterceptor"/>
<!-- 这里将所声明的“方面”增强到需要的地方 -->
<aop:config>
<aop:aspect ref="logger">
<aop:pointcut id="pointcuts.withdrawMethod"
expression="execution(* tutorial.di.ch01.BankICBC.withDraw(..))" />
<aop:around pointcut-ref="pointcuts.withdrawMethod " method="log"/>
</aop:aspect>
</aop:config>
</beans>
- 37 -- 37 -
4. 依赖注入对象的行为增强( AOP )——对依赖对象进行行为增强
依赖注入框架中对 AOP 的实现是靠 Java 的动态代理机制实现的,通常使用 JDK 的
代理类,或是借助 Javassist 及 CGLIB 等工具。
某储户类某储户类
@In!@In!
依赖注入容器依赖注入容器
“bank”
“bank”
日志功能日志功能
JavaProxyJavaProxy
- 38 -- 38 -
4. 依赖注入对象的行为增强( AOP )—— AOP 应用举例
首先就是关于前面介绍过的日志输出类的功能,当然前面的例子非常简单,实际上
要输出的日志信息中往往有很多的可变参数,这时就需要从被拦截对象的上下文中
取出相应的信息进行行为的增强。
最常用的 AOP 应用就是关于 DB 事务的管理了。业务处理成功则向 DB 提交事务
,反之则回滚事务——这是每一个开发者都会写过的代码。但是实际上这种事务的
处理是完全独立的“方面”,不依赖于任何逻辑的实现,即任何有关 DB 的逻辑处理
都是要提交或者回滚的,因此关于 DB 事务管理的功能往往由 AOP 框架去管
理。 Spring 、 Seam 、 Guice 包括 EJB 应用中的容器管理事务的功能,实质上都
是 AOP 的应用。
另外关于安全处理的功能,往往是和开发者所开发的逻辑无关的“方面”,因为所有
需要判定当前的用户是否有权限执行某段逻辑这样的需求,一般都是同样的处理方
式——用户有权限则继续处理,用户无权限则抛出异常。这种情况下为了避免开发
者写出大量的 if 语句,用框架提供的 AOP 的方式去做安全的判定,应是最佳解决
方案。
- 39 -- 39 -
完结
有关依赖注入设计模式的话题,其实是一个非常广泛的话题,不同的框架对其有着共
同或不同的理念,所产生的最佳实践也有相似的一面和各不相同的一面,我们在这里
由于篇幅有限,只能做以上的比较粗浅的介绍,大家如有兴趣可以继续通过互联网和
各种书籍深入研究这种到目前为止非常成功的设计模式。
(完,谢谢大家)

Mais conteúdo relacionado

Semelhante a Di&aop

[xKungFoo2012]Web Service Hack
[xKungFoo2012]Web Service Hack[xKungFoo2012]Web Service Hack
[xKungFoo2012]Web Service Hackpnig0s pnig0s
 
Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程yiditushe
 
Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程appollo0312
 
Cloud Foundry Introduction
Cloud Foundry IntroductionCloud Foundry Introduction
Cloud Foundry Introduction家弘 周
 
中远公司 Java培训资料
中远公司  Java培训资料中远公司  Java培训资料
中远公司 Java培训资料yiditushe
 
Spring 2.x 中文
Spring 2.x 中文Spring 2.x 中文
Spring 2.x 中文Guo Albert
 
Pure mvc教程
Pure mvc教程Pure mvc教程
Pure mvc教程liuweiwei
 
API Survey #2 - Firebase realtime database
API Survey #2 - Firebase realtime databaseAPI Survey #2 - Firebase realtime database
API Survey #2 - Firebase realtime databaseSzuping Wang
 
Alibaba Service Framework Practice
Alibaba Service Framework  PracticeAlibaba Service Framework  Practice
Alibaba Service Framework PracticeShawn Qian
 
[DCTPE2010] 如何開發 CCK 欄位模組
[DCTPE2010] 如何開發 CCK 欄位模組[DCTPE2010] 如何開發 CCK 欄位模組
[DCTPE2010] 如何開發 CCK 欄位模組Drupal Taiwan
 
移动端跨平台技术原理
移动端跨平台技术原理移动端跨平台技术原理
移动端跨平台技术原理gorillazf
 
Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫
Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫
Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫Justin Lin
 
Single-Page Application Design Principles 101
Single-Page Application Design Principles 101Single-Page Application Design Principles 101
Single-Page Application Design Principles 101Jollen Chen
 
Android应用开发 - 沈大海
Android应用开发 - 沈大海Android应用开发 - 沈大海
Android应用开发 - 沈大海Shaoning Pan
 
基于Ivy ant的java构建初探
基于Ivy ant的java构建初探基于Ivy ant的java构建初探
基于Ivy ant的java构建初探Anson Yang
 

Semelhante a Di&aop (20)

[xKungFoo2012]Web Service Hack
[xKungFoo2012]Web Service Hack[xKungFoo2012]Web Service Hack
[xKungFoo2012]Web Service Hack
 
Exodus2 大局观
Exodus2 大局观Exodus2 大局观
Exodus2 大局观
 
Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程
 
Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程Struts+Spring+Hibernate整合教程
Struts+Spring+Hibernate整合教程
 
Cloud Foundry Introduction
Cloud Foundry IntroductionCloud Foundry Introduction
Cloud Foundry Introduction
 
中远公司 Java培训资料
中远公司  Java培训资料中远公司  Java培训资料
中远公司 Java培训资料
 
Autofac.pptx
Autofac.pptxAutofac.pptx
Autofac.pptx
 
Entities in DCPS (DDS)
Entities in DCPS (DDS)Entities in DCPS (DDS)
Entities in DCPS (DDS)
 
Spring 2.x 中文
Spring 2.x 中文Spring 2.x 中文
Spring 2.x 中文
 
Pure mvc教程
Pure mvc教程Pure mvc教程
Pure mvc教程
 
API Survey #2 - Firebase realtime database
API Survey #2 - Firebase realtime databaseAPI Survey #2 - Firebase realtime database
API Survey #2 - Firebase realtime database
 
Alibaba Service Framework Practice
Alibaba Service Framework  PracticeAlibaba Service Framework  Practice
Alibaba Service Framework Practice
 
[DCTPE2010] 如何開發 CCK 欄位模組
[DCTPE2010] 如何開發 CCK 欄位模組[DCTPE2010] 如何開發 CCK 欄位模組
[DCTPE2010] 如何開發 CCK 欄位模組
 
移动端跨平台技术原理
移动端跨平台技术原理移动端跨平台技术原理
移动端跨平台技术原理
 
I os 16
I os 16I os 16
I os 16
 
Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫
Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫
Servlet & JSP 教學手冊第二版 - 第 9 章:整合資料庫
 
CRUD 綜合運用
CRUD 綜合運用CRUD 綜合運用
CRUD 綜合運用
 
Single-Page Application Design Principles 101
Single-Page Application Design Principles 101Single-Page Application Design Principles 101
Single-Page Application Design Principles 101
 
Android应用开发 - 沈大海
Android应用开发 - 沈大海Android应用开发 - 沈大海
Android应用开发 - 沈大海
 
基于Ivy ant的java构建初探
基于Ivy ant的java构建初探基于Ivy ant的java构建初探
基于Ivy ant的java构建初探
 

Di&aop

  • 1. - 1 -- 1 - 目次 1. 入门:依赖注入 2. 依赖注入的应用模式 3. 依赖注入对象的 Scope 及其生命周期 4. 依赖注入对象的行为增强( AOP )
  • 2. - 2 -- 2 - 1. 入门:依赖注入——“依赖”的概念 从现实世界的观点来看,“依赖”即某 个实体对象为了完成某项功能,必须 要依托另外一些实体对象,那么这些 被依托的实体对象即被称为“依赖” ( Dependency ),而依托其它“依 赖”从而完成某项功能的对象,往往 被称作“依赖者”( Dependent )。 某储户类某储户类 public class Depositor { private Bank bank; // 银行是其依赖 private DepositBook depositBook; // 存折是其依赖 public Cash withDraw(BigDecimal amount) { // 依托上面的依赖完成取款业务 return bank.withDraw(depositBook, amount); } } 银行类银行类 存折类存折类
  • 3. - 3 -- 3 - 1. 入门:依赖注入——传统的组件耦合方式 这种手工 new 的组装方式,虽然是 Java 平台程序开发中最基本的方式,但是其会 带给我们在开发、维护以及测试等方面带 来极大的困扰。 构建依赖对象困难,并且往往依赖对象 与开发者正在开发的逻辑无关。 维护不便。只要有构造方法的改动就会 带来所有调用依赖的地方发生改变并且需 要回归测试。 单元测试时很难将 Mock 对象替入实际 依赖。 某储户类某储户类 银行类 存折类 手工组装模式手工组装模式 new new public class Depositor { private Bank bank; private DepositBook depositBook; public Cash withDraw(BigDecimal amount) { bank = new BankICBC("Tianjin", 100); // 初始化依赖 depositBook = new DepositBookICBC("62202", "current", "CNY"); // 初始化依赖 return bank.withDraw(depositBook, amount); } }
  • 4. - 4 -- 4 - 1. 入门:依赖注入——传统的组件耦合方式 用工厂创建依赖对象,可以将创建对象的 过程隐蔽, Depositor 类的开发者利用这 种透明创建对象的方式则可避免手工构建 复杂依赖关系图所带来的诸多不便。并且 ,当依赖的构造方法接口发生变更时,影 响到的也只是工厂类,而不会影响到调用 工厂类的依赖者类本身,从而降低了系统 的维护成本。 某储户类某储户类 工厂模式工厂模式 工厂类工厂类 工厂类工厂类 代码例见下一页。。 。 use use
  • 5. - 5 -- 5 - 1. 入门:依赖注入——传统的组件耦合方式 public class Depositor { private Bank bank; private DepositBook depositBook; public Cash withDraw(BigDecimal amount) { bank = new BankFactory().createBankICBC(); // 工厂创建依赖 depositBook = new DepositBookFactory().createDepositICBC();// 工厂创建依赖 return bank.withDraw(depositBook, amount); } } public class BankFactory { public Bank createBankICBC() { return new BankICBC("Tianjin", 100); } } public class DepositBookFactory { public DepositBook createDepositICBC() { return new DepositBookICBC("62202", "current", "CNY"); } } 工厂模式工厂模式
  • 6. - 6 -- 6 - 1. 入门:依赖注入——传统的组件耦合方式 工厂模式工厂模式 但是随着软件工程的不断复杂化和规模化,工厂模式同样的遇到了很多问题 而被开发人员诟病。围绕着这个话题的讨论有很多,我们不再一一列举,只 举一个比较常见的问题来说明一下。假设我们的银行不只有工商银行,还有 招商银行、中国银行等等,这样就需要我们维护一个越来越庞大的工厂类。 之前的 Bank 工厂就要逐渐这样增加 create 方法: public class BankFactory { public Bank createBankICBC() { return new BankICBC("Tianjin", 100); } public Bank createBankCMB() { // …… }; public Bank createBankBOC() { // …… }; // …… }
  • 7. - 7 -- 7 - 1. 入门:依赖注入——传统的组件耦合方式 工厂模式工厂模式 这样工厂类自身的维护成本逐渐增大,就成为了工厂模式的一个很大的障碍。 因此很多开发者想办法用参数化的形式,来减少工厂类的维护量。 public class BankFactory { public Bank createBank(BankVender bankVendor) { // 参数化的工厂方法 switch (bankVendor) { case ICBC: return createBankICBC(); case CMB: return createBankCMB(); default: return createBankICBC(); } } // …… } 但是这样一来又会回到手工 new 对象的一个问题,即如果工厂方法的参数发 生了变化,可能会引起全部调用到该工厂方法的代码都需要修改及做相应的 回归测试。
  • 8. - 8 -- 8 - Service Locator 容器 Service Locator 容器 1. 入门:依赖注入——传统的组件耦合方式 ServiceLocatorServiceLocator 模模 式式 某储户类某储户类 get get public class Depositor { private Bank bank; private DepositBook depositBook; public Cash withDraw(BigDecimal amount) { bank = (Bank) new ServiceLocator().get("BankICBC"); depositBook = (DepositBook) new ServiceLocator().get("DepositBookICBC"); // 从 ServiceLocator 容器中获取依赖对象 return bank.withDraw(depositBook, amount); } } “BankICBC” “DepositBookICBC” ServiceLocator ,即服务定位器,实质 上是一种改进版的工厂模式。这种模式 的核心思想是,将构建依赖的接口彻底 与依赖者分离,并将此依赖作为“服务” 绑定到一个标识符(通常是一个字符 串),而后依赖者则可通过这个标识符 获取被绑定的依赖。
  • 9. - 9 -- 9 - 1. 入门:依赖注入——传统的组件耦合方式 这样一个能够管理这些依赖(即“服务”)的、类似于工厂类的一个单独 的类,也常常被称作“ ServiceLocator 容器”,这个容器负责各种依赖的注 册、标识符绑定、提供依赖对象等工作。但是实现这样一个容器对于普通开 发者来说代价就太大了。 因此, JCP 组织联合各 IT 供应商制订了统一的 ServiceLocator 容器 标准,开发者可以只管开发自己的依赖类,然后 IT 供应商提供的 ServiceLocator 容器实现(即各种应用服务器级中间件)通过统一的命名 规则将某个标识符绑定到这些依赖类并注册到容器中,等待开发者的请求命 令。 而 ServiceLocator 模式作为业界标准统一化后,一个最典型的应用便 诞生了: JNDI 。例如获取 RemoteEJB 的时候,我们可以执行这样的 JNDI 查找: ServiceLocatorServiceLocator 模模 式式 RemoteBean rb = (RemoteBean) initialContext.lookup("java:comp/env/TEST/RemoteBeanImpl");
  • 10. - 10 -- 10 - 1. 入门:依赖注入——传统的组件耦合方式 但是 ServiceLocator 模式最大的弱 点就是,依赖对象的获取强依赖于 ServiceLocator 容器,除非开发者 愿意花费大量成本自己实现 ServiceLocator 容器,否则就必须 要中间件提供商的支持,这就给我们 的开发和测试带来了很大困扰,因为 往往各个厂商的对标识符的命名方式 都很不一样,使得开发者很不容易获 得到所需要的依赖对象。 ServiceLocatorServiceLocator 模模 式式 Service Locator 容器 Service Locator 容器 “ ???” “ ???” get get Get what?Get what?
  • 11. - 11 -- 11 - 1. 入门:依赖注入——“依赖注入”登场 于是诸多优秀的 IT 工程师开始想出了更加轻量便利、更加具有可测试性和可 维护性的设计模式—— IoC 模式。 IoC ,即 Inversion of Control 的缩写, 中文里被称作“控制反转”。至于为什么会有这么一个看似古怪的名字,我们 稍后会做解释。 2004 年著名软件工程学者和工程师 Martin Fowler 在其论 文《 Inversion of Control Containers and the Dependency Injection pattern 》中将 IoC 更名为 Dependency Injection ,从此依赖注入模式成 为实现组件间松耦合的主流设计思想。
  • 12. - 12 -- 12 - 1. 入门:依赖注入——“依赖注入”登场 依赖注入的原理依赖注入的原理 那么应用了依赖注入,我们的开发模式将会发生什么样的变化呢?我们还是 先来看看刚才的例子。下面的代码就是应用了依赖注入之后的储户 Depositor 类: public class Depositor { @In private Bank bank; // @In 标识此处是一个注入点 @In private DepositBook depositBook; // @In 标识此处是一个注入点 public Cash withDraw(BigDecimal amount) { return bank.withDraw(depositBook, amount); } } 某储户类某储户类 @In!@In! @In!@In! 银行类 存折类 可以看到,这个 Depositor 类几乎就是最开始我们定义的 那个最简单的、没有任何初始 化依赖的过程的类!唯一不同 的是在定义依赖成员变量的时 候,多加了一个 JavaSE5 标 准的注解—— @In 。
  • 13. - 13 -- 13 - 1. 入门:依赖注入——“依赖注入”登场 依赖注入的原理依赖注入的原理 某储户类某储户类 @In!@In! @In!@In! 注入点!注入点! 依赖注入容器依赖注入容器 “depositBook” “bank” 这里被 @In 标注的地方被称作“注入点”( injection point ),声明注入点 起到的作用就是,告诉依赖注入容器“在这里请把依赖对象给我!”,也就是 @In 向依赖注入容器发出了一个请求,之后容器会利用 Java 的反射机制将其 保管的对象 set 到注入点,即完成了一次依赖注入。 如果 @In 注入点所请求的依赖对象自身还有依赖,那么依赖注入容器同样 会以同样的方式先去设定好这个“依赖的依赖”,也就是说,依赖注入容器会帮 助开发者构建一套完整的依赖关系图,最后将一个完全初始化好的依赖对象提 供给依赖者。 Java 反射 Java 反射 依赖注入容器非常地区别于 ServiceLocator 容器,它可以: 使标识符及依赖的注册过程轻量可控 。 对受管理的依赖对象进行生命周期管 理。 对受管理的依赖对象进行行为增强 ( AOP )。
  • 14. - 14 -- 14 - 1. 入门:依赖注入——“依赖注入”登场 ““ 好莱坞原则”好莱坞原则” 如果说传统的组件间耦合方式,例如 new 、工厂模式等,是一种由开发者主动去 构建依赖对象的话,那么依赖注入模式则是其反向的,即被动地等待别人做好一个依赖 对象提供给我。 在美国好莱坞众多电影工厂在寻找演员的时候通常奉行着这么一个原则:不要找我 ,需要的时候我去找你(“ Don’t call us; we’ll call you!” ),把相同的思路运用到我 们的软件工程里通常也被称作“好莱坞原则”( Hollywood Principle ),即告诉开发者 不要主动去构建依赖对象,而是在需要的时候由依赖注入容器把对象提供过来。 这种思路与以前的 new 、工厂创建等方式是正好相反的思想,因此在最开始这样 的思想会被称为“控制反转( IoC )”。从这我们也可以看到,依赖注入的核心思想就是 :由容器提供给我们需要的依赖对象。 某储户类某储户类 @In!@In! @In!@In! 注入点!注入点! 依赖注入容器依赖注入容器 “depositBook” “bank” Java 反射 Java 反射
  • 15. - 15 -- 15 - 1. 入门:依赖注入——“依赖注入”登场 依赖注入框架简介依赖注入框架简介 Apach e Avalo n ~2003 2003 2005 2007 2010 Pico Container 、 Nano Container 、 Apache Hivemind 、 SmartyPants -IoC 最早的 DI 框架 随后涌现出了 诸多 DI 框架 Spring Framework Spring 的出现标 志着 full-stack 框架开始占据主流 至今仍是使用 最广泛的框架 Jboss Seam 集成了 JPA 和 JSF 的 轻量依赖注入框架, 并提供了诸多强大 的开发功能 Google Guice 基于 Java 注解和 Java EDSL 的类型 安全的依赖注入 框架 JavaEE6 : CDI 标准 JSR299 和 JSR330 使得 依赖注入模式统一于 JavaEE 标准
  • 16. - 16 -- 16 - 2. 依赖注入的应用模式——依赖注入容器及其管理对象 在依赖注入容器中声明管理对象在依赖注入容器中声明管理对象 想要容器在被需要时能够将对象提供给注入点处,那么首先在容器里要有这个对 象(严格来说是有这个对象的 META-DATA ,即基础数据,而非对象本身),这就 需要我们首先向容器中“注册”这样的对象进去,或者也可以称为在容器中“声明一个组 件”。不同的依赖注入框架有不同的声明方法,例如 Spring 中可以利用注解或者 XML 方式向容器声明组件。 Spring 容器Spring 容器 @Component // 在 Spring 容器中声明一个“工商银行”的组件 public class BankICBC implements Bank { // …… } <!– 在 Spring 容器中声明一个“工商银行存折”的组件 --> <bean id=" depositBookICBC " class="tutorial.di.ch01.DepositBookICBC "/> 某储户类某储户类 @In! 注入点!注入点! 首先向容器声明 这两个组件 之后容器就可以将 声明好的对象提供 给注入点了
  • 17. - 17 -- 17 - 2. 依赖注入的应用模式——依赖注入的方式 依赖注入的方式依赖注入的方式 在容器中声明好依赖对象之后,这个对象就会在那里“等待”被请求了。那么在什么 地方会被请求呢?这就是前面提到的“注入点”了,即在被声明为“注入点”的地方, 容器会被通知把其管理的依赖对象提供过来。 某储户类某储户类 依赖注入容器依赖注入容器 @In! 在哪里可以声明 这些注入点呢? 一般地,注入点可以被声明在构造函数处、 Setter 方法处、成员变量处。下面我们 详细介绍这几种声明注入点的方法。注意有些依赖注入框架,例如 Guice ,可以将 注入点声明在方法参数上,在此就不再介绍了。
  • 18. - 18 -- 18 - 2. 依赖注入的应用模式——依赖注入的方式 ConstructorConstructor 注入注入 利用 Constructor 注入,即在依赖者类的构造函数上声明注入点,而要被注入 的对象则是构造函数的参数。 某储户类某储户类 依赖注入容器依赖注入容器 ConstructorConstructor @In! 告诉容器利用 构造函数 set 依赖 Spring 框架下的代码例见下一页。。 。
  • 19. - 19 -- 19 - 2. 依赖注入的应用模式——依赖注入的方式 ConstructorConstructor 注入注入 @Component public class Depositor { @Autowired // Constructor 注入点 public Depositor(Bank bank, DepositBook depositBook) { this.bank = bank; this.depositBook = depositBook; } private Bank bank; private DepositBook depositBook; public Cash withDraw(BigDecimal amount) { return bank.withDraw(depositBook, amount); } } @Component // 声明此依赖为 Spring 组件 public class BankICBC implements Bank { // …… } @Component // 声明此依赖为 Spring 组件 public class DepositBookICBC implements DepositBook { // …… }
  • 20. - 20 -- 20 - 2. 依赖注入的应用模式——依赖注入的方式 SetterSetter 注入注入 在依赖者类的 Setter 方法上声明注入点,依赖注入框架利用反射机制调用 Setter 方法来完成依赖注入。 某储户类某储户类 依赖注入容器依赖注入容器 SetterSetter @In! 告诉容器利用 Setter 方法 set 依赖 Seam 框架下的代码例见下一页。。 。
  • 21. - 21 -- 21 - 2. 依赖注入的应用模式——依赖注入的方式 SetterSetter 注入注入 @Name("bank") public class BankICBC implements Bank { // …… } @Name("depositBook") public class DepositBookICBC implements DepositBook { // …… } @Name("depositor") public class Depositor { private Bank bank; private DepositBook depositBook; @In // bank 的 Setter 注入点 public void setBank(Bank bank) { this.bank = bank; } @In // depositBook 的 Setter 注入点 public void setDepositBook(DepositBook depositBook) { this.depositBook = depositBook; } public Cash withDraw(BigDecimal amount) { return bank.withDraw(depositBook, amount); } }
  • 22. - 22 -- 22 - 2. 依赖注入的应用模式——依赖注入的方式 成员变量注入成员变量注入 成员变量注入是指依赖注入框架利用反射机制,直接将依赖者类的成员变量 set 为容器管理的依赖对象。 某储户类某储户类 依赖注入容器依赖注入容器 @In! 告诉容器直接 向成员变量 set 依赖 Seam 框架下的代码例见下一页。。 。 @In!
  • 23. - 23 -- 23 - 2. 依赖注入的应用模式——依赖注入的方式 成员变量注入成员变量注入 @Name("depositor") public class Depositor { @In // bank 的成员变量注入点 private Bank bank; @In // depositBook 的成员变量注入点 private DepositBook depositBook; public Cash withDraw(BigDecimal amount) { return bank.withDraw(depositBook, amount); } } @Name("bank") public class BankICBC implements Bank { // …… } @Name("depositBook") public class DepositBookICBC implements DepositBook { // …… }
  • 24. - 24 -- 24 - 2. 依赖注入的应用模式——依赖注入的方式 注入模式的选择注入模式的选择 再如如果一个类所拥有的依赖数量过多、而开发者又不想构造方法的参数 成为“过长参数列表”(这是一种影响代码的可读性和可维护性的反模 式, Martin Fowler 于 1999 年在其《 Refactoring: Improving the Design of Existing Code 》一书中提出),则可考虑 Setter 注入或者成员变量注入。 此外,如果遇到了类似“循环依赖”的情况,例如宿主和寄生动物这两个对象就 属于相互依存的“循环依赖”模式,这样的情况下,我们就可能需要 Constructor 和 Setter 两种混合的模式才能解决。总而言之,选择依赖注入模式是一个很大 的话题,我们在此就不再深入讨论了。 对于该怎么使用前面介绍的这几种注入模式,究竟在什么情况下该使用哪 种,这是一个讨论非常广泛的话题。这里只举一个简单的例子来说明,比如要 设计一个不可变( final )依赖的类,则必须使用 Constructor 注入方式。 @Component public class Depositor { @Autowired // Constructor 注入点 public Depositor(Bank bank, DepositBook depositBook) { this.bank = bank; this.depositBook = depositBook; } private final Bank bank; // 不可变的依赖 private final DepositBook depositBook; // 不可变的依赖 public Cash withDraw(BigDecimal amount) { return bank.withDraw(depositBook, amount); } }
  • 25. - 25 -- 25 - 2. 依赖注入的应用模式——依赖注入对象的请求模式 依赖注入对象的请求模式依赖注入对象的请求模式 依赖注入容器中已经有了声明的依赖对象,也知道了向哪个注入点提供这 个对象,现在的问题就是,究竟把容器中的哪个对象提供出去呢? 某储户类某储户类 依赖注入容器依赖注入容器 @In! 要把哪个依赖 对象提供出去? 要把哪个依赖 对象提供出去? 这就需要在依赖对象与依赖对象的请求者之间有一个“统一的语言”,即请 求的标识符。依赖对象在容器中以这个标识符被声明,而在注入点上也来请求 这个标识符,这样两方就可以契合上了。一般地,这个标识符可以用字符串、 全类名( FQCN )、两者混合这几种方式,下面详细介绍。
  • 26. - 26 -- 26 - 2. 依赖注入的应用模式——依赖注入对象的请求模式 字符串请求模式字符串请求模式 某储户类某储户类 @In!@In! @In!@In! 依赖注入容器依赖注入容器 “depositBook” “bank” “bank” “depositBook” 顾名思义,字符串请求模式即依赖注入 框架将一个依赖绑定到指定的字符串上 ,而后将其装入依赖注入容器。依赖者 通过这个字符串,向容器请求所需要的 依赖。 Seam 和 Spring 都是典型的基 于字符串请求模式的框架。 @Name("bank") // 将 BankICBC 依赖绑定到 "bank" public class BankICBC implements Bank { // …… } @Name("depositBook") // 将 DepositBookICBC 依赖绑定到 "depositBook" public class DepositBookICBC implements DepositBook { // …… } @In("bank") // 请求标识符为 "bank" 的依赖对象 private Bank bank; @In("depositBook") // 请求标识符为“ depositBook ” 的依赖对象 private DepositBook depositBook;
  • 27. - 27 -- 27 - 2. 依赖注入的应用模式——依赖注入对象的请求模式 字符串请求模式字符串请求模式 某储户类某储户类 @In!@In! @In!@In! 依赖注入容器依赖注入容器 “depositBook” “bak” “bak” “depositBook” 尽管 Seam 和 Spring 都采用了纯字符 串的标识符定义的模式,但这种模式下 有一个很大的弱点:违反了“类型安全 ( type-safe )”原则,也就是说可能 会误将一个本不是依赖者类所希望的依 赖对象注入进来。 @In("bak") // 错误地请求了标识符为 "bak" 的依赖对象 private Bank bank; @Name("bak") // 其他开发者恰好向容器中注册了一个 "bak" 依赖 public class Bak { // …… }
  • 28. - 28 -- 28 - 2. 依赖注入的应用模式——依赖注入对象的请求模式 FQCNFQCN 请求模式请求模式 某储户类某储户类 @In!@In! 依赖注入容器依赖注入容器 Bank.class 为了弥补纯字符串请求模式中的类型安全问 题,全类名( FQCN )请求模式就应运而 生了。其思想便是,在向容器请求依赖对象 的时候,不是通过字符串的标识符、而是通 过被请求的依赖的全类名来定位依赖。这样 如果开发者误将全类名标识符写错的话,在 编译时立即会提醒“类不存在”。并且,如果 使用 Eclipse 等 IDE 开发工具的话,用其 提供的自动完整代码的功能就会轻松地将依 赖的全类名标识符定义到代码中。 // Spring 的全类名注入的 API BeanFactory injector = new FileSystemApplicationContext("depositConfiguration.xml") this.bank = (Bank) injector.getBean(Bank.class); // Seam 的全类名注入的 API this.bank = (Bank) Component.getInstance(BankICBC.class); // Guice 的全类名注入的 API Injector injector = Guice.createInjector(); this.bank = (Bank) injector.getInstance(Bank.class); Bank.class
  • 29. - 29 -- 29 - 2. 依赖注入的应用模式——依赖注入对象的请求模式 FQCNFQCN 请求模式请求模式 某储户类某储户类 @In!@In! 依赖注入容器依赖注入容器 Bank.class 但是如果容器中有多个 Bank 类的实现类, 比如还有一个 BankCMB 的实现类,此时依 赖注入容器就不能正确识别究竟应把哪一个 实现的依赖对象提供给依赖者了,这就是全 类名请求模式的一个缺陷,即其会将依赖对 象的接口限定为只有一个实现。 Bank.class Bank.class
  • 30. - 30 -- 30 - 2. 依赖注入的应用模式——依赖注入对象的请求模式 混合请求模式混合请求模式 某储户类某储户类 @In!@In! 依赖注入容器依赖注入容器 “icbc” Bank.class FQCN (全类名)请求模式会带来依赖定义 的柔软性较差的问题,因此字符串和全类名 混合的模式又应运而生了。顾名思义,这是 一种综合使用字符串和 FQCN 的模式。 “cmb” Bank.class “icbc” Bank.class @In!@In! “cmb” Bank.class // Spring 的字符串 + 全类名注入的 API BeanFactory injector = new FileSystemApplicationContext("depositConfiguration.xml") this.bank = (Bank) injector.getBean(“icbc", Bank.class); // 请求名为 "bank" 且类为 Bank 的依赖 this.bank2 = (Bank) injector.getBean(“cmb", Bank.class); // 请求名为“ cmb" 且类为 Bank 的依赖
  • 31. - 31 -- 31 - 3. 依赖注入对象的 Scope 及其生命周期——依赖注入对象的 Scope 无状态无状态 ScopeScope 某储户类 A某储户类 A @In!@In! 依赖注入容器依赖注入容器 “depositBook” Inject 某储户类 B某储户类 B @In!@In! Inject new new 无状态 Scope 是最简单、生 存期间最短的一种 Scope , 我们所有通过 new 出来的非 静态对象都属于这种 Scope ,它随着使用这个对 象的对象(可能是依赖者类, 也可能是依赖者类里的一个方 法)的消亡而消亡。例如在我 们的例子中,不同的储户需要 不同的存折,因此可以将存折 依赖理解为一个无状态 Scope 。当然依赖注入容器 创建这个依赖的时候远不是 new 那么简单,例如 Spring 框架为依赖者创建一个依赖的 时候就要经过大大小小 9 个阶 段。 “depositBook” “depositBook”
  • 32. - 32 -- 32 - 3. 依赖注入对象的 Scope 及其生命周期——依赖注入对象的 Scope 单例单例 ScopeScope 某储户类 A某储户类 A @In!@In! 依赖注入容器依赖注入容器 “bank” Inject 某储户类 B某储户类 B @In!@In! Inject “bank” “bank” create 单例 Scope ,即 Singleton ,是指以依赖注入 容器( Injector )为单位存在 的唯一的实例。例如在我们的 例子中,假设储户 A 和储户 B 都只能去附近的一所银行,那 么这所银行对于储户 A 和储户 B 来讲,应给都是同一个实例。 在依赖注入容器出现之前,我 们常用静态的模式使一个实例 长久存活在 JVM 的 OLD 区域 中,但是这种方式会造成内存 泄漏和不易管理等问题,因此 在依赖注入设计模式下是不推 荐用静态实现单例模式的。
  • 33. - 33 -- 33 - 3. 依赖注入对象的 Scope 及其生命周期——依赖注入对象的 Scope WebWeb 开发中常用开发中常用 ScopeScope 简介简介第一个比较常用的就是 Application 级 Scope ,通常我们会将一些贯穿整个 EAR 都会用到的常量定义、通用的服务组件、类似 DBConnection 连接创建等比较消耗 资源的组件等等放到这个 Scope 中去。 Application 级 Scope 是与应用程序的 ServletContext 共存亡的,因此可以理解为所部署应用程序的 ServletContext 中 的“单例 Scope” 。 第二个非常常用的是 Session 级的 Scope ,通常我们会将诸如 User 的认证信息 等对象储存在这个区域,它是与客户端(浏览器等)请求所创建的 Session 共存亡 的。 如果把所有数据都存放在 Session 区域,容易增大 Server 端服务的负荷,并且 Session 区域是线程不安全的,因为所有客户端的请求都可以访问到 Session 的数 据并修改。因此开发者常常会需要一个以单次发送的请求即 HttpRequest 为单位的 Scope 。
  • 34. - 34 -- 34 - 3. 依赖注入对象的 Scope 及其生命周期——生命周期管理 生命周期管理生命周期管理 某储户类某储户类 @In!@In! 依赖注入容器依赖注入容器 “bank”“bank” 安排营业员 聘请保安 安排营业员 聘请保安 关闭营业点关闭营业点 @Name("bank") @Scope(ScopeType.APPLICATION) public class BankICBC implements Bank { @Create // 标注为初始化的生命周期方法 public void beforeOpenBank() { assignEmployee(); hireSecurityGuard(); // …… } @Destroy // 标注为结束的生命周期方法 public void closeBank { closeBank(); } } 所谓生命周期管理,就是一 个对象在它所属的 Scope 中从被容器创建开始、到被 提供给依赖者、再到最后的 消亡这一整个过程中,依赖 注入框架提供了一系列的回 调方法的接口,使框架自身 以及开发者都可以利用这些 接口对各个生存时点的依赖 对象做一些操作和管理等。 假设对于银行这个依赖对象 ,在其开业的时候,即开始 进入 Application 级 Scope 的生存期间的时点,一定是 需要很多诸如安排营业员、 聘请保安等初始化的动作的 ,只有这些初始化动作完毕 ,才能做为一个完整的对象 提供给依赖者。
  • 35. - 35 -- 35 - 4. 依赖注入对象的行为增强( AOP )——对依赖对象进行行为增强 所谓 AOP ,就是 Aspect Oriented Programming (面向方面的编程),核心思想 是把一个“方面”独立出来,从而实现组件间的松耦合。在我们的银行依赖中,假设有 个需求,即在每一笔取款业务的前后都要输出日志信息。因此我们需要这样修改我们 的代码: public class BankICBC implements Bank { private static Logger logger = Logger.getLogger(Bank.class.getName()); @Override public Cash withDraw(DepositBook depositBook, BigDecimal amount) { logger.log(Level.INFO, "withdraws starting..."); // …… logger.log(Level.INFO, "withdraws ended…"); } } 我们为了实现这个需求需要在其中加入定义 logger 、输出日志等代码。而这些代码 ,就是我们所说的独立的“方面”。为什么这么说呢?因为日志的输出工作,是与开发 者真正要做的取款业务完全没有关系的。 AOP 的出现,就可以将这个独立的日志处 理的“方面”从实际的依赖对象里分离开来,而在依赖对象在运行的时候,这个“方面” 又可以加到依赖对象身上得以运行,也就是我们所说的依赖对象的行为被增强了,因 为它的行为不但实现了它本身的逻辑,而且也实现了被增强的其它“方面”的逻辑。而 在 AOP 体系内,用以将其它“方面”的逻辑增强到某对象上的组件往往被称作 Interceptor (拦截器)。
  • 36. - 36 -- 36 - 4. 依赖注入对象的行为增强( AOP )——对依赖对象进行行为增强 例如在 Spring 框架中,我们可以这样分离出独立的“方面”: // 此处将日志功能作为“方面”独立出来 public class LogInterceptor { private static Logger logger = Logger.getLogger(LogInterceptor.class.getName()); public Object log(ProceedingJoinPoint call) throws Throwable { logger.log(Level.INFO, "withdraws starting..."); try { return call.proceed(); } finally { logger.log(Level.INFO, "withdraws ended..."); } } } <beans> <!-- …… --> <!-- 这里将“方面”类声明为 Spring 管理的依赖 --> <bean id="logger" class="tutorial.di.ch01.LogInterceptor"/> <!-- 这里将所声明的“方面”增强到需要的地方 --> <aop:config> <aop:aspect ref="logger"> <aop:pointcut id="pointcuts.withdrawMethod" expression="execution(* tutorial.di.ch01.BankICBC.withDraw(..))" /> <aop:around pointcut-ref="pointcuts.withdrawMethod " method="log"/> </aop:aspect> </aop:config> </beans>
  • 37. - 37 -- 37 - 4. 依赖注入对象的行为增强( AOP )——对依赖对象进行行为增强 依赖注入框架中对 AOP 的实现是靠 Java 的动态代理机制实现的,通常使用 JDK 的 代理类,或是借助 Javassist 及 CGLIB 等工具。 某储户类某储户类 @In!@In! 依赖注入容器依赖注入容器 “bank” “bank” 日志功能日志功能 JavaProxyJavaProxy
  • 38. - 38 -- 38 - 4. 依赖注入对象的行为增强( AOP )—— AOP 应用举例 首先就是关于前面介绍过的日志输出类的功能,当然前面的例子非常简单,实际上 要输出的日志信息中往往有很多的可变参数,这时就需要从被拦截对象的上下文中 取出相应的信息进行行为的增强。 最常用的 AOP 应用就是关于 DB 事务的管理了。业务处理成功则向 DB 提交事务 ,反之则回滚事务——这是每一个开发者都会写过的代码。但是实际上这种事务的 处理是完全独立的“方面”,不依赖于任何逻辑的实现,即任何有关 DB 的逻辑处理 都是要提交或者回滚的,因此关于 DB 事务管理的功能往往由 AOP 框架去管 理。 Spring 、 Seam 、 Guice 包括 EJB 应用中的容器管理事务的功能,实质上都 是 AOP 的应用。 另外关于安全处理的功能,往往是和开发者所开发的逻辑无关的“方面”,因为所有 需要判定当前的用户是否有权限执行某段逻辑这样的需求,一般都是同样的处理方 式——用户有权限则继续处理,用户无权限则抛出异常。这种情况下为了避免开发 者写出大量的 if 语句,用框架提供的 AOP 的方式去做安全的判定,应是最佳解决 方案。
  • 39. - 39 -- 39 - 完结 有关依赖注入设计模式的话题,其实是一个非常广泛的话题,不同的框架对其有着共 同或不同的理念,所产生的最佳实践也有相似的一面和各不相同的一面,我们在这里 由于篇幅有限,只能做以上的比较粗浅的介绍,大家如有兴趣可以继续通过互联网和 各种书籍深入研究这种到目前为止非常成功的设计模式。 (完,谢谢大家)