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-cutting)HelloSpeaker类别中,对于 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可以专心于它的职责,可以从图解的方式来更进一步看出代理机制的运作流程。
这是静态代理的基本范例,然而如您所看到的,代理对象的一个接口只服务于一种类型的对象,而且如果要代理的方法很多,您势必要为每个方法进行代理,静态代理在程序规模稍大时就必定无法胜任,在这边介绍静态代理的目的,是在让您了解代理的基本原理。
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()静态方法建立一个代理对象,建立代理对象时必须告知所要代理的接口,之后您可以操作所建立的代理对象,在每次操作时会呼叫InvocationHandler的invoke()方法,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是相同的内容,在这边编写出来是为了范例的完整呈现。接下来编写一个测试的程序,您要使用LogHandler的bind()方法来绑定被代理对象,如下所示:
- 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-cutting)入HelloSpeaker的程序执行流程中,日志这样的动作在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