AOP,Aspect Oriented Programming,就是所谓的面向切面编程。这个词语的含义,网上的解释已经很多很多了,理解起来并不怎么难,照葫芦画瓢也能开发出带aop功能的模块出来。开始我也是这样,但只是会用,对它的原理却并没有做过深入地学习。之前一段时间挺闲,于是就打算从头开始认认真真地去研究下aop的原理。于是便有了如下的一套学习笔记,分享出来,只要照着上面的代码敲一遍,相信也会对aop的实现有所掌握。
在开始aop学习之前,先来说下两种动态代理技术:JDK动态代理及CGLib动态代理。aop便是以这两者为基础,实现了切面编程的功能。
何为代理?当一个类被AOP织入增强后,就产出了一个代理类,这个类融合了原类和一些需要添加的增强逻辑。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以我们可以采用调用原类相同的方式调用代理类。
Spring AOP使用了两种代理机制:一种是基于JDK的动态代理;另一种是基于CGLib的动态代理。之所以需要两种代理机制,很大程度上是因为JDK本身只提供接口的代理,而不支持类的代理。下面依次来讲述这两种动态代理的实现。
JDK动态代理
JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中InvocationHandler是一个接口,可以通过实现该接口来定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。下面给出使用示例:
业务接口:
- package proxy.jdk;
- public interface ForumService {
- void removeTopic(int topicId);
- void removeForum(int forumId);
- }
业务接口实现类:
- package proxy.jdk;
- public class ForumServiceImpl implements ForumService {
- @Override
- public void removeTopic(int topicId) {
- System.out.println("模拟删除Topic记录:" + topicId);
- try {
- Thread.sleep(20);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
- @Override
- public void removeForum(int forumId) {
- System.out.println("模拟删除Forum记录:" + forumId);
- try {
- Thread.sleep(40);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
- }
上面的实现类中,只是简单地实现了接口中定义的方法。现在,我们要实现这样的功能:如果这两个方法被调用,则记录下调用日志,并且简单计算下执行时间。如果照正常的逻辑来处理,那应该是类似下面的逻辑代码:
- @Override
- public void removeTopic(int topicId) {
- System.out.println("调用目标对象方法 [removeTopic]开始...");
- long start = System.currentTimeMillis();
- System.out.println("模拟删除Topic记录:" + topicId);
- try {
- Thread.sleep(20);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- long end = System.currentTimeMillis();
- System.out.println("调用目标对象方法[removeTopic]结束.总共花费了: " + (end - start) + "ms");
- }
日志的记录及时间的计算代码包围在核心的业务代码外,并且同样的代码实现需要对removeForum这个方法再写一遍。可想而知,如果有多个方法要处理,这样的重复处理的代码需要写多次。既浪费时间,又使代码变得臃肿。现在,我们通过JDK动态代理技术,来实现上面的功能:
实现了InvocationHandler接口的类:
- package proxy.jdk;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- public class PerformationHandler implements InvocationHandler {
- // 目标业务类
- private Object target;
- public PerformationHandler(Object target) {
- this.target = target;
- }
- /**
- * 将横切逻辑代码与业务代码编织到一起
- */
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- String methodName = target.getClass().getName() + "." +method.getName();
- System.out.println("调用目标对象方法 [" + methodName + "]开始...");
- long start = System.currentTimeMillis();
- // 通过反射调用目标类的目标方法
- Object obj = method.invoke(target, args);
- long end = System.currentTimeMillis();
- System.out.println("调用目标对象方法[" + methodName + "]结束.总共花费了: " + (end - start) + "ms");
- return obj;
- }
- }
InvocationHandler接口定义了一个invoke()方法,proxy是最终生成的代理实例,一般不会用到;method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是通过被代理实例某一方法的入参,在方法反射调用时使用。
按照我们正常调用的逻辑,实现代码应该如下:
- // 正常的业务实现
- ForumService forumService = new ForumServiceImpl();
- forumService.removeTopic(10);
- forumService.removeForum(13);
但现在我们使用了动态代理,因此就通过代理来调用。
通过Proxy结合PerformationHandler创建业务接口的代理实例:
- // 通过动态代理来实现
- ForumService target = new ForumServiceImpl();
- PerformationHandler handler = new PerformationHandler(target);
- ForumService proxy = (ForumService) Proxy.newProxyInstance(target.getClass().getClassLoader(), target
- .getClass().getInterfaces(), handler);
- proxy.removeForum(10);
- proxy.removeTopic(12);
通过Proxy的newProxyInstance()静态方法为混合了业务处理逻辑和性能监视逻辑的handler创建一个符合ForumService接口的代理实例。该方法的第一个入参为类加载器;第二个入参为创建代理所需要实现的一组接口;第三个参数是整合了业务逻辑和性能监视逻辑(这里即所谓的横切逻辑)的编织器对象。生成的代理实例实现了目标业务类的所有接口,即ForumServiceImpl的ForumService接口。这样,我们就可以按照调用ForumService接口实现相同的方式调用代理实例。运行以上代码,输出结果如下:
调用目标对象方法 [proxy.jdk.ForumServiceImpl.removeForum]开始...
模拟删除Forum记录:10
调用目标对象方法[proxy.jdk.ForumServiceImpl.removeForum]结束.总共花费了: 40ms
调用目标对象方法 [proxy.jdk.ForumServiceImpl.removeTopic]开始...
模拟删除Topic记录:12
调用目标对象方法[proxy.jdk.ForumServiceImpl.removeTopic]结束.总共花费了: 20ms
模拟删除Forum记录:10
调用目标对象方法[proxy.jdk.ForumServiceImpl.removeForum]结束.总共花费了: 40ms
调用目标对象方法 [proxy.jdk.ForumServiceImpl.removeTopic]开始...
模拟删除Topic记录:12
调用目标对象方法[proxy.jdk.ForumServiceImpl.removeTopic]结束.总共花费了: 20ms
我们发现,程序的运行效果和直接在业务类中编写性能监视逻辑的效果一致,但是在这里,原来分散的横切逻辑代码已经被抽取到PerformationHandler中,当调用代理对象的removeForum()和removeTopic()方法时,便调用了invoke()方法。