动态代理再思考

看了透明发表在《程序员》杂志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] – 大师言论

《设计模式》一书中给出的理由是“我们有时需要提供一个代理来控制对这个对象(上面例子中的targetBusinessImpl的一个实例)的访问”。书中列举了几种可能使用到代理的情况:

* 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 BusinessIntfProxy.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

 

关注,AOP

AOP的核心概念是关注点,我开始关注AOP。

一、Why AOP?
a) AOP一般观点
一般在开发系统时,我们可以大致的把系统的需求分类为核心模块级需求和系统级需求。很多系统级需求一般来说是相互独立的,但它们一般都会横切许多核心级模块。以一个电信领域的短信网关系统为例,系统的核心级需求是短信的收发,话单处理等,而其系统级的需求包括日志,校验以及性能问题等。像日志这种系统级的需求就横切短信收发、话单处理等几乎所有网关核心级需求。虽然横切需求会跨越多个模块,但目前的技术倾向于使用一维的方法学来处理这种横切需求,把对应需求的实现强行限制在一维的空间里。这个一维空间就是核心模块级实现,其他横切需求的实现被嵌入在这个占统治地位的空间,换句话说,需求空间是一个n维空间,而实现空间是一维空间,这种不匹配导致了糟糕的需求到实现的映射。

b) AOP开发过程
AOP(面向方面的编程方式)将上面所述的横切需求和核心需求都通通称为关注点(concerns)。AOP认为每一个复杂的系统都可以看作是由多个关注点来组合实现的。在这样的情况下,利用AOP开发系统的过程就变成了“识别关注点”(方面分解)—〉“实现关注点”—〉“组装关注点”(方面的重组,又叫织入)。

二、AOP和OOP
a)  AOP作为OOP的补充,它提供了另外一种考量程序结构(program structure)的方式。OO将系统分解成一系列具有继承体系关系的objects;而AOP则将系统分解为aspects or concerns。而且AOP可更好地将一些横切concerns模块化,便于复用和维护。

b)  AOP,从其本质上讲,使你可以用一种松散耦合的方式来实现独立的关注点,然后,组合这些松散耦合的、模块化实现的横切关注点来搭建系统。与之对照,用OOP建立的系统则是用松散耦合的模块化实现的一般关注点来实现的。在AOP中,这些良好模块化的横切关注点的实现单元叫方面(aspect),而在OOP中,这些一般关注点的实现单元叫做类(class)。

c)  在AOP里,每个关注点的实现并不知道是否有其它关注点关注它,这是AOP和OOP的主要区别,在AOP里,组合的流向是从横切关注点到核心关注点,而OOP则恰恰相反。

d)  感觉AOP是在OOP后对那些objects重新分类的过程,将被广泛关注的objects(即横切于多个objects中的objects)提取出来,加工后再重新和原来核心objects组合起来。

三、AOP基本概念

下面是一些AOP基本概念的解释,摘录自AspectJ的FAQ和Spring AOP的Online book:
a)  Aspect(方面): A modularization of a concern for which the implementation might otherwise cut across multiple objects。

b)  Joinpoint(连接点): Join points are well-defined points in the execution of a program. Not every execution point is a join point: only those points that can be used in a disciplined and principled manner are。

c)  Advice(通知): Advice is code that executes at each join point picked out by a pointcut There are three kinds of advice: before advice, around advice and after advice. As their names suggest, before advice runs before the join point executes; around advice executes before and after the join point; and after advice executes after the join point. The power of advice comes from the advice being able to access values in the execution context of a pointcut.

d)  Pointcut(横切点): A set of joinpoints specifying when an advice should fire. An AOP framework must allow developers to specify pointcuts: for example, using regular expressions.  A pointcut picks out join points . These join points are described by the pointcut declaration. Pointcuts can be defined in classes or in aspects, and can be named or be anonymous.

e)  Weaving(织入):将Aspects组装成为最终系统。这个织入过程可以在编译期完成,也可以在运行时完成。

我想我们可以如下图理解AOP:
|———————————–|
|        pointcut         |
|    |————————|   |
|    |    advice1      |   |
|    |    |————–|  |   |
|    |    | jointpoint1|  |   |
|    |    |————–|  |   |
|    |                |   |
|    |————————|   |
|                        |
|    |————————|   |
|    |    advice2      |   |
|    |    |————–|  |   |
|    |    | jointpoint2|  |   |
|    |    |————–|  |   |
|    |                |   |
|    |————————|   |
|    …….                |
|————————————|

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats