引子
在过去的几年里, AOP( 面向方面编程 ) 已成为 Java 领域的热门话题,对 JAVA 开发人员来说,现在已经能够找到许多关于 AOP 的文章、讨论和实现了。 AOP 通常被称为实现横切关注点的工具,这意味着你可以使用 AOP 来将独立的逻辑片段模块化,也就是我们熟知的关注点,并将这些关注点应用于应用程序的多个地方。
AOP 和 OOP 并不相互抵触,它们是可以相辅相成的两个设计模型, Spring AOP 是实现 AOP 的一种技术,而 Spring AOP 也是 Spring 中一些子框架或子功能所依赖的核心。
在本文中,首先会讲解两种截然不同的 AOP 类型,静态的和动态的。在静态 AOP 中 , 如 AspectJ 的 AOP ,横切逻辑会在编译时应用到你的代码上,若非修改代码并重新编译的话你是不能改变它的。而使用动态 AOP 时,如 Spring 的 AOP ,横切逻辑是在运行时被动态加入的,这允许你无需重新编译代码就能修改横切的使用。这两种 AOP 相互补充,将它们结合使用将会在我们的应用中形成强有力的组合。
Spring 项目中有很多能集各种设计模式、编码技巧为一体的编码艺术,在灵活应用 Spring 的同时,若能把 Spring 项目里面的精华、设计思想、编码技巧等吸纳过来,这对于程序员来说将会是一件非常有意义的事。
从代理机制初探 AOP
我们暂且把 AOP 放到一边,先从一个简单例子来看一个议题,这个例子当中包含日志 (Logging) 动作,程序中常需要为某些动作或事件记下记录,以便在事后检查程序运作过程,或是作为出错时的信息。
来看一个最简单的例子,当你需要在执行某些方法时留下日志信息,可能会如下编写:
package inside.aop;
import java.util.logging.Level;
import java.util.logging.Logger;
public class HelloSpeaker {
private Logger logger=Logger.getLogger(this.getClass().getName());
public void hello(String name){
//方法执行开始时留下记录
logger.log(Level.INFO,"hello method start..............");
//程序主要功能
System.out.println("hello"+name);
//程序执行完毕时留下记录
logger.log(Level.INFO,"hello method end..............");
}
}
在 HelloSpeaker 类中,当执行 hello() 时,你希望方法在开始执行和执行完毕时都能留下记录,最简单的作法就是如以上的程序设计,在方法执行的前后加上日志动作,然而日志的这几行程序代码横切入 (Cross-cutting)HelloSpeaker 类中,对于 HelloSpeaker 类来说,日志的这几个动作并不属于 HelloSpeaker 业务逻辑,这无疑是 HelloSpeaker 增加了额外的职责 ( 违反了面向对象设计的类的单一职责原则 ) 。
可以使用代理 (Proxy) 机制来解决这个问题,在这里讨论两种代理方法:静态代理 (Static Proxy) 与动态代理 (Dynamic Proxy) 。
静态代理
在静态代理的实现中,代理对象与被代理对象必须实现同一个接口,在代理对象中可以实现日志等相关服务,并在需要的时候在调用被代理的对象,如此,被代理对象当中就可以仅保留与业务相关的职责。
重新设计 HelloSpeaker 类,首先定义一个 IHello 接口:
package inside.aop;
public interface IHello {
public void hello(String name);
}
然后让实现业务逻辑的 HelloSpeaker 类实现 IHello 接口,例如:
package inside.aop;
public class HelloSpeaker implements IHello{
public void hello(String name){
//程序主要业务逻辑
System.out.println("hello"+name);
}
}
可以看到,在 HelloSpeaker 类中现在没有任何日志的程序插入其中,日志服务的实现将被放置代理之中,代理对象同样也要实现 IHello 接口,例如:
package inside.aop;
import java.util.logging.Level;
import java.util.logging.Logger;
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) {
//日志服务
logger.log(Level.INFO,"hello method start..............");
//执行业务逻辑
helloObject.hello(name);
//日志服务
logger.log(Level.INFO,"hello method end..............");
}
}
在 HelloProxy 类的 hello() 方法中,要真正实现业务逻辑前后可以安排日志服务,下面我们编写一个测试程序来看看如何使用代理对象。
package inside.aop;
public class StaticProxyTest {
public static void main(String[] args) {
IHello proxy=new HelloProxy(new HelloSpeaker());
proxy.hello("aop");
}
}
程序中调用执行的是代理对象,构造代理对象时必须给它一个被代理对象,记得在操作取回代理对象时,必须转换操作接口为 IHello 接口,下面是实际执行的结果。
2010-9-29 19:10:04 inside.aop.HelloProxy hello
信息: hello method start..............
hello,aop
2010-9-29 19:10:04 inside.aop.HelloProxy hello
信息: hello method end..............
这是静态代理的基本范例,然而正如你看到的,代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每种方法进行代理,静态代理在程序规模较大时就无法胜任了,根据这个设计思想在 JDK1.3 之后就加入了动态代理的功能。在这里介绍静态代理的目的,是为了了解代理的基本原理。
动态代理
在 JDK1.3 之后加入了可协助开发动态代理功能的 API ,从此不必为特定的对象和方法编写特定的代理对象。使用代理对象,可以使用一个处理者 (Handler) 服务于各个对象。首先,一个处理者的类设计必须实现 java.lang.reflect.InvocationHandler 接口,下面用实例来进行说明,设计一个 LogHandler 类:
package inside.aop;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LogHandler implements InvocationHandler {
private Logger logger=Logger.getLogger(this.getClass().getName());
private Object target;
public Object bind(Object target){
this.target=target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object retValue=null;
logger.log(Level.INFO,"method start..........");
retValue=method.invoke(target, args);
logger.log(Level.INFO,"method end..........");
return retValue;
}
}
主要的概念是使用 Proxy .newProxyInstance() 静态方法建立一个代理对象,建立代理对象时必须告知要代理的接口,之后就可以操作所建立的代理对象。在每次操作时会执行 InvocationHandler 的 invoke() 方法, invoke() 方法会传入被代理对象的方法名称与执行参数,实际上要执行的方法会交由 method.invoke() 。如果对上面代码不清楚的地方,请读者自行查阅相关的 JDK 动态代理的只是。
要实现动态代理,同样必须定义所要代理的接口,此处使用之前定义的 IHello 接口,以及 HelloSpeaker 类。测试程序如下所示:
package inside.aop;
public class DynamicProxyTest {
public static void main(String[] args) {
LogHandler logHandler=new LogHandler();
IHello proxy=(IHello) logHandler.bind(new HelloSpeaker());
proxy.hello("AOP");
}
}
来看一下执行结果,如下所示:
2010-9-29 19:42:36 inside.aop.LogHandler invoke
信息: method start..........
hello,AOP
2010-9-29 19:42:36 inside.aop.LogHandler invoke
信息: method end..........
LogHandler 不再服务于特定对象接口,而 HelloSpeaker 也不用插入任何有关日志的动作,它不用意识到日志动作的存在。
现在回到 AOP 的议题上,那么我们之前的例子和 AOP 有什么关系?
HelloSpeaker 本身的职责是显示文字,却要插入日志动作,这使得 HelloSpeaker 的职责加重,用 AOP 的术语来说,日志的程序代码横切 (Cross-cutting) 入 HelloSpeaker 的程序执行流程中,日志这样的动作在 AOP 中称之为横切关注点 (Cross-cutting concern) 。
使用代理对象将日志等于业务逻辑无关的动作或任务提取出来,设计成一个服务对象,这样的对象称之为切面 (Aspect) 。
AOP 中的 Aspect 所指的像日志等这类的动作或服务,将这些动作 (Cross-cutting concerns) 设计为通用、不介入特定业务对象的一个职责清楚地 Aspect 对象,这就是所谓的 Aspect-oriented programming ,缩写名称即为 AOP 。
从上面的几个例子中可以看出,在好的设计之下, Aspect 可以独立于应用程序之外,在必要的时候,可以介入应用程序之中提供服务,在不需要相关服务的时候,又可以将这些 Aspect 直接从应用程序中脱离出来,而应用程序本身不需要修改任何一行程序代码。
AOP 术语
Cross-cutting concern( 横切关注点 )
在上面的例子中,日志的动作原先被横切 (Cross -cutting) 入至 HelloSpeaker 本身所负责的业务流程中,另外类似于日志这类的动作,如安全 (Security) 检查、事务 (Transaction) 等系统层面的服务 (Service) ,在一些应用程序之中常被见到安插到各个对象的处理流程之中,这些动作在 AOP 的术语称为 Cross-cutting concerns 。
Aspect(切面)
将散落在各个业务逻辑之中的 Cross-cutting concerns 收集起来,设计成各个独立可重用的对象,这些对象称为 Aspect 。例如,在我们的例子中,将日志的动作设计为一个 LogHandler 类, LogHandler 类在 AOP 的术语就是 Aspect 的一个具体事例。
在 AOP 中着重于 Aspect 的辨认,使之从业务流程中独立出来。在需要该服务的时候,织入 (weave) 至应用程序之上;在不需要服务的时候,也可以马上从应用程序中剥离,且应用程序中的可重用组件不用做任何修改。
另一方面,对于应用程序中的可重用组件来说,按照 AOP 的设计方式,它不用知道提供服务的对象是否存在,具体地说,与服务相关的 API 不会出现在可重用的应用程序组件之上,因而可提高这些组件的可重用性,你可以把这些组件应用至其他的应用程序之中,不会因为加入了某些服务而与目前的应用程序框架发生耦合。
在 Spring AOP 中,一个方面是由一个实现 Advisor( 通知者 ) 接口的类来表示。 Spring 提供了一些使用方便的 Advisor 接口的接口类,这样不用在自己的程序中创建各种各样不同的 Advisor 实例。
Advice(增强或通知)
Advice 不管怎么翻译成建议、通知或者增强,都不能直接反映其内容。笔者认为通知稍微能够体现出 Advice 的本质。
Aspect 当中 Cross-cutting concerns 的具体实现称之为 Advice 。以日志的动作而言, Advice 中会包含日志程序代码是如何实现。 Advice 中包含了 Cross-cutting concerns 的行为或所要提供的服务。
换一种说法,通知 (Advice) 是指在定义好的切入点处,所要执行的程序代码。
JoinPoint(连接点)
Advice 在应用程序执行时加入业务流程的点或时机称之为 Joinpoint ,具体来说,就是 Advice 在应用程序中被执行的时机。 Spring 只支持方法的 Joinpoint ,执行时机可能是某个方法被执行之前或之后 ( 或两者都有 ) ,或是方法中某个异常发生的时候。
Spring AOP 中最明显的简化之一就是它只支持一种类型的连接点:方法调用。我们可以用它来完成大多数用到 AOP 的日常编程任务。
Pointcut(切入点)
Pointcut 定义了感兴趣的 Joinpoint ,当调用的方法符合 Pointcut 表示式时,将 Advice 织入至应用程序上提供服务。切入点指一个或多个连接点,可以理解成一个点的集合。切入点的描述比较具体,而且一般会跟连接点上下文环境结合。
Target(目标对象)
一个 Advice 被应用的对象或目标对象,在基于拦截器机制实现的 AOP 框架中,位于拦截器链上最末端的对象实例。一般情况下,拦截器末端都包含一个目标对象,通常也是实际业务对象。
Introduction(引入)
对于一个现存的类, Introduction 可以为其增加行为,且不用修改该类的程序,具体来说,可以为某个已编写或编译完的类,在执行时期动态地加入一些方法或行为,而不用修改或新增任何一行程序代码。
在 Spring 中,引入被认为是一种特殊的通知。
Interceptor(拦截器)
拦截器是用来实现对连接点进行拦截,从而在连接点或后加入自定义的切面模块功能。在大多数 JAVA 的框架实现中,都是使用拦截器来实现字段访问及方法调用的拦截 (Interception) 。所以作用于同一个连接点的多个拦截器组成的一个拦截器链 (Interceptor chain) ,链接上的每个拦截器通常会调用下一个拦截器。 Spring AOP 就是采用拦截器来实现。
Proxy(代理)
在之前的静态代理和动态代理中,已经使用了实际的程序范例介绍过的代理机制, Spring 的 AOP 主要是通过动态代理来完成的,可用于代理任何的接口。另一方面, Spring 也可以使用 CGLIB 代理,可以代理类。
Weave(织入)
Advice 被应用至对象之上的过程成为织入 (Weave) ,在 AOP 中织入的方式有几个时间点:编译时期 (Compile time) 、类加载时期 (Classload time) 、执行时期 (Runtime).