(spring-第16回【AOP基础篇】)基本概念

  1. AOP(Aspect Oriented Programing),面向切面方程。介绍具体定义前,先看一个例子:
     1 package com.baobaotao.concept;
     2 
     3 public class ForumService {
     4     private TransactionManager transManager;
     5     private PerformanceMonitor pmonitor;
     6     private TopicDao topicDao;
     7     private ForumDao forumDao;
     8 
     9     public void removeTopic(int topicId) {
    10         pmonitor.start();
    11         transManager.beginTransaction();
    12 
    13         topicDao.removeTopic(topicId);
    14 
    15         transManager.endTransaction();
    16         pmonitor.end();
    17     }
    18     public void CreateForum(Forum forum) {
    19         pmonitor.start();
    20         transManager.beginTransaction();
    21         
    22         forumDao.create(forum);
    23         
    24         transManager.endTransaction();
    25         pmonitor.end();
    26     }
    27 }

    上面代码中,10、11、15、16行和19、20、24、25行是重复的。这是一个监视程序,ForumService 中真正工作的方法是第13行和第22行。我们希望把重复的代码逻辑从原来的方法中抽离出来变成一个独立的模块,而工作类被抽离后变成下面这样:

     1 package com.baobaotao.concept;
     2 
     3 public class ForumService {
     4 
     5     private TopicDao topicDao;
     6     private ForumDao forumDao;
     7 
     8     public void removeTopic(int topicId) {
     9 //--------------------------------------------
    10         topicDao.removeTopic(topicId);
    11 //--------------------------------------------
    12     }
    13     public void CreateForum(Forum forum) {
    14 //--------------------------------------------
    15         forumDao.create(forum);
    16 //---------------------------------------------      
    17     }
    18 }

    当第8行或第15行开始工作的时候,又能够把刚才抽离出来的独立的模块插进去,这就是AOP的任务。它就像一个切面的树的年轮,而一层层的圆环就是要抽离出来的东西,真正工作的代码就是圆心。我们把圆环抽出来,在圆心工作的时候,再把圆环插进去。所以,AOP名曰:面向切面方程。

  2. 基本概念
    • 连接点(JoinPoint):抽离后方法开始执行时,第10行上方、第10行下方,第15行上方,第15行下方都需要插入抽离的代码,那么,"第10行上方"、“第10行下方”,“第15行上方”、“第15行下方”这些具有方向性质的点都是连接点。连接点由两个元素确定,即:“第10行(要执行的代码点)”+“上方(方向定位)”。就是代码中红线的地方。
    • 切点(PointCut):AOP使用切点来定位连接点。也就是说,切点是用来寻找连接点的方式。
    • 增强(Advice):就是程序代码执行时要插入代码上下方的抽离代码,就是上例中抽离出去的10、11、15、16行重复代码。
    • 目标对象(Target):就是"增强"要插入的类。在上例中,就是ForumService。
    • 引介(Introduction):一种特殊的增强。一般的增强是在类的方法上下插入增强代码,而引介是为该类增加一些属性和方法(通过增加属性、方法的方式让目标对象功能强大起来)。
    • 织入(Weaving):把增强代码插入目标对象的过程就是织入。织入分为编译期织入、类加载期织入、动态代理织入。
      • 编译期织入。
      • 类加载期织入。
      • 动态代理织入:在运行期,为目标类插入增强,生成子类。(也就是说,插入增强代码后,生成一个子类,由子类来执行父类的方法)。Spring采用动态代理织入。而AspectJ采用编译期织入和类加载期织入。
    • 代理(Proxy):AOP织入增强代码后,生成一个新类,这个新类就是代理。
    • 切面(Aspect):切点和增强合起来就是切面。
  3. AOP的工作重心:把增强代码插入到目标类的连接点上。包括两个工作:
    • 怎样通过切点和增强定位到连接点上。
    • 怎样在增强中编写切面的代码。
  4. Spring的AOP使用了两种机制实现面向切面方程:
    • 基于JDK的动态代理(只提供接口的代理,不支持类的代理)。
    • 基于CGLib的动态代理。
  5. 具体例子(未使用AOP之前)(下面提到的”性能监视“只是一个”增强“的功能例子,并非AOP的专有名词。)。
    • 下面是”目标类“的代码:
       1 package com.baobaotao.proxy;
       2 
       3 public class ForumServiceImpl implements ForumService {
       4 
       5     public void removeTopic(int topicId) {
       6         PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeTopic");
       7         System.out.println("模拟删除Topic记录:"+topicId);
       8         try {
       9             Thread.currentThread().sleep(20);
      10         } catch (Exception e) {
      11             throw new RuntimeException(e);
      12         }        
      13         PerformanceMonitor.end();
      14     }
      15 
      16     public void removeForum(int forumId) {
      17         PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeForum");
      18         System.out.println("模拟删除Forum记录:"+forumId);
      19         try {
      20             Thread.currentThread().sleep(40);
      21         } catch (Exception e) {
      22             throw new RuntimeException(e);
      23         }        
      24         PerformanceMonitor.end();
      25     }
      26 }

      第6行和第13行是一个性能监视功能的简单调用。上面的代码很简单,两个方法都有着相同的性能监视代码(可用作增强的代码),而之间包裹着具体的打印代码。

    • 下面是性能监视的简单实现类(使用了ThreadLocal保证线程安全)。

       1 package com.baobaotao.proxy;
       2 
       3 public class PerformanceMonitor {
       4     private static ThreadLocal<MethodPerformace> performaceRecord = new ThreadLocal<MethodPerformace>();
       5     public static void begin(String method) {
       6         System.out.println("begin monitor...");
       7         MethodPerformace mp = performaceRecord.get();
       8         if(mp == null){
       9             mp = new MethodPerformace(method);
      10             performaceRecord.set(mp);
      11         }else{
      12             mp.reset(method);    
      13         }
      14     }
      15     public static void end() {
      16         System.out.println("end monitor...");
      17         MethodPerformace mp = performaceRecord.get();
      18         mp.printPerformace();
      19     }
      20 }

      从性能监视类可以看出,里面用到了一个记录性能监视信息的辅助类。

    • 下面是记录性能监视信息的类:

       1 package com.baobaotao.proxy;
       2 
       3 public class MethodPerformace {
       4     private long begin;
       5     private long end;
       6     private String serviceMethod;
       7     public MethodPerformace(String serviceMethod){
       8         reset(serviceMethod);
       9     }
      10     public void printPerformace(){
      11         end = System.currentTimeMillis();
      12         long elapse = end - begin;
      13         System.out.println(serviceMethod+"花费"+elapse+"毫秒。");
      14     }
      15     public void reset(String serviceMethod){
      16         this.serviceMethod = serviceMethod;
      17         this.begin = System.currentTimeMillis();
      18     }
      19 }
    • 从代码中可以看到,ForumServiceImpl 在执行第6行的时候,把removeTopic的方法名传给了性能监视类PerformanceMonitor,而 PerformanceMonitor在调用begin方法时,把removeTopic的方法名又传给了MethodPerformace类。而传过来的唯一作用就是在 PerformanceMonitor执行end()方法时,利用MethodPerformace打印出该方法名。
    • 下面是测试类:
       1 package com.baobaotao.proxy;
       2 
       3 public class TestForumService {
       4     public static void main(String[] args) {
       5     
       6         ForumService forumService = new ForumServiceImpl();
       7         forumService.removeForum(10);
       8         forumService.removeTopic(1012);
       9     }
      10 }
    • 打印结果:
      begin monitor...
      模拟删除Topic记录:1012
      end monitor...
      com.baobaotao.proxy.ForumServiceImpl.removeTopic花费20毫秒。
    • 整个代码很容易理解。很明显,根据目标类的6、13、17、24行可以看出,这需要使用AOP来抽离代码。而这个时候就可以使用JDK动态代理了。
  6. 使用JDK动态代理改造
    • 首先,把目标类的重复代码(可增强的代码)抽离出来,变成下面这样:
       1 package com.baobaotao.proxy;
       2 
       3 public class ForumServiceImpl implements ForumService {
       4 
       5     public void removeTopic(int topicId) {
       6 //        PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeTopic");
       7         System.out.println("模拟删除Topic记录:"+topicId);
       8         try {
       9             Thread.currentThread().sleep(20);
      10         } catch (Exception e) {
      11             throw new RuntimeException(e);
      12         }        
      13 //        PerformanceMonitor.end();
      14     }
      15 
      16     public void removeForum(int forumId) {
      17 //        PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeForum");
      18         System.out.println("模拟删除Forum记录:"+forumId);
      19         try {
      20             Thread.currentThread().sleep(40);
      21         } catch (Exception e) {
      22             throw new RuntimeException(e);
      23         }        
      24 //        PerformanceMonitor.end();
      25     }
      26 }

      我把重复代码抽离掉了。

    • 抽离出来的功能不能扔掉,总要有个栖居之地。java.lang.reflect.InvocationHandler就是放抽离代码的地方:
       1 package com.baobaotao.proxy;
       2 
       3 import java.lang.reflect.InvocationHandler;
       4 import java.lang.reflect.Method;
       5 /**实现InvocationHandler*/
       6 public class PerformaceHandler implements InvocationHandler {
       7     private Object target;
       8     public PerformaceHandler(Object target){
       9         this.target = target;
      10     }
      11     /**重写invoke方法
      12      * @param proxy 最终生成的代理实例(一般不会用到)
      13      * @param method 是原始目标类(即,被代理类)的某个方法(比如removeForum),通过它,利用反射,来调用目标类的该方法。
      14      * @param args 是method方法需要的一组入参。
      15      * */
      16     public Object invoke(Object proxy, Method method, Object[] args)
      17             throws Throwable {
      18         /**只要给了target类和要调用的method方法,就可以用下面的语句给begin传入具体的方法名*/
      19         PerformanceMonitor.begin(target.getClass().getName()+"."+ method.getName());
      20         //用method方法使用反射技术调用目标类的具体方法。
      21         Object obj = method.invoke(target, args);
      22         PerformanceMonitor.end();
      23         return obj;
      24     }
      25 }

      上面代码就很好的把抽离的代码(增强)与目标类的具体方法(连接点)交织了起来。

    • 下面是创建代理实例来调用目标类的具体方法(根据前面的思路,并不是在目标类现有的基础上强加上增强代码,而是拷贝一份目标类(生成代理),然后把编织过的代码赋予代理类):
       1 package com.baobaotao.proxy;
       2 
       3 import java.lang.reflect.Proxy;
       4 
       5 public class TestForumService {
       6     public static void main(String[] args) {
       7         // 使用JDK动态代理
       8         ForumService target = new ForumServiceImpl();
       9         //新建PerformaceHandler对象,具体的交织过程已经在PerformaceHandler类中加了注释解读。
      10         PerformaceHandler handler = new PerformaceHandler(target);
      11         /**下面这一行,生成代理,有三个参数:
      12          * 第一个参数:目标类的类加载器;
      13          * 第二个参数:目标类的所有接口;
      14          * 第三个参数:handler;
      15          * */
      16         ForumService proxy = (ForumService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
      17                 target.getClass().getInterfaces(), handler);
      18         proxy.removeForum(10);
      19         proxy.removeTopic(1012);
      20         
      21     }
      22 }

      PerformaceHandler类中,我们是需要method参数的。而上面第16行生成代理的方法中的三个参数,足以用来一一遍历目标类的每一个方法,然后生成的代理类 proxy成功的为目标类的每一个方法(removeForum、removeTopic)加入了增强代码。整个过程的时序图如下:

  7. 使用CGLib改造:JDK代理是基于接口的,而现实中很多业务程序都不是基于接口定义方法的,这就要使用CGLib来完成AOP贡功能了。 CGLib采用底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并顺势织入横切逻辑:
     1 package com.baobaotao.proxy;
     2 
     3 import java.lang.reflect.Method;
     4 
     5 import net.sf.cglib.proxy.Enhancer;
     6 import net.sf.cglib.proxy.MethodInterceptor;
     7 import net.sf.cglib.proxy.MethodProxy;
     8 
     9 public class CglibProxy implements MethodInterceptor {
    10     private Enhancer enhancer = new Enhancer();
    11 
    12     public Object getProxy(Class clazz) {
    13         enhancer.setSuperclass(clazz);//clazz就是原目标类,即父类
    14         enhancer.setCallback(this);
    15         return enhancer.create();//通过字节码技术动态生成子类,并创建子类的实例。
    16     }
    17     public Object intercept(Object obj, Method method, Object[] args,
    18             MethodProxy proxy) throws Throwable {//拦截父类所有方法的调用
    19         PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName());
    20         Object result=proxy.invokeSuper(obj, args);//由proxy代理类调用父类的方法;
    21         PerformanceMonitor.end();
    22         return result;
    23     }
    24 }

    下面是测试:

     1 package com.baobaotao.proxy;
     2 
     3 
     4 public class TestForumService {
     5     public static void main(String[] args) {
     6         
     7         //使用CGLib动态代理
     8         CglibProxy proxy = new CglibProxy();
     9         //动态生成子类的方式创建代理类,它拥有原目标类的方法和增强代码
    10         ForumService forumService = (ForumService)proxy.getProxy(ForumServiceImpl.class);
    11         forumService.removeForum(10);
    12         forumService.removeTopic(1023);
    13         
    14     }
    15 }

    打印结果是一样的。

  8. 上面的JDK动态代理以及CGLib改造只是基础版本,为了说明概念的,但是它们存在以下问题:
    • 目标类的所有方法都加了增强代码(明显实际中我们只想增强部分方法)。
    • 我们死板的总是在业务方法的开始与结束前织入了增强代码(切点的方位不仅仅只有开始、结束前这两个位置,还有很多种可能性)。
    • 如果另一个类也要创建代理,我们就又需要编写另一个类的代理实例。也就是说,上面的栗子无法做到通用性。
  9. Spring AOP的主要工作就是基于上面提出的几个问题而展开的。后期我们将会逐步学习。 
原文地址:https://www.cnblogs.com/mesopotamia/p/5156447.html