AOP是Aspect Oriented Programing的简称,面向切面编程。AOP适合于那些具有横切逻辑的应用:如性能监测,访问控制,事务管理以及日志记录。AOP将这些分散在各个业务逻辑中的代码通过横向切割的方式抽取到一个独立的模块中。
一、AOP术语
**1.连接点(Joinpoint)**
程序执行的某个特定位置:如类开始初始化之前、类初始化之后、类某个方法调用前、调用后等;一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就成为“连接点”,Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后以及方法调用前后的这些程序执行点织入增强。比如:黑客攻击系统需要找到突破口,没有突破口就没有办法攻击,从某种程度上来说,AOP就是一个黑客,连接点就是AOP向目标类攻击的候选点。
连接点有两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位;如在Test.foo()方法执行前的连接点,执行点为Test.foo,方位为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中定义。
**2.切点(Pointcut)**
每个程序类都拥有许多连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。但在为数众多的连接点钟,如何定位到某个连接点上呢?AOP通过切点定位特定连接点。通过数据库查询的概念来理解切点和连接点:连接点相当于数据库表中的记录,而切点相当于查询条件。连接点和切点不是一一对应的关系,一个切点可以匹配多一个连接点。
在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点;其实确切的说应该是执行点而非连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息。
**3.增强(Advice)**
增强是织入到目标类连接点上的一段程序代码(好比AOP以黑客的身份往业务类中装入木马),增强还拥有一个和连接点相关的信息,这边是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点了,所以Spring提供的增强接口都是带方位名的:BefortAdvice、AfterReturningAdvice、ThrowsAdvice等。(有些将Advice翻译为通知,但通知就是把某个消息传达给被通知者,并没有为被通知者做任何事情,而Spring的Advice必须嵌入到某个类的连接点上,并完成了一段附加的应用逻辑;)
**4.目标对象(Target)**
增强逻辑的织入目标类,如果没有AOP,目标业务类需要自己实现所有逻辑,在AOP的帮助下,目标类只实现那些非横切逻辑的程序逻辑,而其他监测代码则可以使用AOP动态织入到特定的连接点上。
**5.引介(Introduction)**
引介是一种特殊的增强,它为类添加一些属性和方法,这样即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态的为该业务类添加接口的实现逻辑,让这个业务类成为这个接口的实现类。
**6.织入(Weaving)**
织入是将增强添加到目标类具体连接点上的过程,AOP就像一台织布机,将目标类、增强或者引介编织到一起,AOP有三种织入的方式:
a.编译期间织入,这要求使用特殊的Java编译器;
b.类装载期织入,这要求使用特殊的类装载器;
c.动态代理织入,在运行期为目标类添加增强生成子类的方式。
Spring采用动态代理织入,而AspectJ采用编译器织入和类装载期织入。
**7.代理(Proxy)**
一个类被AOP织入增强后,就产生出了一个结果类,它是融合了原类和增强逻辑的代理类。
**8.切面(Aspect)**
切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
总结:AOP的工作重点就是如何将增强应用于目标对象的连接点上,这里首先包括两个工作:第一,如何通过切点和增强定位到连接点;第二,如何在增强中编写切面的代码。
二、AOP实例(通过Proxy代理模式)
Spring AOP使用纯java实现,不需要专门的编译过程和类装载器,它在运行期间通过代理方式向目标类织入增强代码,它更侧重于提供一种和Spring IoC容器整合的AOP实现,在Spring中,我们可以无缝的将AOP,IoC,AspectJ整合在一起。
Spring AOP使用了两种代理机制:一种是基于JDK的动态代理,一种是基于CGLib的动态代理;
JDK1.3以后,java提供了动态代理技术,允许开发者在运行期间动态的创建接口的代理实例,JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler,其中InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态的将横切逻辑和业务逻辑编织在一起。
下面我们来看一个JDK动态代理的例子:
1.业务接口UserService.java
package spring.aop.demo1;
public interface UserService {
void removeUser(int userId);
}
2.横切逻辑代理监视代码PerformanceMonitor.java
package spring.aop.demo1;
public class MethodPerformance {
private long begin;
private long end;
private String serviceMethod;
public MethodPerformance(String serviceMethod) {
this.serviceMethod = serviceMethod;
this.begin = System.currentTimeMillis();
}
public void printPerformance() {
this.end = System.currentTimeMillis();
long elapse = end - begin;
System.out.println(serviceMethod + "花费" + elapse + "毫秒");
}
}
package spring.aop.demo1;
public class PerformanceMonitor {
// 通过一个ThreadLocal保存调用线程相关的性能监视信息
private static ThreadLocal<MethodPerformance> performanceRecord = new ThreadLocal<MethodPerformance>();
// 启动对一目标方法的性能监视
public static void begin(String method) {
System.out.println("begin monitor...");
MethodPerformance mp = new MethodPerformance(method);
performanceRecord.set(mp);
}
public static void end() {
System.out.println("end monitor...");
MethodPerformance mp = performanceRecord.get();
mp.printPerformance();
}
}
3.横切逻辑代理代码PerformanceHandler.java
package spring.aop.demo1;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class PerformanceHandler implements InvocationHandler {
private Object target;
public PerformanceHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object arg0, Method arg1, Object[] arg2)
throws Throwable {
PerformanceMonitor.begin(target.getClass().getName() + "."
+ arg1.getName());
Object obj = arg1.invoke(target, arg2);// 通过反射机制调用目标对象的方法
PerformanceMonitor.end();
return obj;
}
}
首先,我们实现InvocationHandler接口,该接口定义了一个invoke方法,proxy最是最终生成的一个代理实例,一般不会用到,参数arg1是被代理目标实例的某个具体的方法,通过它可以发起目标实例方法的反射调用;参数arg2是通过被代理实例某一个方法的入参,在方法反射调用时候使用,通过代理将横切逻辑代码和业务类的代码编织到了一起。我们在构造函数里通过target传入希望被代理的目标对象,将目标实例产地个method.inoke(),调用目标实例的方法。
4.通过Proxy结合PerformanceHandler创建UserService接口的代理实例:
package spring.aop.demo1;
import java.lang.reflect.Proxy;
public class UserServiceImpl implements UserService {
@Override
public void removeUser(int userId) {
System.out.println("模拟删除用户:" + userId);
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
// 将目标业务类和横切代码编织到一起
PerformanceHandler handler = new PerformanceHandler(userService);
// 根据编织了目标业务类逻辑和性能监视横切逻辑的InvocationHandler实例创建代理实例
UserService proxy = (UserService) Proxy.newProxyInstance(userService
.getClass().getClassLoader(), userService.getClass()
.getInterfaces(), handler);
proxy.removeUser(3);
}
}
输出:
begin monitor…
模拟删除用户:3
end monitor…
spring.aop.demo1.UserServiceImpl.removeUser花费203毫秒
说明:上面的代码完成业务类代码和横切代码的编制工作,并生成了代理实例,newProxyInstance方法的第一个参数为类加载器,第二个参数为目标类所实现的一组接口,第三个参数是整合了业务逻辑和横切逻辑的编织器对象。使用JDK代理模式有一个限制,即它只能为接口创建代理实例,这一点我们可以从Proxy.newProxyInstance的方法签名中就可以看的很清楚,第二个参数interfaces就是需要代理实例实现的接口列表。
CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的结束拦截所有父类方法的调用,并顺势织入横切逻辑。我们采用CGLib技术可以编写一个可以为任何类创建织入横切逻辑代理对象的代理创建器,下面看一个使用CGLib代理技术实现横切的一个例子:
1.CglibProxy.java
package spring.aop.demo2;
import java.lang.reflect.Method;
import spring.aop.demo1.PerformanceMonitor;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor {
// private static CglibProxy proxy = new CglibProxy();
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz);// 设置需要创建子类的类
enhancer.setCallback(this);
return enhancer.create();// 通过字节码技术动态创建子类实例
}
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2,
MethodProxy arg3) throws Throwable {
PerformanceMonitor.begin(arg0.getClass().getName() + "."
+ arg1.getName());
Object result = arg3.invokeSuper(arg0, arg2);
PerformanceMonitor.end();
return result;
}
}
2.UserServiceImpl.java
package spring.aop.demo2;
public class UserServiceImpl{
public void removeUser(int userId) {
System.out.println("模拟删除用户:" + userId);
}
public void addUser(int userId) {
// TODO Auto-generated method stub
}
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
UserServiceImpl userService =(UserServiceImpl)proxy.getProxy(UserServiceImpl.class);
userService.removeUser(7);
}
}
输出:
begin monitor…
模拟删除用户:7
end monitor…
spring.aop.demo2.UserServiceImpl
4d9bdf63.removeUser花费15毫秒
总结:用户通过getProxy(Class clazz)为一个类创建动态代理对象,该代理对象通过扩展clazz创建代理对象,在这个代理对象中,我们织入横切逻辑代码。intercept是CGLib定义的Interceptor接口的方法,它拦截所有目标方法的调用,参数arg0表示目标类的实例;参数arg1表示目标类方法的反射对象;arg2表示目标类方法的参数的反射对象;arg3表示代理类实例;我们看到输出spring.aop.demo2.UserServiceImpl4d9bdf63.removeUser,这个特殊的类就是CGLib为UserService动态创建的子类。Spring AOP的底层就是通过代理(JDK动态代理或CGlib代理)来实现AOP的,但是这种实现方式存在三个明显需要改进的地方:
a.目标类的所有方法都添加了横切逻辑,而有时,这并不是我们所期望的,我们可能只希望对业务类中的某些特定的方法添加横切逻辑;
b.我们通过硬编码的方式制定了织入横切逻辑的织入点,即在目标业务方法的开始和结束前织入代码;
c.我们手工编写代理实例的创建过程,为不同类创建代理时,需要分别编写相应的创建代码,无法做到通用;
CGLib所创建的动态代理对象的性能比JDK的高大概10倍,但CGLib在创建代理对象的时间比JDK大概多8倍,所以对于singleton的代理对象或者具有实例池的代理,因为无需重复的创建代理对象,所以比较适合CGLib动态代理技术,反之选择JDK代理。值得一提的是由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中final的方法进行代理。
三、创建增强类
Spring使用增强定义横切逻辑,同时由于Spring只支持方法连接点,增强还包括了在方法的哪一点加入横切代码的方位信息,所以增强既包含横切逻辑,还包含部分连接点的信息。
前置增强:org.springframework.aop.BeforeAdvice 代表前置增强,因为Spring只支持方法级的增强,所以MethodBeforeAdvice是目前可用的前置增强,表示在目标方法执行前实施增强,而BeforeAdvice是为了将来版本扩展需要而定义的,下面我们看一个前置通知的小例子:
1.Waiter.java
package spring.aop.beforeadvicedemo;
public interface Waiter {
void greetTo(String name);
void serverTo(String name);
}
2.GreetingBeforeAdvice.java 实现前置增强接口的横切逻辑
package spring.aop.beforeadvicedemo;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
//arg0是目标类的方法,arg1是目标类方法的参数,arg2是目标类的实例
@Override
public void before(Method arg0, Object[] arg1, Object arg2)
throws Throwable {
String clientName = (String)arg1[0];
System.out.println("How are you! Mr." + clientName);
}
}
3.目标类NaiveWaiter.java和测试代码
package spring.aop.beforeadvicedemo;
import org.springframework.aop.BeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;
public class NaiveWaiter implements Waiter {
@Override
public void greetTo(String name) {
System.out.println("great to " + name);
}
@Override
public void serverTo(String name) {
System.out.println("serving "+ name);
}
public static void main(String[] args) {
BeforeAdvice advice = new GreetingBeforeAdvice();
Waiter waiter = new NaiveWaiter();
//Spring提供的代理工厂
ProxyFactory pf = new ProxyFactory();
//设置代理目标
pf.setTarget(waiter);
//为代理目标添加增强
pf.addAdvice(advice);
//生成代理实例
Waiter waiterProxy = (Waiter)pf.getProxy();
waiterProxy.greetTo("nicholaslee");
waiterProxy.serverTo("nicholaslee");
}
}
输出:
How are you! Mr.nicholaslee
great to nicholaslee
How are you! Mr.nicholaslee
serving nicholaslee
说明:在测试代码中,我们用到了org.springframework.aop.framework.ProxyFactory,这个内部就是使用了我们之前的JDK代理或者CGLib代理的技术,将增强应用到目标类中。Spring定义了org.springframework.aop.framework.AopProxy接口,并提供了两个final的实现类,其中:
Cglib2AopProxy使用CGLib代理技术创建代理,而JdkDynamicAopProxy使用JDK代理技术创建代理;
如果通过ProxyFactory的setInterfaces(Class[] interfaces)指定针对接口进行代理,ProxyFactory就使用JdkDynamicAopProxy;
如果是通过类的代理则使用Cglib2AopProxy,另外也可以通过ProxyFactory的setOptimize(true)方法,让ProxyFactory启动优化代理模式,这样针对接口的代理也会使用Cglib2AopProxy。
BeforeAdvice advice = new GreetingBeforeAdvice();
Waiter waiter = new NaiveWaiter();
//Spring提供的代理工厂
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(waiter.getClass().getInterfaces());
以上代码就指定了JdkDynamicAopProxy进行代理;
BeforeAdvice advice = new GreetingBeforeAdvice();
Waiter waiter = new NaiveWaiter();
//Spring提供的代理工厂
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(waiter.getClass().getInterfaces());
pf.setOptimize(true);
以上代码虽然指定了代理的接口,但由于setOptimize(true),所以还是使用了Cglib2AopProxy代理;
我们使用了addAdvice来添加一个增强,用户可以用该方法添加多个增强,形成一个增强链,调用顺序和添加顺序一致,下标从0开始:
package spring.aop.beforeadvicedemo;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class GreetingBeforeAdvice2 implements MethodBeforeAdvice {
@Override
public void before(Method arg0, Object[] arg1, Object arg2)
throws Throwable {
System.out.println( "我是第一个横切逻辑");
}
}
public static void main(String[] args) {
BeforeAdvice advice = new GreetingBeforeAdvice();
BeforeAdvice advice2 = new GreetingBeforeAdvice2();
Waiter waiter = new NaiveWaiter();
//Spring提供的代理工厂
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(waiter.getClass().getInterfaces());
pf.setOptimize(true);
//设置代理目标
pf.setTarget(waiter);
//为代理目标添加增强
pf.addAdvice(0,advice2);
pf.addAdvice(1,advice);
//生成代理实例
Waiter waiterProxy = (Waiter)pf.getProxy();
waiterProxy.greetTo("nicholaslee");
waiterProxy.serverTo("nicholaslee");
}
输出:
我是第一个横切逻辑
How are you! Mr.nicholaslee
great to nicholaslee
我是第一个横切逻辑
How are you! Mr.nicholaslee
serving nicholaslee
我们还可以将以上代码更加优化一下,可以通过依赖注入来实例化:
模拟删除用户:0
method:removeUser
抛出异常:运行异常。
成功回滚事务。
添加用户1
method:addUser
抛出异常:数据库插入异常。
成功回滚事务。
也可以配置注入方式: