动态代理再思考
看了透明发表在《程序员》杂志2005年第一期上的“动态代理的前世今生”,让我不仅了解了“动态代理”这门技术,更让我知道了一段Java技术的发展史。带着对Rickard Oberg的钦佩之情,怀着对Rod Johnson敬仰之义我踏上了动态代理再思考之路。
[关键词]
代理(proxy)
基础设施(infrastructure)
业务组件(business component)
拦截器(interceptor)
面向方面编程(AOP)
千里之行,始于足下;
九层之台,起于累土
任何事情都不能一蹴而就,物极必反的道理相信大家都或多或少的懂一些。
动态代理是一门较高级的技术,我们自己在平时的开发中也许很少用到,但是在你使用的开源工具包中也许就有它的足迹。动态代理技术用起来简单,但是理解起来并不是那么顺畅,我们从最简单的地方开说。
我们先来看看动态代理的定义:
[Definition]
动态代理类是这样的一个类:可以在运行时、在创建这个类的时候才指定它所实现的接口。每个代理类的实例都有一个对应的InvocationHandler对象。
也许看完这个定义,第一感觉是“看了还不如不看”J。
不过在你理解了动态代理之后你会体会到这句话的确很精辟。
下面是改自JDK Doc中的一个动态代理的例子,我们先来个感性认识,看例子的时候别忘了回头复习一下那个Definition,也你灵光一闪,一切都豁然开朗。
/*******************************begin******************************************/
[Demo-1]
//要代理的接口的定义
public interface BusinessIntf {
void doSomething();
}
//用户代码
BusinessIntf b = (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader ,
new Class[] { BusinessIntf.class },
handler);
b.doSomething();
/*********************************end****************************************/
观后而感之,使用动态代理就这么简单。在运行时、在Proxy实例创建时指定要代理的接口(这里的代理接口是BusinessIntf,我们要通过Proxy来获得该接口的一个实现类的实例)。除了指定代理接口之外,我们不能忘记还有个重要的参数需要传递, 那就是一个InvocationHandler接口的实现。大家一定想到了真正的业务逻辑实现一定与handler参数有关,继续探秘。
察看Doc,发现InvocationHandler下面只有这么一个方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
Doc中的英文说明太长,不看了,找一个例子看看吧。
/************************************begin************************************/
public class MyInvocationHandler implements InvocationHandler{
private final Object target;
public MyInvocationHandler(final Object target){
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Object result = method.invoke (target, args);
return result;
} catch (final InvocationTargetException e) {
throw e.getTargetException( );
}
}
};
BusinessImpl target = new BusinessImpl (); //BusinessImpl class implements the BusinessIntf
MyInvocationHandler handler = new MyInvocationHandler (target);
/***********************************end***************************************/
恍然大悟,原来真正实现业务逻辑的是传给MyInvocationHandler的一个BusinessIntf的实现。
也许你又陷入另一种疑惑当中,你心里可能在想这样的一个问题:只是为了获得BusinessIntf的一个实现类的实例而已,用得着使用动态代理这样高级的技术,绕个大圈子吗?像下面这样写不就可以了么
BusinessIntf b = new BusinessImpl ();
b.doSomething();
或者如果想写的高级一点我们可以采用静态代理,使用工厂模式
比如:
class BusinessFactory {
//…
public static BusinessInf getBussinessImpl() {
return new BusinessImpl();
}
}
BusinessIntf b = BusinessFactory.getBussinessImpl();
b.doSomething();
我曾几何时不是这么想的。不过还是先看看下面的理由能否说服你吧。
[理由1] – 大师言论
《设计模式》一书中给出的理由是“我们有时需要提供一个代理来控制对这个对象(上面例子中的target,BusinessImpl的一个实例)的访问”。书中列举了几种可能使用到代理的情况:
* Remote Proxy – 隐藏对象的空间信息
* Virtual Proxy – 不常见
* Protection Proxy – 访问权限控制
* Smart Reference – 用于提供访问对象时的附加操作
[理由2] – 动态性
运行时改变 – 体现出其动态性
之所以称之为动态代理,就是因为该代理类的实例可实现任意的业务接口,并且可以在运行时决定一个实例究竟实现哪个接口。
从上面的代码也可以看出:
1、 我们可以在运行时改变我们要实现的接口;
2、 我们可以在运行时改变传入的InvocationHandler的实现;换句话说InvocationHandler可以创建任何接口的实例;
3、 我们可以改变在MyInvocationHandler中那个真正实现业务逻辑的对象(就是那个target)。
以上的动态性是使用静态代理较难做到的。
美则观之,
美则用之
经过上面的阐述,我们领略些动态代理的优势,不过我们再来看看Demo-1的用户代码,
BusinessIntf b = (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader ,
new Class[] { BusinessIntf.class },
handler);
b.doSomething();
要使用BusinessIntf接口还真是不那么容易,起码我们需要自己传入handler,而handler的定义也给用户带来了很大的麻烦。
我们要明确用户究竟想要什么?
当用户写下如下代码“BusinessIntf b = ”时你会怎么想,显然用户需要的是一个BusinessIntf接口实现类的实例。而像上面的代码我们却要求用户写一些他们并不十分关心的东西,这显然不美。我们来做一下改进,使动态代理可以像静态代理那样用。
[Demo-2]
/*******************************begin******************************************/
public class BusinessProxyFactory {
public static BusinessIntf newProxyInstance() {
BusinessImpl target = new BusinessImpl ();
MyInvocationHandler handler = new MyInvocationHandler (target);
return (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader() ,
new Class[] { BusinessIntf.class },
handler);
}
}
//用户代码
BusinessIntf b = BusinessProxyFactory.newProxyInstance();
b.doSomething();
/*******************************end******************************************/
轻量级容器之风行
自从PicoContainer、Spring等轻量级容器诞生后,在J2EE世界就刮起了一股“轻量级”之风。轻量级容器实现了一种“依赖注入”的机制。
以PicoContainer为例,它实现了
a) 全权管理组件的创建、生命周期和依赖关系;
b) 使用者获取组件必须通过容器,容器保证组件全局唯一访问点。
我在这里对上面的代码进行“容器化改造”,使之跟上“容器之风”J
// GeneralInvocationHandler.java
public interface GeneralInvocationHandler extends InvocationHandler{
Class getImplClass();
}
//ProxyFactory.java
public class ProxyFactory {
private GeneralInvocationHandler handler;
public ProxyFactory(GeneralInvocationHandler handler){
this.handler = handler;
}
public Object newProxyInstance(){
return Proxy.newProxyInstance(handler.getImplClass().getClassLoader(),
new Class[] { handler.getImplClass() },
handler);
}
}
[Note] Demo-3设计说明(2)
在类图中BusinessImplProxy实现了GeneralInvocationHandler,并依赖BusinessIntf接口,也就是说一个GeneralInvocationHandler的实现类(如BusinessImplProxy)是与一个特定的业务接口绑定的,它只能代理唯一的接口,不过选择哪个代理接口的实现类,我们可以在配置文件中在运行时指定。
//BusinessIntf.java,定义一个业务接口
public interface BusinessIntf {
void doSomething();
}
//BusinessImplProxy.java,该类绑定了BusinessIntf接口
public class BusinessImplProxy implements GeneralInvocationHandler{
private BusinessIntf b ;
public BusinessImplProxy(BusinessIntf b){
this.b = b;
}
public Class getImplClass() {
return BusinessIntf.class;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Object result = method.invoke(b, args);
return result;
} catch (final InvocationTargetException ex) {
throw ex.getTargetException( );
}
}
}
[Note] Demo-3设计说明(3)
经过上面的两个说明,我们可以得出下面结论:
1、 ProxyFactory可以获取任意接口的实例,它依赖于一个绑定了特定业务接口的GeneralInvocationHandler的实现类;
2、 GeneralInvocationHandler的实现类绑定了特定的业务接口,我们可以在运行时指定具体的业务接口的实现类;
3、 所有这些我们都使用PicoContainer来进行组装,我们只需要提供配置文件。
/*******************************begin******************************************/
//Client.java ,欲使用BusinessIntf接口的Client
public class Client {
private ProxyFactory pf;
public Client(ProxyFactory pf){
this.pf = pf;
}
public void run(){
BusinessIntf b = (BusinessIntf)pf.newProxyInstance();
b.doSomething();
}
}
//Main.java
public class Main {
public PicoContainer buildContainer(ScriptedContainerBuilder builder,
PicoContainer parentContainer, Object scope) {
ObjectReference containerRef = new SimpleReference();
ObjectReference parentContainerRef = new SimpleReference();
parentContainerRef.set(parentContainer);
builder.buildContainer(containerRef, parentContainerRef, scope, true);
return (PicoContainer) containerRef.get();
}
public void startup() {
Reader script = null;
try {
script = new FileReader("nanocontainer.xml");
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
XMLContainerBuilder builder = new XMLContainerBuilder(script,
getClass().getClassLoader());
PicoContainer pico = buildContainer(builder, null, "SOME_SCOPE");
Client c = (Client)pico.getComponentInstance(Client.class);
c.run();
}
public static void main(String[] args){
Main app = new Main();
app.startup();
}
}
//nanocontainer.xml
/*******************************end******************************************/
进化,go on!, AOP
[Note]
基础设施、业务组件和用户代码三者之间的关系:
l 基础设施:包括系统的日志、安全性检查、事务管理等,这些功能的共同点 就是存在于各个业务对象的继承体系当中,任何业务对象都有可能需要它们。
l 业务组件:系统对外提供核心业务逻辑的业务对象或业务对象的集合。
l 用户代码:根据系统提供的业务接口,调用业务组件完成特定功能。
一般用户代码只和业务组件打交道,用户并不关心业务组件是否使用了和使用了哪些基础设施。在Note中也说过基础设施存在于各个业务组件中,我们来考虑这样一个问题:假设我们有业务组件business1,business2,business3,我们提供了日志和事务管理两种基础设施,开始的时候我们由于需求的原因,我们只在各个业务组件(business1—business3)中使用了日志这么一种基础设施,现在需求发生变化了,我们需要在各个组件中加入事务管理。我们怎么办?体力活,一个组件一个组件的修改。客户的需求总是在变化,也许明天又会有“添加安全性检查”的需求。现在一切都集中到了这样一个问题上:
[问题]
“如何不修改业务组件代码,而动态的添加和删除组件需要的基础设施”?
Interceptor(拦截器),将各个基础设施都实现为拦截器,业务组件需要哪些基础设施直接在配置文件中配置即可。而业务组件在真正执行业务前需经过一个基础设施的拦截器链的拦截。而拦截器的一个主要的实现技术就是“动态带来技术”。当然这个实现更加复杂。
了解AOP的人对上面的描述一定不会感到陌生,因为这也恰是一种AOP的思想。目前很多AOP的开源实现都是基于“动态代理”技术。著名的AOP联盟也发布了“基于动态代理的AOP框架”。如果对之感兴趣的话,可以继续深入研究。
参考资料
1、“动态代理的前世今生” 《程序员》 2005-01期
2、《Hardcore Java》
3、JDK Doc
4、PicoContainer/NanoContainer Doc
评论