Spring AOP的实现机制

27 篇文章 3 订阅

拿来学习

http://www.iteye.com/topic/1116696

http://blog.csdn.net/moreevan/article/details/11977115


一、基本概念

什么是AOP

AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

 

而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

 

使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”

 

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。


AOP使用场景

AOP用来封装横切关注点,具体可以在下面的场景中使用:

 

Authentication 权限

Caching 缓存

Context passing 内容传递

Error handling 错误处理

Lazy loading 懒加载

Debugging  调试

logging, tracing, profiling and monitoring 记录跟踪 优化 校准

Performance optimization 性能优化

Persistence  持久化

Resource pooling 资源池

Synchronization 同步

Transactions 事务


AOP相关概念

方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。

 

连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。

 

通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。Spring中定义了四个advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice

 

切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解, MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上

 

引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口

 

目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO

 

AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

 

织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。


二、具体实现


1 AOP各种的实现

AOP就是面向切面编程,我们可以从几个层面来实现AOP。

在编译器修改源代码,在运行期字节码加载前修改字节码或字节码加载后动态创建代理类的字节码,以下是各种实现机制的比较。 

 

类别

机制

原理

优点

缺点

静态AOP

静态织入

在编译期,切面直接以字节码的形式编译到目标字节码文件中。

对系统无性能影响。

灵活性不够。

动态AOP

动态代理

在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中。

相对于静态AOP更加灵活。

切入的关注点需要实现接口。对系统有一点性能影响。

动态字节码生成

在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中。

没有接口也可以织入。

扩展类的实例方法为final时,则无法进行织入。

自定义类加载器

在运行期,目标加载前,将切面逻辑加到目标字节码里。

可以对绝大部分类进行织入。

代码中如果使用了其他类加载器,则这些类将不会被织入。

字节码转换

在运行期,所有类加载器加载字节码前,前进行拦截。

可以对所有类进行织入。

 



2 AOP里的公民

  • Joinpoint:拦截点,如某个业务方法。
  • Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint。
  • Advice:  要切入的逻辑。
  • Before Advice 在方法前切入。
  • After Advice 在方法后切入,抛出异常时也会切入。
  • After Returning Advice 在方法返回后切入,抛出异常则不会切入。
  • After Throwing Advice 在方法抛出异常时切入。
  • Around Advice 在方法执行前后切入,可以中断或忽略原有流程的执行。 
  • 公民之间的关系

    织入器通过在切面中定义pointcut来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类。

3 AOP的实现机制 
  本章节将详细介绍AOP有各种实现机制。


3.1 动态代理
  Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。


3.1.1 使用动态代理
  那如何使用动态代理来实现AOP。下面的例子演示在方法执行前织入一段记录日志的代码,其中Business是代理类,LogInvocationHandler是记录日志的切面,IBusiness, IBusiness2是代理类的接口,Proxy.newProxyInstance是织入器。
清单一:动态代理的演示

Java代码 复制代码  收藏代码
  1. public static void main(String[] args) {   
  2.     //需要代理的接口,被代理类实现的多个接口都必须在这里定义   
  3.     Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class };   
  4.     //构建AOP的Advice,这里需要传入业务类的实例   
  5.     LogInvocationHandler handler = new LogInvocationHandler(new Business());   
  6.     //生成代理类的字节码加载器   
  7.     ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();   
  8.     //织入器,织入代码并生成代理类   
  9.     IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);   
  10.     //使用代理类的实例来调用方法。   
  11.     proxyBusiness.doSomeThing2();   
  12.     ((IBusiness) proxyBusiness).doSomeThing();   
  13. }   
  14.   
  15. /**  
  16. * 打印日志的切面  
  17. */   
  18. public static class LogInvocationHandler implements InvocationHandler {   
  19.   
  20.     private Object target; //目标对象   
  21.   
  22.     LogInvocationHandler(Object target) {   
  23.         this.target = target;   
  24.     }   
  25.   
  26.     @Override   
  27.     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {   
  28.         //执行原有逻辑   
  29.         Object rev = method.invoke(target, args);   
  30.         //执行织入的日志,你可以控制哪些方法执行切入逻辑   
  31.         if (method.getName().equals("doSomeThing2")) {   
  32.             System.out.println("记录日志");   
  33.         }   
  34.         return rev;   
  35.     }   
  36. }   
  37.   
  38. 接口IBusiness和IBusiness2定义省略。   
