面向切面编程(3):AOP实现机制


分类: Java&Java EE架构   110人阅读  评论(0)  收藏  举报

目录(?)[+]


1 AOP各种的实现


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

图1 AOP实现的不同层面

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

类别

机制

原理

优点

缺点

静态AOP

静态织入

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

对系统无性能影响。

灵活性不够。

动态AOP

动态代理

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

相对于静态AOP更加灵活。

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

动态字节码生成

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

没有接口也可以织入(Weave)。

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

自定义类加载器

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

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

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

字节码转换

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

可以对所有类进行织入。

 


2 AOP里的公民


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

    图2 AOP概念之间的关系

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


3 AOP的实现机制


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


3.1 动态代理


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

图3 JDK动态代理

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

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. import java.lang.reflect.InvocationHandler;  
  2. import java.lang.reflect.Method;  
  3. import java.lang.reflect.Proxy;  
  4.   
  5. // 接口IBusiness和IBusiness2定义省略  
  6.   
  7. // 业务类,需要代理的类  
  8. class Business implements IBusiness, IBusiness2 {  
  9.   
  10.     @Override  
  11.     public boolean doSomeThing() {  
  12.         System.out.println("执行业务逻辑");  
  13.         return true;  
  14.     }  
  15.   
  16.     @Override  
  17.     public void doSomeThing2() {  
  18.         System.out.println("执行业务逻辑2");  
  19.     }  
  20.   
  21. }  
  22.   
  23. public class DynamicProxyDemo {  
  24.   
  25.     public static void main(String[] args) {  
  26.         //需要代理的接口,被代理类实现的多个接口都必须在这里定义     
  27.         Class[] proxyInterface = new Class[]{IBusiness.class, IBusiness2.class};  
  28.         //构建AOP的Advice,这里需要传入业务类的实例     
  29.         LogInvocationHandler handler = new LogInvocationHandler(new Business());  
  30.         //生成代理类的字节码加载器     
  31.         ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();  
  32.         //织入器,织入代码并生成代理类     
  33.         IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);  
  34.         //使用代理类的实例来调用方法。     
  35.         proxyBusiness.doSomeThing2();  
  36.         ((IBusiness) proxyBusiness).doSomeThing();  
  37.     }  
  38. }  
  39.   
  40. /** 
  41.  * 打印日志的切面 
  42.  */  
  43. class LogInvocationHandler implements InvocationHandler {  
  44.   
  45.     private Object target; //目标对象  
  46.   
  47.     LogInvocationHandler(Object target) {  
  48.         this.target = target;  
  49.     }  
  50.   
  51.     @Override  
  52.     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  53.         //执行原有逻辑     
  54.         Object rev = method.invoke(target, args);  
  55.         //执行织入的日志,你可以控制哪些方法执行切入逻辑     
  56.         if (method.getName().equals("doSomeThing2")) {  
  57.             System.out.println("记录日志");  
  58.         }  
  59.         return rev;  
  60.     }  
  61. }  
  输出:
[plain]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. 执行业务逻辑2     
  2. 记录日志     
  3. 执行业务逻辑   
  可以看到“记录日志”的逻辑切入到Business类的doSomeThing方法前了。

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

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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 });    
  其中getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:在当前类加载器的缓存里搜索是否有代理类,没有则生成代理类并缓存在本地JVM里。

清单三:查找代理类

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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. }  
  代理类的生成主要是以下这两行代码。

清单四:生成并加载代理类

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘)     
  2. proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);     
  3. //使用类加载器将字节码加载到内存中     
  4. proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);  
   ProxyGenerator.generateProxyClass()方法属于sun.misc包下,Oracle并没有提供源代码,但是我们可以使用JD-GUI这样的反编译软件打开jre\lib\rt.jar来一探究竟,以下是其核心代码的分析。
清单五:代理类的生成过程
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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);     
  那么通过以上分析,我们可以推出动态代理为我们生成了一个这样的代理类。把方法doSomeThing的方法体修改为调用LogInvocationHandler的invoke方法。下面是Proxy.newProxyInstance为Business类生成的代理类。
清单六:生成的代理类源码
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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. }  
  从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题,第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的, 首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。 


3.2 动态字节码生成


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


图4 动态字节码生成

  下面介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的、高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm。

清单七:使用CGLib实现AOP

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. import java.lang.reflect.Method;  
  2. import net.sf.cglib.proxy.Enhancer;  
  3. import net.sf.cglib.proxy.MethodInterceptor;  
  4. import net.sf.cglib.proxy.MethodProxy;  
  5.   
  6. // 类Business,可以不实现任何接口  
  7.   
  8. public class CglibAopDemo {  
  9.   
  10.     public static void main(String[] args) {  
  11.         byteCodeGe();  
  12.     }  
  13.   
  14.     public static void byteCodeGe() {  
  15.         //创建一个织入器     
  16.         Enhancer enhancer = new Enhancer();  
  17.         //设置父类     
  18.         enhancer.setSuperclass(Business.class);  
  19.         //设置需要织入的逻辑     
  20.         enhancer.setCallback(new LogIntercept());  
  21.         //使用织入器创建子类     
  22.         Business newBusiness = (Business) enhancer.create();  
  23.         newBusiness.doSomeThing2();  
  24.     }  
  25.   
  26.     /** 
  27.      * 记录日志 
  28.      */  
  29.     public static class LogIntercept implements MethodInterceptor {  
  30.   
  31.         @Override  
  32.         public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {  
  33.             //执行原有逻辑,注意这里是invokeSuper     
  34.             Object rev = proxy.invokeSuper(target, args);  
  35.             //执行织入的日志     
  36.             if (method.getName().equals("doSomeThing2")) {  
  37.                 System.out.println("记录日志");  
  38.             }  
  39.             return rev;  
  40.         }  
  41.     }  
  42. }  
  这里目标类是Busniess(无需从接口继承);切面(即拦截器)是实现MethodInterceptor接口的类LogIntercept,用来记录日志;织入器是Enhancer。


3.3 自定义类加载器


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

图5 自定义类加载器

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

图6 自定义类加载器实现原理

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

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package aopexample;  
  2.   
  3. import javassist.CannotCompileException;  
  4. import javassist.ClassPool;  
  5. import javassist.CtClass;  
  6. import javassist.CtMethod;  
  7. import javassist.Loader;  
  8. import javassist.NotFoundException;  
  9. import javassist.Translator;  
  10.   
  11. public class JavassistAopDemo {  
  12.   
  13.     public static void main(String[] args) throws NotFoundException, Throwable {  
  14.         //获取存放CtClass的容器ClassPool     
  15.         ClassPool cp = ClassPool.getDefault();  
  16.         //创建一个类加载器     
  17.         Loader cl = new Loader();  
  18.         //增加一个转换器     
  19.         cl.addTranslator(cp, new MyTranslator());  
  20.         //启动MyTranslator的main函数     
  21.         cl.run("aopexample.JavassistAopDemo$MyTranslator", args);  
  22.     }  
  23.   
  24.     // 内嵌类,转换器  
  25.     public static class MyTranslator implements Translator {  
  26.   
  27.         @Override  
  28.         public void start(ClassPool pool) throws NotFoundException,  
  29.                 CannotCompileException {  
  30.         }  
  31.   
  32.         /** 
  33.          * 类装载到JVM前进行代码织入 
  34.          */  
  35.         @Override  
  36.         public void onLoad(ClassPool pool, String classname) {  
  37.             if (!"aopexample.Business".equals(classname)) {  
  38.                 return;  
  39.             }  
  40.             //通过获取类文件     
  41.             try {  
  42.                 CtClass cc = pool.get(classname);  
  43.                 //获得指定方法名的方法     
  44.                 CtMethod m = cc.getDeclaredMethod("doSomeThing");  
  45.                 //在方法执行前插入代码     
  46.                 m.insertBefore("{ System.out.println(\"记录日志\"); }");  
  47.             } catch (NotFoundException | CannotCompileException e) {  
  48.             }  
  49.         }  
  50.   
  51.         public static void main(String[] args) {  
  52.             Business b = new Business();  
  53.             b.doSomeThing2();  
  54.             b.doSomeThing();  
  55.         }  
  56.     }  
  57. }  
  58.   
  59. class Business {  
  60.   
  61.     public boolean doSomeThing() {  
  62.         System.out.println("执行业务逻辑");  
  63.         return true;  
  64.     }  
  65.   
  66.     public void doSomeThing2() {  
  67.         System.out.println("执行业务逻辑2");  
  68.     }  
  69. }  
  输出:
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. 执行业务逻辑2     
  2. 记录日志     
  3. 执行业务逻辑  
  这里转换器MyTranslator是一个切面,用作类加载监听器。看起来是不是特别简单,CtClass是一个class文件的抽象描述。这里先获取一个类容器cp和一个类加载器c1,然后给c1增加一个监听器。用c1加载的所有类都会放到cp容器中。当用c1加载类aopexample.JavassistAopDemo$MyTranslator并运行其中的main方法时,随后加载aopexample.Business类。每加载一个类到JVM之前,监听器里的onLoad()触发,它可以给类中的方法织入一段代码。咱们也可以使用insertAfter()在方法的末尾插入代码,使用insertAt()在指定行插入代码。

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


3.4 字节码转换


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

  使用Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在JVM上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和Java类操作了,这样的特性实际上提供了一种虚拟机级别支持的AOP实现方式,使得开发者无需对JDK做任何升级和改动,就可以实现某些AOP的功能了。
  开发者可以让Instrumentation代理在main函数运行前执行。简要说来就是如下几个步骤:

  (1)编写premain函数。编写一个Java类,包含如下两个方法当中的任何一个。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public static void premain(String agentArgs, Instrumentation inst);  [1]  
  2. public static void premain(String agentArgs); [2]  
  其中,[1]的优先级比[2] 高,将会被优先执行。在这个premain函数中,开发者可以进行对类的各种操作。agentArgs是premain函数得到的程序参数,随同 “ – javaagent”一起传入。与main函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。Inst 是一个java.lang.instrument.Instrumentation的实例,由 JVM 自动传入。用它来注册转换器监控代理,在JVM启动main函数之前进行拦截,切入我们需要执行的逻辑。

  (2)jar文件打包。将这个Java类打包成一 jar文件,并在其中的manifest属性当中加入” Premain-Class”来指定步骤1当中编写的那个带有premain的Java 类。

  (3)运行。运行Java程序时增加如下的JVM启动参数:java -javaagent:<jar文件的位置> [= 传入premain的参数]

  首先需要创建字节码转换器,使用java.lang.instrument.ClassFileTransformer。该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码。下面是完整代码:

清单九:使用JDK Instrument实现字节码转换

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package instrumentationexample;  
  2.   
  3. import java.io.IOException;  
  4. import java.lang.instrument.ClassFileTransformer;  
  5. import java.lang.instrument.IllegalClassFormatException;  
  6. import java.lang.instrument.Instrumentation;  
  7. import java.security.ProtectionDomain;  
  8. import javassist.CannotCompileException;  
  9. import javassist.ClassPool;  
  10. import javassist.CtClass;  
  11. import javassist.CtMethod;  
  12. import javassist.NotFoundException;  
  13.   
  14. public class MyClassFileTransformer implements ClassFileTransformer {  
  15.   
  16.     /** 
  17.      * 字节码加载到虚拟机前会进入这个方法 
  18.      */  
  19.     @Override  
  20.     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,  
  21.             ProtectionDomain protectionDomain, byte[] classfileBuffer)  
  22.             throws IllegalClassFormatException {  
  23.         System.out.println(className);  
  24.         //如果加载Business类才拦截     
  25.         if (!"instrumentationexample/Business".equals(className)) {  
  26.             return null;  
  27.         }  
  28.         //javassist的包名是用点分割的,需要转换下     
  29.         if (className.contains("/")) {  
  30.             className = className.replaceAll("/"".");  
  31.         }  
  32.         try {  
  33.             //通过包名获取类文件     
  34.             CtClass cc = ClassPool.getDefault().get(className);  
  35.             //获得指定方法名的方法     
  36.             CtMethod m = cc.getDeclaredMethod("doSomeThing");  
  37.             //在方法执行前插入代码     
  38.             m.insertBefore("{ System.out.println(\"记录日志\"); }");  
  39.             return cc.toBytecode();  
  40.         } catch (NotFoundException | CannotCompileException | IOException e) {  
  41.             //忽略异常处理  
  42.         }  
  43.         return null;  
  44.     }  
  45.   
  46.     public static void premain(String options, Instrumentation ins) {  
  47.         //注册我自己的字节码转换器     
  48.         ins.addTransformer(new MyClassFileTransformer());  
  49.     }  
  50.   
  51.     public static void main(String[] args) {  
  52.         new Business().doSomeThing();  
  53.         new Business().doSomeThing2();  
  54.     }  
  55. }  
  56.   
  57. class Business {  
  58.   
  59.     public boolean doSomeThing() {  
  60.         System.out.println("执行业务逻辑");  
  61.         return true;  
  62.     }  
  63.   
  64.     public void doSomeThing2() {  
  65.         System.out.println("执行业务逻辑2");  
  66.     }  
  67. }  
  JDK instrument包中的ClassFileTransformer用作字节码转换器,其transform()方法用于在JVM加载类的字节码时进行拦截,它使用Javassist在Business类的方法插入日志代码。在premain函数中通过Instrumentation注册我们定义的字节码转换器,该方法在main函数之前执行。

  我们需要将这个有premain函数的类打包成InstrumentationExample.jar,并修改该jar包里的META-INF\MANIFEST.MF文件,加入Premain-Class属性来指定premain所在的类。

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Manifest-Version: 1.0  
  2. Premain-Class: instrumentationexample.MyClassFileTransformer  
  用这个包做Instrumentation代理,就可以在运行Business类的doSomeThing方法时,织入日志记录逻辑。运行结果如下:
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. C:\dist>java -javaagent:InstrumentationExample.jar -jar InstrumentationExample.jar  
  2. java/lang/invoke/MethodHandleImpl  
  3. java/lang/invoke/MemberName$Factory  
  4. java/lang/invoke/LambdaForm$NamedFunction  
  5. java/lang/invoke/MethodType$ConcurrentWeakInternSet  
  6. java/lang/invoke/MethodHandleStatics  
  7. java/lang/invoke/MethodHandleStatics$1  
  8. java/lang/invoke/MethodTypeForm  
  9. java/lang/invoke/Invokers  
  10. java/lang/invoke/MethodType$ConcurrentWeakInternSet$WeakEntry  
  11. java/lang/Void  
  12. java/lang/IllegalAccessException  
  13. sun/misc/PostVMInitHook  
  14. sun/launcher/LauncherHelper  
  15. sun/launcher/LauncherHelper$FXHelper  
  16. instrumentationexample/Business  
  17. 记录日志  
  18. 执行业务逻辑  
  19. 执行业务逻辑2  
  20. java/lang/Shutdown  
  21. java/lang/Shutdown$Lock  
  22.   
  23. C:\dist>  
  执行main函数,你会发现切入的代码无侵入性的织入进去了。从输出中可以看到系统类加载器加载的类也经过了这里。 当然,程序运行的main函数不一定要放在premain所在的这个jar文件里面,这里只是为了例子程序打包的方便而放在一起的。


4 AOP实战


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

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

  以下实战是我在询盘管理的天使瀑布项目中使用AOP实现的一个简单的方法监控。代码不是很复杂,关键是将监控代码和业务代码的分离和复用。(解释:询盘enquiry,又称询价,是指交易的一方为购买或出售某种商品,向对方口头或书面发出的探询交易条件的过程。其内容可繁可简,可只询问价格,也可询问其他有关的交易条件。询盘对买卖双方均无约束力,接受询盘的一方可给予答复,亦可不做回答。但作为交易磋商的起点,商业习惯上,收到询盘的一方应迅速作出答复。常用于国际贸易、电子商务的交易 )


4.1 方法监控


  我使用Spring AOP监控询盘生成方法的调用次数,以便于观察整个询盘生成的过程。设计思路如下:

图7 用AOP统计询盘生成的次数

  每个方法调用成功后,统计调用次数并存入缓存服务器,每天晚上11点50分从缓存服务器中获取数据并存入数据库。因为每天的方法调用次数近百万,为了降低数据库压力不能实时入库。

  只要配置了注解的方法将会被统计调用次数,有的方法需要方法调用成功后才记录,而下面这个方法要求返回值为false才记录:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @MethodInvokeTimesMonitor(value = "KEY_FILTER_NUM", returnValue = false)  
  2. public boolean evaluateMsg(String message) {}  
  我使用的是Spring2.5.5和AspectJ的方式来配置AOP,首先需要启用对AspectJ的支持。

启动AOP

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. xsi:schemaLocation="  
  2. http://www.springframework.org/schema/aop  
  3. http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"  
  4.   
  5. <!--启用对aspectJ的支持-->  
  6. <aop:aspectj-autoproxy proxy-target-class="true"/>  
   proxy-target-class设置为true表示让Spring使用CGlib来实现AOP,配置为false表示使用动态代理实现AOP,默认使用动态代理。其次定义@MethodInvokeTimesMonitor注解。

