AOP基础知识 Static proxy 与 Dynamic proxy

暂且先将AOP这个英文缩写名词放到一边,先从一个简单常见的例子来着手,这个例子当中含有日志(Logging)动作,程序中很常需要为某些动作或事件作下记录,以便在事后检视程序运作过程或是作为除错时的资讯。

来看一个最简单的例子,当您需要在执行某些方法时留下日志讯息,直觉的,您可能会如下编写:

package onlyfun.caterpillar;

import java.util.logging.*;

public class HelloSpeaker {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());

    public void hello(String name) {
        //
方法执行开始时留下日志
        logger.log(Level.INFO, "hello method starts....");
        //
程序主要功能
        System.out.println("Hello, " + name);
        //
方法执行完毕前留下日志
        logger.log(Level.INFO, "hello method ends....");
    }
}


HelloSpeaker类别中,当执行hello()方法时,您希望该方法执行开始与执行完毕时都能留下日志,最简单的作法就是如以上的程序设计,在方法执行的前后加上日志动作,然而记录的这几行程序码横切入(Cross-cuttingHelloSpeaker类别中,对于 HelloSpeaker来说,日志的这几个动作并不属于HelloSpeaker业务逻辑(显示"Hello"等文字),这使得 HelloSpeaker增加了额外的职责。

想想如果程序中这种日志的动作到处都有需求,以上的写法势必造成您必须到处编写这些日志动作的程序码,这将使得维护日志程序码的困难度加大。如果需要的服务(Service)不只有日志动作,有一些非对象本身职责的相关动作也混入了对象之中(例如权限检查、交易管理等等),会使得对象的负担更形加重,甚至混淆了对象本身该负有的职责,对象本身的职责所占的程序码,或许还小于这些与物件职责不相关的动作或服务的程序码。

另一方面,使用以上的写法,若您有一日不再需要日志(或权限检查、交易管理等)的服务,那么您将需要修改所有留下日志动作的程序码,您无法简单的就将这些相关服务从即有的程序中移去。

可以使用代理(Proxy)机制来解决这个问题,在这边讨论两种代理方式:静态代理(Static proxy动态代理(Dynamic proxy

 

 

 

Static proxy


在静态代理的实现中,代理对象与被代理的对象都必须实现同一个接口,在代理对象中可以实现记录等相关服务,并在需要的时候再呼叫被代理的对象,如此被代理对象当中就可以仅保留业务相关职责。

举个实际的例子来说,首先定义一个IHello接口:

  • IHello.java

package onlyfun.caterpillar;

public interface IHello {
    public void hello(String name);
}


然后让实现业务逻辑的HelloSpeaker类别要实现IHello接口,例如:

  • HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name);
    }
}


可以看到,在HelloSpeaker类别中现在没有任何日志的程序码插入其中,日志服务的实现将被放至代理对象之中,代理对象同样也要实现IHello接口,例如:

  • HelloProxy.java

package onlyfun.caterpillar;

import java.util.logging.*;

public class HelloProxy implements IHello {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());
   
    private IHello helloObject;

    public HelloProxy(IHello helloObject) {
        this.helloObject = helloObject;
    }

    public void hello(String name) {
        //
日志服务
        log("hello method starts....");     

        //
执行业务逻辑
        helloObject.hello(name);
       
        //
日志服务
        log("hello method ends....");
    }
   
    private void log(String msg) {
        logger.log(Level.INFO, msg);
    }
}


HelloProxy类别的hello()方法中,真正实现业务逻辑前后可以安排记录服务,可以实际编写一个测试程序来看看如何使用代理对象。

  • ProxyDemo.java

package onlyfun.caterpillar;

public class ProxyDemo {
    public static void main(String[] args) {
        IHello proxy =
            new HelloProxy(new HelloSpeaker());
        proxy.hello("Justin");
    }
}


程序中调用执行的是代理对象,建构代理对象时必须给它一个被代理对象,记得在操作取回的代理对象时,必须转换操作接口为IHello接口。

代理对象HelloProxy将代理真正的HelloSpeaker来执行hello(),并在其前后加上日志的动作,这使得我们的 HelloSpeaker在编写时不必介入日志动作,HelloSpeaker可以专心于它的职责,可以从图解的方式来更进一步看出代理机制的运作流程。

AOP基础知识 Static proxy 与 Dynamic proxy

这是静态代理的基本范例,然而如您所看到的,代理对象的一个接口只服务于一种类型的对象,而且如果要代理的方法很多,您势必要为每个方法进行代理,静态代理在程序规模稍大时就必定无法胜任,在这边介绍静态代理的目的,是在让您了解代理的基本原理。

 

 

 

Dynamic proxy

 

JDK 1.3之后加入了可协助开发动态代理功能的API等相关类别,您不必为特定对象与方法编写特定的代理对象,使用动态代理,可以使得一个处理者(Handler)服务于各个对象,首先,一个处理者的类别设计必须实作java.lang.reflect.InvocationHandler接口,以实例来进行说明,例如设计一个LogHandler类别:

  • LogHandler.java

package onlyfun.caterpillar;

import java.util.logging.*;
import java.lang.reflect.*;

public class LogHandler implements InvocationHandler {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());
   
    private Object delegate;

    public Object bind(Object delegate) {
        this.delegate = delegate;
        return Proxy.newProxyInstance(
                           delegate.getClass().getClassLoader(),
                           delegate.getClass().getInterfaces(),
                           this);
    }

    public Object invoke(Object proxy, Method method,
                         Object[] args) throws Throwable {
        Object result = null;
       
        try {
            log("method starts..." + method);
           
            result = method.invoke(delegate, args);
           
            logger.log(Level.INFO, "method ends..." + method);
        } catch (Exception e){
            log(e.toString());
        }
       
        return result;
    }
   
    private void log(String message) {
        logger.log(Level.INFO, message);
    }
}


主要的概念是使用Proxy.newProxyInstance()静态方法建立一个代理对象,建立代理对象时必须告知所要代理的接口,之后您可以操作所建立的代理对象,在每次操作时会呼叫InvocationHandlerinvoke()方法,invoke()方法会传入被代理对象的方法名称与执行参数,实际上要执行的方法交由method.invoke(),您在method.invoke()前后加上记录动作,method.invoke()传回的对象是实际方法执行过后的回传结果。

要实现动态代理,同样必须定义所要代理的接口,例如:

  • IHello.java

package onlyfun.caterpillar;

public interface IHello {
    public void hello(String name);
}


然后让实现业务逻辑的HelloSpeaker类别要实现IHello接口,例如:

  • HelloSpeaker.java

package onlyfun.caterpillar;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name);
    }
}


眼尖的您或许发现到了,这跟之前 上面  中的IHello接口、HelloSpeaker是相同的内容,在这边编写出来是为了范例的完整呈现。接下来编写一个测试的程序,您要使用LogHandlerbind()方法来绑定被代理对象,如下所示:

  • ProxyDemo.java

package onlyfun.caterpillar;

public class ProxyDemo {
    public static void main(String[] args) {
        LogHandler logHandler  = new LogHandler();
       
        IHello helloProxy =
                (IHello) logHandler.bind(new HelloSpeaker());
        helloProxy.hello("Justin");
    }
}


回到AOP的议题上,这个例子与AOP有何关系?

如以上的例子中示范的,HelloSpeaker本身的职责是显示输出文字,却必须插入日志(Log)动作,这使得HelloSpeaker的职责加重,在AOP的术语来说,日志的程序码横切(Cross-cuttingHelloSpeaker的程序执行流程中,日志这样的动作在AOP中称之为横切关切点(Cross-cutting concern

使用代理对象将记录等与业务逻辑无关的动作或务提取出来,设计为为一个服务对象,像是之前范例中示范的HelloProxy或是LogHandler,这样的对象称之为切面(Aspect

AOP
中的Aspect所指的可以是像日志等这类的动作或服务,您将这些动作(Cross-cutting concerns)设计为通用、不介入特定业务对象的一个职责清楚的Aspect对象,这就是所谓的Aspect-oriented programming,缩写名词即为AOP

在好的设计之下,Aspect可以独立于应用程序之外,在必要的时候,可以介入应用程序之中提供服务,而不需要相关服务的时候,又可以将这些Aspect直接从应用程序中脱离,而您的应用程序本身不需修改任何一行程序码。

 


原文链接: http://www.dlog.cn/nicholascoder/diary/12917

转载于:https://my.oschina.net/moroseyu/blog/51374