在一个服务的流程中插入与业务逻辑无关的系统服务(如Logging、Security),这样的逻辑称为Cross-cutting concerns,将Cross-cutting concerns独立出来设计为一个对象,这样的特殊对象称之为Aspect(切面),Aspect-oriented programming着重在Aspect的设计上以及与应用程序的织入(Weave)。
AOP跟OOP并不互相抵触,它们是可以相辅相成的两个设计模型,Spring AOP是实现AOP的一种技术。
一、AOP入门
1.1 从代理机制初探AOP
一个例子,这个例子中含有日志(Logging)动作。
public class HelloSpeaker{
private Logger logger = Logger.getLogger(this.getClass().getName());
public void hello(String name){
logger.log(Level1.INFO,"hello method start...");
System.out.println("hello"+name);
logger.log(Level1.INFO,"hello method end...");
}
}
在这个例子中,违反了单一职责原则。对于HelloSpeaker来说,日志记录不属于它的业务逻辑部分,而logging代码横切入(Cross-cutting)HelloSpeaker类中。
这种代码到处都有会使得系统难以维护。如果需要的服务(Service)不只是日志,如权限检测,事务管理等,会使得系统维护负担加重。甚至混淆了本身该有的职责。
可以使用代理(Proxy)机制来解决这个问题:静态代理和动态代理。
l 静态代理
在静态代理中,代理对象和被代理对象必须实现同一个接口。代理对象中可以实现Logging等系统功能,并在需要的时候再调用被代理对象,如此被代理对象中就可以仅保留业务逻辑相关的职责。
例如:
public Interface IHello{
public void hello(String name);
}
业务逻辑的HelloSpeaker实现该接口。
public class HelloSpeaker{
public void hello(String name){
System.out.println("Hello" + name);
}
}
这个类没有插入与业务逻辑无关的内容。
日志服务的实现将被放置在代理对象中,代理对象要实现IHello接口。例如:
public class HelloProxy implements IHello{
private Logger logger = Logger.getLogger(this.getClass().getName());
IHello helloObject;
public HelloProxy(IHello helloObject){
this.helloObject = helloObject;
}
public void hello(String name){
log("hello method start...");
this.helloObject.hello(name);
log("hello method end...");
}
public void log(String msg){
logger.log(msg);
}
}
编写测试客户程序。
public class ProxyTest{
public static void main(String[] args){
IHello proxy = new HelloProxy(new HelloSpeaker());
proxy.hello("SH");
}
}
静态代理一个接口只服务于一种类型的对象。在程序规模稍大时就无法胜任了。
l 动态代理
不必为特定的对象与方法编写特定的代理对象。使用动态代理,可使一个处理者服务于多个对象。首先,一个处理者必须实现java.lang.reflect.InvocationHandler接口,例如:
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("method ends" + method);
}
catch(Exception e){
log(e.toString());
}
return result;
}
private void log(String message){
logger.log(message);
}
}
主要的概念是使用Proxy.newProxyInstance()静态方法建立一个代理,建立代理时必须告知所要代理的接口,之后可以操作所建立的代理对象。在每次操作时会执行InvocationHandler的invoke()方法,传入被代理对象的方法名与执行参数,实际上要执行的方法会交由method.invoke(),method.invoke()传回的对象是实际方法执行过后的回传结果。
要执行动态代理,同样要实现代理的接口。与静态代理一样。例如:
public Interface IHello{
public void hello(String name);
}
让业务逻辑的类实现IHello接口,例如:
public class HelloSpeaker implements IHello{
public void hello(String name){
System.out.println("Hello" + name);
}
}
写个客户程序:
public class ProxyTest{
public static void main(String[] args){
LogHandler logHandler = new LogHandler();
IHello helloProxy = logHandler.bind(new HelloSpeaker());
helloProxy.hello("SH");
}
}
与静态代理的区别:
LogHandler不再服务于特定的对象与接口。
代理与AOP关系:
使用代理将日志等与业务逻辑无关的动作或任务提取出来,设计成一个服务对象,像之前提到的HelloProxy和LogHandler,这样的对象称之为切面(Aapect)。
AOP中的Aspect所指的像日志这类的动作或服务,将这些动作(Cross-cutting concerns)设计为通用的、不介入特定业务逻辑对象的一个职责清楚的Aspect对象,这就是Aspect-oriented programming,AOP。
1.2 AOP观念与术语
AOP全名为Aspect-Oriented Programming,有关于AOP的许多名词术语都过于抽象,单从字面上并不容易理解其名词意义,这边将以之前介绍代理机制的范例来逐一对照以介绍AOP的术语与观念:
- Cross-cutting concern
在DynamicProxyDemo项目的例子中,记录的动作原先被横切(Cross-cutting)入至HelloSpeaker本身所负责的业务流程之中,另外类似于记录这类的动作,如安全(Security)检查、事务(Transaction)等系统层面的服务(Service),在一些应用程序之中常被见到安插至各个对象的处理流程之中,这些动作在AOP的术语中被称之为Cross-cutting concerns。
以图片说明可强调出Cross-cutting concerns的意涵,例如原来的业务流程是很单纯的:
现在为了要加入记录(Logging)与安全(Security)检查等服务,对象的程序代码中若被硬生生的写入相关的Logging、Security程序片段,则可使用以下图解表示出Cross-cutting与Cross-cutting concerns的概念:
Cross-cutting concerns若直接撰写在负责某业务的对象之流程中,会使得维护程序的成本增高,例如若您今天要将对象中的记录功能修改或是移除该服务,则必须修改所有撰写曾记录服务的程序代码,然后重新编译,另一方面,Cross-cutting concerns混杂于业务逻辑之中,使得业务对象本身的逻辑或程序的撰写更为复杂。
- Aspect
将散落于各个业务对象之中的Cross-cutting concerns收集起来,设计各个独立可重用的对象,这些对象称之为Aspect,例如在DynamicProxyDemo项目中将登录的动作设计为一个LogHandler类别,LogHandler类别在AOP的术语就是Aspect的一个具体实例,在AOP中着重于Aspect的辨认,将之从业务流程中独立出来,在需要该服务的时候,缝合(Weave)至应用程序之上,不需要服务的时候,也可以马上从应用程序中脱离,应用程序中的可重用组件不用作任何的修改,例如在DynamicProxyDemo项目中的HelloSpeaker所代表的角色就是应用程序中可重用的组件,在它需要记录服务时并不用修改本身的程序代码。
另一方面,对于应用程序中可重用的组件来说,以AOP的设计方式,它不用知道处理提供服务的对象之存在,具体的说,与服务相关的API不会出现在可重用的应用程序组件之中,因而可提高这些组件的重用性,您可以将这些组件应用至其它的应用程序之中,而不会因为目前加入了某些服务而与目前的应用程序框架发生耦合。
- Advice
Aspect的具体实作称之为Advice,以记录的动作而言,Advice中会包括真正的记录程序代码是如何实作的,像是DynamicProxyDemo项目中的LogHandler类别就是Advice的一个具体实例,Advice中包括了Cross-cutting concerns的行为或所要提供的服务。
- Joinpoint
Aspect在应用程序执行时加入业务流程的点或时机称之为Joinpoint,具体来说,就是Advice在应用程序中被呼叫执行的时机,这个时机可能是某个方法被呼叫之前或之后(或两者都有),或是某个例外发生的时候。
- Pointcut
Pointcut是一个定义,藉由这个定义您可以指定某个Aspect在哪些Joinpoint时被应用至应用程序之上。具体的说,您可以在某个定义档中撰写Pointcut,当中说明了哪些Aspect要应用至应用程序中的哪些Joinpoint。
- Target
一个Advice被应用的对象或目标对象,例如DynamicProxyDemo项目中的HelloSpeaker就是LogHandler这个Advice的Target。
- Introduction
对于一个现存的类别,Introduction可以为其增加行为,而不用修改该类别的程序,具体的说,您可以为某个已撰写、编译完成的类别,在执行时期动态加入一些方法或行为,而不用修改或新增任何一行程序代码。
- Proxy
在《Expert One-on-One J2EE Development WIthout EJB》一书中,Rod Johnson、Juergen Hoeller在第八章中有提到,AOP的实作有五个主要的策略: Dynamic Proxies、Dynamic Byte Code Generation、Java Code Generation、Use of a Custon Class Loader、Language Extensions。
在之前静态代理与动态代理中,已经使用实际的程序范例介绍过代理机制的实现,Spring的AOP主要是透过动态代理来完成。
- Weave
Advice被应用至对象之上的过程称之为缝合(Weave),在AOP中缝合的方式有几个时间点:编译时期(Compile time)、类别加载时期(Classload time)、执行时期(Runtime)。
结合DynamicProxyDemo的实例,将以上介绍过的AOP相关名词具体的使用图片来加以表示,有助于您对每一个名词的理解与认识:
1.3 Spring AOP
不同的 AOP 框架会有其对 AOP 概念的不同实作方式,主要的差别在于所提供的 Joinpoints、Aspects 的丰富程度,以及它们如何被缝合(Weave)至应用程式之上(像是 Pointcuts 的定义方式)。
Spring 的 Advices 是用 Java 程式语言来撰写,而不使用特定的 AOP 语言,在定义 Pointcuts 时可 以使用 XML 组态档案,这两者的撰写 对于 Java 开发人员来说都很熟悉,您不必学习特定的语法, 就可以用熟悉的 Java 程式语言与 XML 格式来运用 Spring AOP。
Spring 的 AOP 实作中会实作 AOP Alliance(http://www.sourceforge.net/projects/aopalliance)所规 范的介面,AOP Alliance 是由许多团体所组成的联合计划(Joint project),这些团体对于 AOP 的 实作要求必须遵合所制订出来的介面规范,目的是对 Java 的 AOP 实作介面标准化,以增加 AOP 实作类别在不同的 Java 应用程式之间的可移植性。
Spring 的 Advices 是在执行时期导入至 Targets,您可以让 Targets 实作预先定义好的介面,则 Spring 在执行时期会使用 java.lang.reflect.Proxy 来进行动态代理,如果不实作介面,则 Spring 会使用 CGLIB 为您的 Targets 产生一个子类别作 为代理类别(Proxy classes)。
在 Spring AOP 中,您应该以实作介面的方式为优先,这可以让应用程式的元件彼此之间的耦合 度降低,使用 Proxy classes 的方式,由于必须产生子类别,所以对于被宣告为 final 的方法无法 进行代理,而且这个方式基本上是让一些无法更动原始码的第三方 (Third‐party)类别或是遗 产类别(Legacy classes)来使用。
Spring 的只支援方法的 Joinpoints,也就是 Advices 将在方法呼叫的前后被应用,Spring 不支援 Field 成员的 Jointpoints,这是因为在 Spring 的设计哲学中认为,支援 Field 成员的 Joinpoints 会破坏物 件的封装性。
在Spring2.0中提供了3种实现AOP的方式。
1、实现Spring API的传统方式。
2、基于XML的设置。
3、使用@AspectJ的Annotation支持。