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是织入器。
清单一:动态代理的演示

  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. public static class LogInvocationHandler implements InvocationHandler {
  18. private Object target; //目标对象
  19. LogInvocationHandler(Object target) {
  20. this.target = target;
  21. }
  22. @Override
  23. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  24. //执行原有逻辑
  25. Object rev = method.invoke(target, args);
  26. //执行织入的日志,你可以控制哪些方法执行切入逻辑
  27. if (method.getName().equals("doSomeThing2")) {
  28. System.out.println("记录日志");
  29. }
  30. return rev;
  31. }
  32. }
  33. 接口IBusiness和IBusiness2定义省略。

业务类,需要代理的类。

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

输出

  1. 执行业务逻辑2
  2. 记录日志
  3. 执行业务逻辑

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

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

  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里。

  清单三:查找代理类。

  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. }

代理类的生成主要是以下这两行代码。

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

  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来一探究竟,以下是其核心代码的分析。
清单五:代理类的生成过程

  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. //添加一个带有InvocationHandler的构造方法
  9. MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
  10. //循环生成方法体代码(省略)
  11. //方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略)
  12. this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")
  13. //将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。
  14. localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");
  15. localFileOutputStream.write(this.val$classFile);

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

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

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

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

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

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

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

  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);

清单九:类加载监听器

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

输出:

  1. 执行业务逻辑2
  2. 记录日志
  3. 执行业务逻辑

其中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加入记录日志的代码。

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

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

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

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

  1. Manifest-Version: 1.0
  2. Premain-Class: bci. MyClassFileTransformer

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

             3.4.4 输出

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

  1. public static void main(String[] args) {
  2. new Business().doSomeThing();
  3. new Business().doSomeThing2();
  4. }

输出

  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

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

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

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

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

可以获取代理类

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

不能获取代理类

AOP的实现原理的更多相关文章

  1. Spring aop的实现原理

    简介 前段时间写的java设计模式--代理模式,最近在看Spring Aop的时候,觉得于代理模式应该有密切的联系,于是决定了解下Spring Aop的实现原理. 说起AOP就不得不说下OOP了,OO ...

  2. Spring AOP异常捕获原理

    Spring AOP异常捕获原理:        被拦截的方法,须显式的抛出异常,且不能做任何处理, 这样AOP才能捕获到方法中的异常,进而进行回滚.        换句话说,就是在Service层的 ...

  3. AOP切面实现原理以及多个切面切同一个地方时的优先级讲解

    此博文的编写,源于前段时间的惨痛面试经历.刚好近几天尘埃落定.手头事少,遂总结一二,与各位道友分享,欢迎吐槽指正.今年年初的这段面试经历,已于之前的博文中 整理发出(https://www.cnblo ...

  4. 漫画 | Spring AOP的底层原理是什么?

    1.Spring中配置的bean是在什么时候实例化的? 2.描述一下Spring中的IOC.AOP和DI IOC和AOP是Spring的两大核心思想 3.谈谈IOC.AOP和DI在项目开发中的应用场景 ...

  5. 关于spring,IOC和AOP的解析原理和举例

    引用自:https://blog.csdn.net/paincupid/article/details/43152397 IOC:就是DAO接口的实现不再是业务逻辑层调用工厂类去获取,而是通过容器(比 ...

  6. Spring AOP 的实现 原理

    反射实现 AOP 动态代理模式实例说明(Spring AOP 的实现 原理)   比如说,我们现在要开发的一个应用里面有很多的业务方法,但是,我们现在要对这个方法的执行做全面监控,或部分监控.也许我们 ...

  7. 浅析Spring中AOP的实现原理——动态代理

    一.前言   最近在复习Spring的相关内容,刚刚大致研究了一下Spring中,AOP的实现原理.这篇博客就来简单地聊一聊Spring的AOP是如何实现的,并通过一个简单的测试用例来验证一下.废话不 ...

  8. Spring技术内幕:Spring AOP的实现原理(二)

    **二.AOP的设计与实现 1.JVM的动态代理特性** 在Spring AOP实现中, 使用的核心技术时动态代理.而这样的动态代理实际上是JDK的一个特性.通过JDK的动态代理特性,能够为随意Jav ...

  9. AOP的实现原理——动态代理

    IOC负责将对象动态的 注入到容器,从而达到一种需要谁就注入谁,什么时候需要就什么时候注入的效果,可谓是招之则来,挥之则去.想想都觉得爽,如果现实生活中也有这本事那就爽 歪歪了,至于有多爽,各位自己脑 ...

随机推荐

  1. 那些年【深入.NET平台和C#编程】

    一.深入.NET框架 1..NET框架具有两个组件:CLR(公共语言运行时)和FCL(框架类库),CLR是.NET框架的基础 2.框架核心类库: System.Collections.Generic: ...

  2. Mysql - 增删改

    因为项目原因, mysql用了两年了, 但是一直都未曾去总结过. 最近也是领导让总结项目, 才想起把mysql的使用小结一下. 一. Create 1. 单条插入, sql格式: insert int ...

  3. Firebug中调试中的js脚本中中文内容显示为乱码

    Firebug中调试中的js脚本中中文内容显示为乱码 设置 页面 UFT-8 编码没用, 解决方法:点击 "Firebug"工具栏 中的"选项"---" ...

  4. CSS笔记

    初级篇===========================选择器============================元素选择器css:h1{color: red}html:<h1> ...

  5. Centos6.x 下安装Jexus独立版

    操作步骤: #cd /tmp #wget linuxdot.net/down/jexus-5.8.1-x64.tar.gz 注:如果有新版本,则修改为相应版本号即可. #tar -zxvf jexus ...

  6. .NET Core dotnet 命令大全

    dotnet 命令大全,让你理解dotnet 命令. 本文将以一个实例串起 dotnet 所有命令,让你玩转dotnet 命令. 本篇文章编写环境为windows 10 ,dotnet 命令同样适用于 ...

  7. 从零开始,DIY一个jQuery(1)

    从本篇开始会陪大家一起从零开始走一遍 jQuery 的奇妙旅途,在整个系列的实践中,我们会把 jQuery 的主要功能模块都了解和实现一遍. 这会是一段很长的历程,但也会很有意思 —— 作为前端领域的 ...

  8. ABP(现代ASP.NET样板开发框架)系列之8、ABP日志管理

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之8.ABP日志管理 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)” ...

  9. Microsoft Visual Studio 2013 — Project搭载IIS配置的那些事

    前段时间在改Bug打开一个project时,发生了一件奇怪的事,好好的一直不能加载solution底下的这个project,错误如下图所示:大致的意思就是这个project的web server被配置 ...

  10. mysql交互协议解析——mysql包基础数据、mysql包基本格式

    mysql交互协议是开发mysql周边组件常用的协议,如JDBC,libmysql等等. 在此我们要认识到mysql交互协议其实是半双工的交互协议,至于为什么,这里就先挖个小坑,以后再填. 在探讨my ...