定义注解

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Retention(RetentionPolicy.RUNTIME)  
  2. @Target(ElementType.METHOD)  
  3. public @interface MethodInvokeTimesMonitor {   
  4.   
  5.     /**  
  6.      * 监控名称,和数据库存储字段名称保持一致 
  7.      */  
  8.     String value();  
  9.   
  10.     /** 
  11.      * 要求返回值为空或等returnValue才记录 
  12.      */   
  13.     boolean returnValue() default true;   
  14. }  
  最后定义一个切面,在切面中定义拦截的方法和在方法返回后记录调用次数的Advice,我们在这里定义了拦截所有配置了注解的方法。
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Aspect  
  2. public class MethodAspect {  
  3.   
  4.     @Resource  
  5.     private XpCacheClient eqUserFloattedCacheClient;  
  6.   
  7.     /** 
  8.      * 切入点,所有配置MethodInvokeTimesMonitor注解的方法 
  9.      */  
  10.     @Pointcut("@annotation(com.alibaba.myalibaba.eq.monitor.MethodInvokeTimesMonitor)")  
  11.     public void allMethodInvokeTimesMonitor() {  
  12.     }  
  13.   
  14.     /** 
  15.      * 统计方法的调用次数 
  16.      * @param methodInvokeTimesMonitor 注解传递的参数 
  17.      */  
  18.     @AfterReturning(value = "MethodAspect.allMethodInvokeTimesMonitor() && @annotation(methodInvokeTimesMonitor)",   
  19.             returning = "retVal")  
  20.     public void statInvokeTimes(MethodInvokeTimesMonitor methodInvokeTimesMonitor, Object retVal) {  
  21.         String name = methodInvokeTimesMonitor.value();  
  22.         //获取方法的返回值  
  23.         boolean returnValue = methodInvokeTimesMonitor.returnValue();  
  24.         //如果返回值不为空,则判断返回值是否和要求的返回值一致,如果一致则记录调用次数  
  25.         if (retVal != null && retVal instanceof Boolean && ((Boolean) retVal == returnValue)) {  
  26.             statInvokeTimes(name);  
  27.         }  
  28.         //如果无返回值,则直接记录调用次数  
  29.         if (retVal == null) {  
  30.             statInvokeTimes(name);  
  31.         }  
  32.     }  
  33.   
  34.     private void statInvokeTimes(String name) {  
  35.         //只缓存当天的数据  
  36.         String key = getCacheKey(name);  
  37.         //没有则为1,有则自增长1  
  38.         Integer num = eqUserFloattedCacheClient.get(key);  
  39.         if (num == null) {  
  40.             eqUserFloattedCacheClient.put(key, 1);  
  41.         } else {  
  42.             eqUserFloattedCacheClient.syncPut(key, ++num);  
  43.         }  
  44.     }  
  45.   
  46.     private String getCacheKey(String name) {  
  47.         return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "_" + name;  
  48.     }  
  49. }  
  @Pointcut用于定义切入点表达式,为了表达式可以复用,所以在单独的方法上配置。@AfterReturning表示在方法执行后进行切入,里面的MethodAspect.allMethodInvokeTimesMonitor()表示使用这个方法的切入点表达式,而@annotation(methodInvokeTimesMonitor)表示将当参数传递给statInvokeTimes方法,returning = "retVal"则表示将被切入方法的返回值赋值给retVal,并传递给statInvokeTimes方法。
  定义MethodAspect切面为Spring的Bean,如果不配置则AOP不会生效。
[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <bean class="com.alibaba.myalibaba.eq.commons.monitor.MethodAspect"/>  
  Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点, 第一个只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。 第二个同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。 第三个性能不是最好的,从上面我们得知使用自定义类加载器,性能要优于动态代理和CGlib。
  可以获取代理类:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public IMsgFilterService getThis() {     
  2.     return (IMsgFilterService) AopContext.currentProxy();     
  3. }     
  4.     
  5. public boolean evaluateMsg () {     
  6.     // 执行此方法将织入切入逻辑     
  7.     return getThis().evaluateMsg(String message);     
  8. }     
  9.     
  10. @MethodInvokeTimesMonitor("KEY_FILTER_NUM")     
  11. public boolean evaluateMsg(String message) {     
  不能获取代理类:
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public boolean evaluateMsg () {     
  2.     // 执行此方法将不会织入切入逻辑     
  3.     return evaluateMsg(String message);     
  4. }     
  5.     
  6. @MethodInvokeTimesMonitor("KEY_FILTER_NUM")     
  7. public boolean evaluateMsg(String message) {     


本文转自:http://www.iteye.com/topic/1116696

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值