AOP(面向切面编程)是OOP的有益补充,它只适合那些具有横切逻辑的应用场合,如性能监测,访问控制,事物管理,日志记录等。至于怎么理解横切逻辑,敲完实例代码也就明白了。
为什么要使用AOP,举个栗子:需要监测一些方法的执行所消耗的时间,在每个方法开始执行前调用一次记录时间的方法beginTime,在每个方法执行结束后调用一次记录时间的方法endTime,再endTime-beginTime就是该方法所消耗的时间。这样要为每个需要监测的方法都加上这两个记录时间的方法。这样重复的监测代码就与业务逻辑代码混杂在一起了,并且无法通过抽象父类的方式消除这样的重复性横切代码。AOP就是为此而生的,将这样的重复性代码抽取出来,再动态的将这些代码切入到指定的执行点,实现同样的效果。
一、AOP关键词
1、目标对象—Target:需要动态切入代码的目标类。
2、连接点—Joinpoint:Spring只支持方法的连接点,方法调用前,方法调用后,方法调用前后(环绕),方法抛出异常后。连接点是类中客观存在的。每个方法都有这4个连接点。选择在哪个连接点切入代码就是下面的“切点”了。
3、切点—Pointcut:选择了某个连接点切入代码,这个连接点就是切点。例如准备在方法执行前记录一次时间(切入并执行一次记录时间的代码),那么方法执行前这个连接点就是切点。
4、增强—Advice:增强是织入切点的一段程序代码。BeforeAdvice(方法调用前),AfterReturningAdvice(访问返回后的位置),ThrowsAdvice(方法抛出异常的位置)。结合切点和增强才能实施增强逻辑。
5、引介—Introduction:一种特殊的增强,可以为类添加一些属性和方法。可以动态的为目标类添加接口的实现逻辑,让目标类成为这个接口的实现类。
6、织入—Weaving:织入是将增强添加到目标类的具体连接点上的过程。
7、代理—Proxy:目标类被织入增强后,就会产生一个结果类,它是融合了目标类和增强逻辑的代理类。可以采用调用原类相同的方式调用代理类。
8、切面—Aspect:切面由切点和增强/引介组成,包括横切逻辑的定义和连接点定义。
实现AOP有许多工具:AspectJ、AspectWerkz、JBossAOP、SpringAop,这里就学习SpringAOP。
**需求:对某个目标业务类的方法进行性能监测,打印方法执行的时间。**通过代理的方式将业务类方法中开启和结束性能监测的横切代码从业务类中移除,通过JDK或CGLib动态代理技术将横切代码动态织入目标方法的相应位置。
Spring AOP支持JDK动态代理和CGLib动态代理实现AOP,区别于使用场景下文会讨论。
二、JDK动态代理实现AOP
CGLib的Maven依赖
<!-- cglib依赖(spring依赖) -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.0</version>
<exclusions>
<exclusion>
<artifactId>asm</artifactId>
<groupId>org.ow2.asm</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
Java提供了动态代理技术,允许开发者在运行期创建接口的代理实例。主要涉及java.lang.reflect包中的两个类:Proxy和InvocationHandler。
1、目标类需要实现的接口 — ForumService
package com.mistra.aop;
/**
* @author Mistra-WangRui
* @create 2018-03-28 0:15
* @desc 定义接口
*/
public interface ForumService {
//两个模拟方法
public void removeTopic(int topicId);
public void removeForum(int forumId);
}
2、目标类 — ForumServiceImpl
package com.mistra.aop;
/**
* @author Mistra-WangRui
* @create 2018-03-28 0:17
* @desc 目标类
*/
public class ForumServiceImpl implements ForumService {
public void removeTopic(int topicId) {
System.out.println("模拟删除Topic记录:" + topicId + "条。");
try {
Thread.currentThread().sleep(200);
}catch (Exception e){
throw new RuntimeException();
}
}
public void removeForum(int forumId) {
System.out.println("模拟删除Forum记录:" + forumId + "条。");
try {
Thread.currentThread().sleep(40);
}catch (Exception e){
throw new RuntimeException();
}
}
}
目标类—需要织入增强代码的类,只编写具体的业务逻辑代码,性能监测的记录时间的代码通过AOP动态织入。
3、记录性能监测信息的类 — MethodPerformance
package com.mistra.aop;
/**
* @author Mistra-WangRui
* @create 2018-03-28 0:23
* @desc 记录性能监视信息
*/
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(){
end = System.currentTimeMillis();//记录目标类方法执行完毕后的系统时间
long elapse = end - begin;
System.out.println(serviceMethod+"耗时"+elapse+"毫秒。");
}
}
4、性能监测实现类 — PerfomanceMonitor
package com.mistra.aop;
/**
* @author Mistra-WangRui
* @create 2018-03-28 0:22
* @desc 性能监视实现类
*/
public class PerfomanceMonitor {
//通过一个ThreadLocal保存于调用线程相关的性能监视信息----保证线程安全(查阅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();//打印方法性能监视的结果
}
}
5、性能监测横切代码 — PerformanceHandler
package com.mistra.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author Mistra-WangRui
* @create 2018-03-28 0:21
* @desc 性能监视横切代码
*/
public class PerformanceHandler implements InvocationHandler {
private Object target;
public PerformanceHandler(Object target){//target为目标业务类
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
PerfomanceMonitor.begin(target.getClass().getName()+"."+method.getName());
Object obj = method.invoke(target,args);//通过Java反射方法调用业务类的目标方法
PerfomanceMonitor.end();
return obj;
}
}
在构造函数里传入希望被代理的目标类。
InvocationHandler只有一个invoke方法,就是通过此方法将横切逻辑代码和业务类方法的业务逻辑代码编织到一起。proxy是最终生成的代理类实例(一般不会用到),method是被代理目标类的某个具体方法,args是method的入参。
PerfomanceMonitor.begin(),PerfomanceMonitor.end()方法就是性能监测的横切代码了。
method.invoke(target,args)通过Java反射方法调用目标类的目标方法。
6、创建代理实例 — ForumServiceTest
package com.mistra.aop;
import org.testng.annotations.Test;
import java.lang.reflect.Proxy;
/**
* @author Mistra-WangRui
* @create 2018-03-28 0:35
* @desc 创建代理实例
*/
public class ForumServiceTest {
@Test
public void proxy(){
ForumService target = new ForumServiceImpl();//被代理的目标类
PerformanceHandler handler = new PerformanceHandler(target);//将目标类与横切代码编织到一起
ForumService proxy = (ForumService) Proxy.newProxyInstance(//创建代理实例
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler);
proxy.removeForum(10);//调用代理实例的方法
proxy.removeTopic(1012);
}
}
new PerformanceHandler(target):将性能监测横切逻辑编织到ForumService实例中。
Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler):通过Proxy的newProxyInstance()静态方法为编织了业务逻辑和性能监测逻辑的handler创建一个符合ForumService接口的代理实例。第一个入参是类加载器,第二个入参为目标业务类(要被代理的对象,这里就是ForumServiceImpl)实现的接口列表,第三个入参是整合了业务逻辑和横切逻辑的编织对象。
这个代理实例proxy实现了目标业务类(ForumServiceImpl)所实现的所有接口,这里就一个,即ForumService。就可以按照调用ForumService接口实例相同的方式调用代理实例。
运行proxy(),输出一下信息,成功在方法前后进行了横切逻辑织入:
三、CGLib动态代理实现AOP
1、性能监测横切代码 — CglibProxy
package com.mistra.aop;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author Mistra-WangRui
* @create 2018/3/28 14:41
* @desc CGLib代理
*/
public class CglibProxy implements MethodInterceptor{
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
enhancer.setSuperclass(clazz);//设置需要创建子类的类
enhancer.setCallback(this);//设置回调
return enhancer.create();//通过字节码技术冬天创建子类实例
}
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {//拦截父类所有方法的调用
PerfomanceMonitor.begin(o.getClass().getName()+"."+method.getName());
Object result=methodProxy.invokeSuper(o,objects);//通过代理类调用父类中的方法
PerfomanceMonitor.end();
return result;
}
}
通过getProxy()为一个目标类创建动态代理对象。
intercept()方法会拦截所有目标类方法的调用。o是目标类实例,method是目标类方法的反射对象,objects是方法的入参,methodProxy是代理类实例。
会拦截父类所有方法的调用,都执行横切逻辑织入。
2、创建代理实例 — ForumServiceTest2
package com.mistra.aop;
import org.testng.annotations.Test;
/**
* @author Mistra-WangRui
* @create 2018/3/28 14:56
* @desc CGLib代理创建代理对象实例
*/
public class ForumServiceTest2 {
@Test
public void proxy(){
CglibProxy proxy = new CglibProxy();
ForumServiceImpl forumService = (ForumServiceImpl)proxy.getProxy(ForumServiceImpl.class);
forumService.removeForum(10);
forumService.removeTopic(1023);
}
}
通过proxy.getProxy()方法为ForumServiceImpl 创建了一个织入了性能监测逻辑的代理对象,并调用代理类的业务方法。
输出信息:
图中的 com.mistra.aop.ForumServiceImpl$
E
n
h
a
n
c
e
r
B
y
C
G
L
I
B
EnhancerByCGLIB
EnhancerByCGLIB$74e237b4 就是CGLib动态创建的子类。
四、JDK动态代理和CGLib动态代理的区别与使用场景
1、JDK动态代理只能创建接口对象的代理实例,CGLib动态代理能创建接口对象和普通类的代理实例。
2、CGLib创建的动态代理对象的性能比JDK动态代理创建的动态代理对象性能高。
3、但CGLib创建代理对象所耗费的时间比JDK动态代理多。
对于singleton的代理对象或者具有实例池的代理,因为不用频繁的创建代理对象,所以比较适合用CGLib动态代理技术,反之则适合采用JDK动态代理技术。
五、缺点
1、使用JDK动态代理和CGLib动态代理的话,目标类的所有方法都添加了性能监测横切逻辑,有时候只想对部分方法添加横切逻辑。
2、手工编写代理实例的创建过程,为不同的目标类创建代理时无法通用。
3、通过硬编码的方式指定织入横切逻辑的位置。
下篇文章介绍了更方便快捷的方式: Spring AOP之—基于ProxyFactory的类编码方式和XML配置方式实现AOP