public static void main(String[] args) { 
    //需要代理的接口,被代理类实现的多个接口都必须在这里定义 
    Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class }; 
    //构建AOP的Advice,这里需要传入业务类的实例 
    LogInvocationHandler handler = new LogInvocationHandler(new Business()); 
    //生成代理类的字节码加载器 
    ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader(); 
    //织入器,织入代码并生成代理类 
    IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler); 
    //使用代理类的实例来调用方法。 
    proxyBusiness.doSomeThing2(); 
    ((IBusiness) proxyBusiness).doSomeThing(); 
} 

/** 
* 打印日志的切面 
*/ 
public static class LogInvocationHandler implements InvocationHandler { 

    private Object target; //目标对象 

    LogInvocationHandler(Object target) { 
        this.target = target; 
    } 

    @Override 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        //执行原有逻辑 
        Object rev = method.invoke(target, args); 
        //执行织入的日志,你可以控制哪些方法执行切入逻辑 
        if (method.getName().equals("doSomeThing2")) { 
            System.out.println("记录日志"); 
        } 
        return rev; 
    } 
} 

接口IBusiness和IBusiness2定义省略。 

 

   业务类,需要代理的类。

Java代码 复制代码  收藏代码
  1. public class Business implements IBusiness, IBusiness2 {   
  2.   
  3.     @Override   
  4.     public boolean doSomeThing() {   
  5.         System.out.println("执行业务逻辑");   
  6.         return true;   
  7.     }   
  8.   
  9.     @Override   
  10.     public void doSomeThing2() {   
  11.         System.out.println("执行业务逻辑2");   
  12.     }   
  13.   
  14. }   
public class Business implements IBusiness, IBusiness2 { 

    @Override 
    public boolean doSomeThing() { 
        System.out.println("执行业务逻辑"); 
        return true; 
    } 

    @Override 
    public void doSomeThing2() { 
        System.out.println("执行业务逻辑2"); 
    } 

} 

 

   输出

Java代码 复制代码  收藏代码
  1. 执行业务逻辑2   
  2. 记录日志   
  3. 执行业务逻辑   
执行业务逻辑2 
记录日志 
执行业务逻辑 

 

  可以看到“记录日志”的逻辑切入到Business类的doSomeThing方法前了。


 

3.1.2 动态代理原理
    本节将结合动态代理的源代码讲解其实现原理。动态代理的核心其实就是代理对象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。让我们进入newProxyInstance方法观摩下,核心代码其实就三行。
清单二:生成代理类

Java代码 复制代码  收藏代码
  1. //获取代理类   
  2. Class cl = getProxyClass(loader, interfaces);   
  3. //获取带有InvocationHandler参数的构造方法   
  4. Constructor cons = cl.getConstructor(constructorParams);   
  5. //把handler传入构造方法生成实例   
  6. return (Object) cons.newInstance(new Object[] { h });     
//获取代理类 
Class cl = getProxyClass(loader, interfaces); 
//获取带有InvocationHandler参数的构造方法 
Constructor cons = cl.getConstructor(constructorParams); 
//把handler传入构造方法生成实例 
return (Object) cons.newInstance(new Object[] { h });   

 

    其中getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:在当前类加载器的缓存里搜索是否有代理类,没有则生成代理类并缓存在本地JVM里。清单三:查找代理类。

Java代码 复制代码  收藏代码
  1.  // 缓存的key使用接口名称生成的List   
  2. Object key = Arrays.asList(interfaceNames);   
  3. synchronized (cache) {   
  4.     do {   
  5. Object value = cache.get(key);   
  6.          // 缓存里保存了代理类的引用   
  7. if (value instanceof Reference) {   
  8.     proxyClass = (Class) ((Reference) value).get();   
  9. }   
  10. if (proxyClass != null) {   
  11. // 代理类已经存在则返回   
  12.     return proxyClass;   
  13. else if (value == pendingGenerationMarker) {   
  14.     // 如果代理类正在产生,则等待   
  15.     try {   
  16. cache.wait();   
  17.     } catch (InterruptedException e) {   
  18.     }   
  19.     continue;   
  20. else {   
  21.     //没有代理类,则标记代理准备生成   
  22.     cache.put(key, pendingGenerationMarker);   
  23.     break;   
  24. }   
  25.     } while (true);   
  26. }   
 // 缓存的key使用接口名称生成的List 
Object key = Arrays.asList(interfaceNames); 
synchronized (cache) { 
    do { 
Object value = cache.get(key); 
         // 缓存里保存了代理类的引用 
if (value instanceof Reference) { 
    proxyClass = (Class) ((Reference) value).get(); 
} 
if (proxyClass != null) { 
// 代理类已经存在则返回 
    return proxyClass; 
} else if (value == pendingGenerationMarker) { 
    // 如果代理类正在产生,则等待 
    try { 
cache.wait(); 
    } catch (InterruptedException e) { 
    } 
    continue; 
} else { 
    //没有代理类,则标记代理准备生成 
    cache.put(key, pendingGenerationMarker); 
    break; 
} 
    } while (true); 
} 

  

代理类的生成主要是以下这两行代码。 清单四:生成并加载代理类

 

Java代码 复制代码  收藏代码
  1. //生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘)   
  2. proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);   
  3. //使用类加载器将字节码加载到内存中   
  4. proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);   
//生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘) 
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); 
//使用类加载器将字节码加载到内存中 
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length); 

 

  ProxyGenerator.generateProxyClass()方法属于sun.misc包下,Oracle并没有提供源代码,但是我们可以使用JD-GUI这样的反编译软件打开jre\lib\rt.jar来一探究竟,以下是其核心代码的分析。
清单五:代理类的生成过程

Java代码 复制代码  收藏代码
  1. //添加接口中定义的方法,此时方法体为空   
  2. for (int i = 0; i < this.interfaces.length; i++) {   
  3.   localObject1 = this.interfaces[i].getMethods();   
  4.   for (int k = 0; k < localObject1.length; k++) {   
  5.      addProxyMethod(localObject1[k], this.interfaces[i]);   
  6.   }   
  7. }   
  8.   
  9. //添加一个带有InvocationHandler的构造方法   
  10. MethodInfo localMethodInfo = new MethodInfo("<init>""(Ljava/lang/reflect/InvocationHandler;)V"1);   
  11.   
  12. //循环生成方法体代码(省略)   
  13. //方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略)   
  14. this.cp.getInterfaceMethodRef("InvocationHandler""invoke""Object; Method; Object;")   
  15.   
  16. //将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。   
  17. localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");   
  18. localFileOutputStream.write(this.val$classFile);   
//添加接口中定义的方法,此时方法体为空 
for (int i = 0; i < this.interfaces.length; i++) { 
  localObject1 = this.interfaces[i].getMethods(); 
  for (int k = 0; k < localObject1.length; k++) { 
     addProxyMethod(localObject1[k], this.interfaces[i]); 
  } 
} 

//添加一个带有InvocationHandler的构造方法 
MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1); 

//循环生成方法体代码(省略) 
//方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略) 
this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;") 

//将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。 
localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class"); 
localFileOutputStream.write(this.val$classFile); 

 

  那么通过以上分析,我们可以推出动态代理为我们生成了一个这样的代理类。把方法doSomeThing的方法体修改为调用LogInvocationHandler的invoke方法。
清单六:生成的代理类源码

 

Java代码 复制代码  收藏代码
  1. public class ProxyBusiness implements IBusiness, IBusiness2 {   
  2.   
  3. private LogInvocationHandler h;   
  4.   
  5. @Override   
  6. public void doSomeThing2() {   
  7.     try {   
  8.         Method m = (h.target).getClass().getMethod("doSomeThing"null);   
  9.         h.invoke(this, m, null);   
  10.     } catch (Throwable e) {   
  11.         // 异常处理(略)   
  12.     }   
  13. }   
  14.   
  15. @Override   
  16. public boolean doSomeThing() {   
  17.     try {   
  18.        Method m = (h.target).getClass().getMethod("doSomeThing2"null);   
  19.        return (Boolean) h.invoke(this, m, null);   
  20.     } catch (Throwable e) {   
  21.         // 异常处理(略)   
  22.     }   
  23.     return false;   
  24. }   
  25.   
  26. public ProxyBusiness(LogInvocationHandler h) {   
  27.     this.h = h;   
  28. }   
  29.   
  30. //测试用   
  31. public static void main(String[] args) {   
  32.     //构建AOP的Advice   
  33.     LogInvocationHandler handler = new LogInvocationHandler(new Business());   
  34.     new ProxyBusiness(handler).doSomeThing();   
  35.     new ProxyBusiness(handler).doSomeThing2();   
  36. }   
  37. }   
public class ProxyBusiness implements IBusiness, IBusiness2 { 

private LogInvocationHandler h; 

@Override 
public void doSomeThing2() { 
    try { 
        Method m = (h.target).getClass().getMethod("doSomeThing", null); 
        h.invoke(this, m, null); 
    } catch (Throwable e) { 
        // 异常处理(略) 
    } 
} 

@Override 
public boolean doSomeThing() { 
    try { 
       Method m = (h.target).getClass().getMethod("doSomeThing2", null); 
       return (Boolean) h.invoke(this, m, null); 
    } catch (Throwable e) { 
        // 异常处理(略) 
    } 
    return false; 
} 

public ProxyBusiness(LogInvocationHandler h) { 
    this.h = h; 
} 

//测试用 
public static void main(String[] args) { 
    //构建AOP的Advice 
    LogInvocationHandler handler = new LogInvocationHandler(new Business()); 
    new ProxyBusiness(handler).doSomeThing(); 
    new ProxyBusiness(handler).doSomeThing2(); 
} 
} 

 

3.1.3 小结 
    从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题,第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。

3.2 动态字节码生成
   使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以使用Cglib实现AOP不需要基于接口。


    本节介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的,高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用Cglib前需要引入Asm的jar。 清单七:使用CGLib实现AOP

Java代码 复制代码  收藏代码
  1. public static void main(String[] args) {   
  2.         byteCodeGe();   
  3.     }   
  4.   
  5.     public static void byteCodeGe() {   
  6.         //创建一个织入器   
  7.         Enhancer enhancer = new Enhancer();   
  8.         //设置父类   
  9.         enhancer.setSuperclass(Business.class);   
  10.         //设置需要织入的逻辑   
  11.         enhancer.setCallback(new LogIntercept());   
  12.         //使用织入器创建子类   
  13.         IBusiness2 newBusiness = (IBusiness2) enhancer.create();   
  14.         newBusiness.doSomeThing2();   
  15.     }   
  16.   
  17.     /**  
  18.      * 记录日志  
  19.      */   
  20.     public static class LogIntercept implements MethodInterceptor {   
  21.   
  22.         @Override   
  23.         public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {   
  24.             //执行原有逻辑,注意这里是invokeSuper   
  25.             Object rev = proxy.invokeSuper(target, args);   
  26.             //执行织入的日志   
  27.             if (method.getName().equals("doSomeThing2")) {   
  28.                 System.out.println("记录日志");   
  29.             }   
  30.             return rev;   
  31.         }   
  32.     }   
public static void main(String[] args) { 
        byteCodeGe(); 
    } 

    public static void byteCodeGe() { 
        //创建一个织入器 
        Enhancer enhancer = new Enhancer(); 
        //设置父类 
        enhancer.setSuperclass(Business.class); 
        //设置需要织入的逻辑 
        enhancer.setCallback(new LogIntercept()); 
        //使用织入器创建子类 
        IBusiness2 newBusiness = (IBusiness2) enhancer.create(); 
        newBusiness.doSomeThing2(); 
    } 

    /** 
     * 记录日志 
     */ 
    public static class LogIntercept implements MethodInterceptor { 

        @Override 
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable { 
            //执行原有逻辑,注意这里是invokeSuper 
            Object rev = proxy.invokeSuper(target, args); 
            //执行织入的日志 
            if (method.getName().equals("doSomeThing2")) { 
                System.out.println("记录日志"); 
            } 
            return rev; 
        } 
    } 

 

 

3.3 自定义类加载器
   如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接。

 



Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:






    我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,咱们再看看使用Javassist实现AOP的代码:
清单八:启动自定义的类加载器

Java代码 复制代码  收藏代码
  1. //获取存放CtClass的容器ClassPool   
  2. ClassPool cp = ClassPool.getDefault();   
  3. //创建一个类加载器   
  4. Loader cl = new Loader();   
  5. //增加一个转换器   
  6. cl.addTranslator(cp, new MyTranslator());   
  7. //启动MyTranslator的main函数   
  8. cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);   
//获取存放CtClass的容器ClassPool 
ClassPool cp = ClassPool.getDefault(); 
//创建一个类加载器 
Loader cl = new Loader(); 
//增加一个转换器 
cl.addTranslator(cp, new MyTranslator()); 
//启动MyTranslator的main函数 
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args); 

 清单九:类加载监听器

Java代码 复制代码  收藏代码
  1. public static class MyTranslator implements Translator {   
  2.   
  3.         public void start(ClassPool pool) throws NotFoundException, CannotCompileException {   
  4.         }   
  5.   
  6.         /* *  
  7.          * 类装载到JVM前进行代码织入  
  8.          */   
  9.         public void onLoad(ClassPool pool, String classname) {   
  10.             if (!"model$Business".equals(classname)) {   
  11.                 return;   
  12.             }   
  13.             //通过获取类文件   
  14.             try {   
  15.                 CtClass  cc = pool.get(classname);   
  16.                 //获得指定方法名的方法   
  17.                 CtMethod m = cc.getDeclaredMethod("doSomeThing");   
  18.                 //在方法执行前插入代码   
  19.                 m.insertBefore("{ System.out.println(\"记录日志\"); }");   
  20.             } catch (NotFoundException e) {   
  21.             } catch (CannotCompileException e) {   
  22.             }   
  23.         }   
  24.   
  25.         public static void main(String[] args) {   
  26.             Business b = new Business();   
  27.             b.doSomeThing2();   
  28.             b.doSomeThing();   
  29.         }   
  30.     }   
public static class MyTranslator implements Translator { 

        public void start(ClassPool pool) throws NotFoundException, CannotCompileException { 
        } 

        /* * 
         * 类装载到JVM前进行代码织入 
         */ 
        public void onLoad(ClassPool pool, String classname) { 
            if (!"model$Business".equals(classname)) { 
                return; 
            } 
            //通过获取类文件 
            try { 
                CtClass  cc = pool.get(classname); 
                //获得指定方法名的方法 
                CtMethod m = cc.getDeclaredMethod("doSomeThing"); 
                //在方法执行前插入代码 
                m.insertBefore("{ System.out.println(\"记录日志\"); }"); 
            } catch (NotFoundException e) { 
            } catch (CannotCompileException e) { 
            } 
        } 

        public static void main(String[] args) { 
            Business b = new Business(); 
            b.doSomeThing2(); 
            b.doSomeThing(); 
        } 
    } 

 输出: 

Java代码 复制代码  收藏代码
  1. 执行业务逻辑2   
  2. 记录日志   
  3. 执行业务逻辑  
执行业务逻辑2 
记录日志 
执行业务逻辑

 
    其中Bussiness类在本文的清单一中定义。看起来是不是特别简单,CtClass是一个class文件的抽象描述。咱们也可以使用insertAfter()在方法的末尾插入代码,使用insertAt()在指定行插入代码。

3.3.1 小结
    从本节中可知,使用自定义的类加载器实现AOP在性能上要优于动态代理和Cglib,因为它不会产生新类,但是它仍然存在一个问题,就是如果其他的类加载器来加载类的话,这些类将不会被拦截。

3.4 字节码转换
    自定义的类加载器实现AOP只能拦截自己加载的字节码,那么有没有一种方式能够监控所有类加载器加载字节码呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,开发者可以构建一个字节码转换器,在字节码加载前进行转换。本节使用Instrumentation和javassist来实现AOP。

3.4.1 构建字节码转换器
    首先需要创建字节码转换器,该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码。

Java代码 复制代码  收藏代码
  1. public class MyClassFileTransformer implements ClassFileTransformer {   
  2.   
  3.     /**  
  4.      * 字节码加载到虚拟机前会进入这个方法  
  5.      */   
  6.     @Override   
  7.     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,   
  8.                             ProtectionDomain protectionDomain, byte[] classfileBuffer)   
  9.             throws IllegalClassFormatException {   
  10.         System.out.println(className);   
  11.         //如果加载Business类才拦截   
  12.         if (!"model/Business".equals(className)) {   
  13.             return null;   
  14.         }   
  15.   
  16.         //javassist的包名是用点分割的,需要转换下   
  17.         if (className.indexOf("/") != -1) {   
  18.             className = className.replaceAll("/"".");   
  19.         }   
  20.         try {   
  21.             //通过包名获取类文件   
  22.             CtClass cc = ClassPool.getDefault().get(className);   
  23.             //获得指定方法名的方法   
  24.             CtMethod m = cc.getDeclaredMethod("doSomeThing");   
  25.             //在方法执行前插入代码   
  26.             m.insertBefore("{ System.out.println(\"记录日志\"); }");   
  27.             return cc.toBytecode();   
  28.         } catch (NotFoundException e) {   
  29.         } catch (CannotCompileException e) {   
  30.         } catch (IOException e) {   
  31.             //忽略异常处理   
  32.         }   
  33.         return null;   
  34. }   
public class MyClassFileTransformer implements ClassFileTransformer { 

    /** 
     * 字节码加载到虚拟机前会进入这个方法 
     */ 
    @Override 
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) 
            throws IllegalClassFormatException { 
        System.out.println(className); 
        //如果加载Business类才拦截 
        if (!"model/Business".equals(className)) { 
            return null; 
        } 

        //javassist的包名是用点分割的,需要转换下 
        if (className.indexOf("/") != -1) { 
            className = className.replaceAll("/", "."); 
        } 
        try { 
            //通过包名获取类文件 
            CtClass cc = ClassPool.getDefault().get(className); 
            //获得指定方法名的方法 
            CtMethod m = cc.getDeclaredMethod("doSomeThing"); 
            //在方法执行前插入代码 
            m.insertBefore("{ System.out.println(\"记录日志\"); }"); 
            return cc.toBytecode(); 
        } catch (NotFoundException e) { 
        } catch (CannotCompileException e) { 
        } catch (IOException e) { 
            //忽略异常处理 
        } 
        return null; 
} 

 

3.4.2 注册转换器
    使用premain函数注册字节码转换器,该方法在main函数之前执行。

Java代码 复制代码  收藏代码
  1. public class MyClassFileTransformer implements ClassFileTransformer {   
  2.     public static void premain(String options, Instrumentation ins) {   
  3.         //注册我自己的字节码转换器   
  4.         ins.addTransformer(new MyClassFileTransformer());   
  5. }   
  6. }   
public class MyClassFileTransformer implements ClassFileTransformer { 
    public static void premain(String options, Instrumentation ins) { 
        //注册我自己的字节码转换器 
        ins.addTransformer(new MyClassFileTransformer()); 
} 
} 

 

3.4.3 配置和执行
    需要告诉JVM在启动main函数之前,需要先执行premain函数。首先需要将premain函数所在的类打成jar包。并修改该jar包里的META-INF\MANIFEST.MF 文件。 

Java代码 复制代码  收藏代码
  1. Manifest-Version: 1.0   
  2. Premain-Class: bci. MyClassFileTransformer  
Manifest-Version: 1.0 
Premain-Class: bci. MyClassFileTransformer

     然后在JVM的启动参数里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

             3.4.4 输出

    执行main函数,你会发现切入的代码无侵入性的织入进去了。

Java代码 复制代码  收藏代码
  1. public static void main(String[] args) {   
  2.    new Business().doSomeThing();   
  3.    new Business().doSomeThing2();   
  4. }   
  5.    
public static void main(String[] args) { 
   new Business().doSomeThing(); 
   new Business().doSomeThing2(); 
} 
 

   输出

Java代码 复制代码  收藏代码
  1. model/Business   
  2. sun/misc/Cleaner   
  3. java/lang/Enum   
  4. model/IBusiness   
  5. model/IBusiness2   
  6. 记录日志   
  7. 执行业务逻辑   
  8. 执行业务逻辑2   
  9. java/lang/Shutdown   
  10. java/lang/Shutdown$Lock   
model/Business 
sun/misc/Cleaner 
java/lang/Enum 
model/IBusiness 
model/IBusiness2 
记录日志 
执行业务逻辑 
执行业务逻辑2 
java/lang/Shutdown 
java/lang/Shutdown$Lock 

  

 从输出中可以看到系统类加载器加载的类也经过了这里。

 

4 AOP实战
说了这么多理论,那AOP到底能做什么呢? AOP能做的事情非常多。

  • 性能监控,在方法调用前后记录调用时间,方法执行太长或超时报警。
  • 缓存代理,缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
  • 软件破解,使用AOP修改软件的验证类的判断逻辑。
  • 记录日志,在方法执行前后记录系统日志。
  • 工作流系统,工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务。
  • 权限验证,方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。 

4.1 Spring的AOP
    Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点,第一个只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。第二个同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。第三个性能不是最好的,从3.3章节我们得知使用自定义类加载器,性能要优于动态代理和CGlib。
可以获取代理类

Java代码 复制代码  收藏代码
  1. public IMsgFilterService getThis()   
  2. {   
  3.         return (IMsgFilterService) AopContext.currentProxy();   
  4. }   
  5.   
  6. public boolean evaluateMsg () {   
  7.    // 执行此方法将织入切入逻辑   
  8. return getThis().evaluateMsg(String message);   
  9. }   
  10.   
  11. @MethodInvokeTimesMonitor("KEY_FILTER_NUM")   
  12. public boolean evaluateMsg(String message) {   
public IMsgFilterService getThis() 
{ 
        return (IMsgFilterService) AopContext.currentProxy(); 
} 

public boolean evaluateMsg () { 
   // 执行此方法将织入切入逻辑 
return getThis().evaluateMsg(String message); 
} 

@MethodInvokeTimesMonitor("KEY_FILTER_NUM") 
public boolean evaluateMsg(String message) { 

 不能获取代理类

Java代码 复制代码  收藏代码
  1. public boolean evaluateMsg () {   
  2.    // 执行此方法将不会织入切入逻辑   
  3. return evaluateMsg(String message);   
  4. }   
  5.   
  6. @MethodInvokeTimesMonitor("KEY_FILTER_NUM")   
  7. public boolean evaluateMsg(String message) {   
public boolean evaluateMsg () { 
   // 执行此方法将不会织入切入逻辑 
return evaluateMsg(String message); 
} 

@MethodInvokeTimesMonitor("KEY_FILTER_NUM") 
public boolean evaluateMsg(String message) { 

 

 4.2 参考资料

  • Java 动态代理机制分析及扩展
  • CGlib的官方网站
  • ASM官方网站
  • JbossAOP
  • Java5特性Instrumenttation实践

 

  • 大小: 17 KB
  • 大小: 22.4 KB
  • 大小: 13.3 KB
  • 大小: 10.9 KB
  • 大小: 11.6 KB
  • 大小: 18.5 KB



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值