• AOP与JAVA动态代理


    1、AOP的各种实现

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

    • 在编译期修改源代码
    • 在运行期字节码加载前修改字节码
    • 在运行期字节码加载后动态创建代理类的字节码

    2、AOP各种实现机制的比较

    以下是各种实现机制的比较:

    类别机制原理优点缺点
    静态AOP 静态织入 在编译期,切面直接以字节码的形式编译到目标字节码文件中 对系统无性能影响 灵活性不够
    动态AOP 动态代理 在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中 相对于静态AOP更加灵活

    切入的关注点需要实现接口。

    对系统有一点性能影响

    动态字节码生成 CGLIB 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 没有接口也可以织入 扩展类的实例方法为final时,则无法进行织入
    自定义类加载器   在运行期,目标加载前,将切面逻辑加到目标字节码里 可以对绝大部分类进行织入 代码中如果使用了其他类加载器,则这些类将不会被织入
    字节码转换   在运行期,所有类加载器加载字节码前进行拦截 可以对所有类进行织入  

     

    3、AOP里的公民

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

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

    4、AOP的实现机制

    • 动态代理
    • 动态字节码生成
    • 自定义类加载器
    • 字节码转换
     4.1 动态代理

    静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了

    动态代理:即在运行期动态创建代理类,使用动态代理实现AOP需要4个角色:

    • 被代理的类:即AOP里所说的目标对象
    • 被代理类的接口
    • 织入器:使用接口反射机制生成一个代理类,在这个代理类中织入代码
    • InvocationHandler切面:切面,包含了Advice和Pointcut

    4.1.1 动态代理的演示

    例子演示的是在方法执行前织入一段记录日志的代码,其中

    • Business是代理类
    • LogInvocationHandler是记录日志的切面
    • IBusiness、IBusiness2是代理类的接口
    • Proxy.newProxyInstance是织入器
    复制代码
    public interface IBusiness {
        void doSomeThing();
    }
    
    public interface IBusiness2 {
        void doSomeThing2();
    }
    
    public class Business implements IBusiness, IBusiness2 {
        @Override
        public void doSomeThing() {
            System.out.println("执行业务逻辑");
        }
    
        @Override
        public void doSomeThing2() {
            System.out.println("执行业务逻辑2");
        }
    }
    复制代码
    复制代码
    package aop;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    /**
     * 打印日志的切面
     */
    public class LogInvocationHandler implements InvocationHandler {
    
        private Object target;//目标对象
    
        public LogInvocationHandler(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //执行织入的日志,你可以控制哪些方法执行切入逻辑
            if (method.getName().equals("doSomeThing2")) {
                System.out.println("记录日志");
            }
            //执行原有逻辑
            Object recv = method.invoke(target, args);
            return recv;
        }
    }
    复制代码
    复制代码
    package aop;
    
    import java.lang.reflect.Proxy;
    
    
    public class Main {
        public static void main(String[] args) {
            //需要代理的类接口,被代理类实现的多个接口都必须在这这里定义
            Class[] proxyInterface = new Class[] {IBusiness.class, IBusiness2.class};
            //构建AOP的Advice,这里需要传入业务类的实例
            LogInvocationHandler handler = new LogInvocationHandler(new Business());
            //生成代理类的字节码加载器
            ClassLoader classLoader = Business.class.getClassLoader();
            //织入器,织入代码并生成代理类
            IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
            proxyBusiness.doSomeThing2();
            ((IBusiness)proxyBusiness).doSomeThing();
        }
    }
    复制代码

    执行结果:

    记录日志
    执行业务逻辑2
    执行业务逻辑

    4.1.2 动态代理的原理

    本节将结合动态代理的源代码讲解其实现原理

    动态代理的核心其实就是代理对象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)

    让我们进入newProxyInstance方法观摩下,核心代码就三行:

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

    getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:

    • 在当前类加载器的缓存里搜索是否有代理类
    • 没有则生成代理
    • 并缓存在本地JVM里

    查找代理类getProxyClass(loader, interfaces)方法:

    复制代码
     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 } 
    复制代码

    生成加载代理类:

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

    代理类生成过程ProxyGenerator.generateProxyClass()方法的核心代码分析:

    复制代码
    //添加接口中定义的方法,此时方法体为空 
    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); 
    复制代码

    通过以上分析,我们可以推出动态代理为我们生产了一个这样的代理类。把方法soSomeThing的方法体修改为调用LogInvocationHandler的invoke方法

    代码如下:

    复制代码
    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(); 
    } 
    }  
    复制代码
    4.1.3 小结

    从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题:

    • 第一,代理类必须实现一个接口,如果没实现接口会抛出一个异常
    • 第二,性能影响,因为动态代理是使用反射机制实现的,首先反射肯定比直接调用要慢,其次使用反射大量生成类文件可能引起full gc,因为字节码文件加载后会存放在JVM运行时方法区(或者叫永久代、元空间)中,当方法区满时会引起full gc,所以当你大量使用动态代理时,可以将永久代设置大一些,减少full gc的次数
    4.2 CGLIB动态字节码生成

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

    本节介绍如何使用cglib来实现动态字节码技术。

    cglib是一个强大的、高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用cglib前需要引入Asm的jar

    4.2.1 使用cglib实现AOP
    复制代码
     1 package cglib;
     2 
     3 /**
     4  * 这个是没有实现接口的实现类
     5  */
     6 public class BookFacadeImpl {
     7     public void addBook() {
     8         System.out.println("增加图书的普通方法。。。");
     9     }
    10 
    11     public void deleteBook() {
    12         System.out.println("删除图书的普通方法。。。");
    13     }
    14 }
    复制代码
    复制代码
     1 package cglib;
     2 
     3 import net.sf.cglib.proxy.Enhancer;
     4 import net.sf.cglib.proxy.MethodInterceptor;
     5 import net.sf.cglib.proxy.MethodProxy;
     6 
     7 import java.lang.reflect.Method;
     8 
     9 /**
    10  * 使用cglib动态代理
    11  */
    12 public class BookFacadeCglib implements MethodInterceptor {
    13 
    14     private Object target;
    15 
    16     /**
    17      * 创建代理对象
    18      *
    19      * @param target
    20      * @return
    21      */
    22     public Object getInstance(Object target) {
    23         this.target = target;
    24         Enhancer enhancer = new Enhancer();
    25         enhancer.setSuperclass(this.target.getClass());
    26         //回调方法
    27         enhancer.setCallback(this);
    28         //创建代理
    29         return enhancer.create();
    30     }
    31 
    32     //回调方法
    33     @Override
    34     public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    35         if (method.getName().equals("addBook")) {
    36             System.out.println("记录增加图书的日志");
    37         }
    38         methodProxy.invokeSuper(obj, args);
    39         return null;
    40     }
    41 }
    复制代码
    复制代码
    package cglib;
    
    /**
     * 测试cglib字节码代理
     */
    public class TestCglib {
        public static void main(String[] args) {
            BookFacadeCglib cglib = new BookFacadeCglib();
            BookFacadeImpl bookFacade = (BookFacadeImpl) cglib.getInstance(new BookFacadeImpl());
            bookFacade.addBook();
            bookFacade.deleteBook();
        }
    }
    复制代码

    执行结果:

    记录增加图书的日志
    增加图书的普通方法。。。
    删除图书的普通方法。。。

    4.3 自定义类加载器

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

    Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法

    这比使用cglib实现AOP更加高效,并且没有太多限制,实现原理如下图:

    我们使用类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑

    4.3.1 Javassist实现AOP的代码

    清单1:启动自定义的类加载器

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

    清单2:类加载监听器

    复制代码
    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(); 
            } 
        } 
    复制代码

    输出:

    执行业务逻辑2   

    记录日志   

    执行业务逻辑 

    4.3.2 小结

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

    4.4 字节码转换

    自定义类加载器实现AOP只能拦截自己加载的字节码,那么有一种方式能够监控所有类加载器加载的字节码吗?

    有,使用Instrumentation,它是Java5的新特性,使用Instrument,开发者可以构建一个字节码转换器,在字节码加载前进行转换

    本节使用Instrumentation和javassist来实现AOP

    4.4.1 构建字节码转换器

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

    复制代码
     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 } 
    复制代码
    4.4.2 注册转换器

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

    public class MyClassFileTransformer implements ClassFileTransformer { 
        public static void premain(String options, Instrumentation ins) { 
            //注册我自己的字节码转换器 
            ins.addTransformer(new MyClassFileTransformer()); 
    } 
    } 
    4.4.3 配置和执行

    需要告诉JVM在启动main函数之前,需要先执行premain函数。

    首先,需要将premain函数所在的类打成jar包,并修改jar包里的META-INFMANIFEST.MF文件

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

    其次,在JVM的启动参数里加上-javaagent:D:javaprojectsopencometProjectAoplibaop.jar 

    4.4.4 输出

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

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

    输出:

    复制代码
     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 
    复制代码

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

    5、AOP实战

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

    Spring默认采取动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用cglib机制

    但Spring的AOP有一定的缺点:

    • 第一,只能对方法进行切入,不能对接口、字段、静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法都将被切入)
    • 第二,同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean
    • 第三,性能不是最好的。从前面几节得知,我们自定义的类加载器,性能优于动态代理和cglib
    复制代码
    public IMsgFilterService getThis() { 
      return (IMsgFilterService) AopContext.currentProxy(); 
    } 
    
    public boolean evaluateMsg () { 
      // 执行此方法将织入切入逻辑 
      return getThis().evaluateMsg(String message); 
    } 
    
    @MethodInvokeTimesMonitor("KEY_FILTER_NUM") 
    public boolean evaluateMsg(String message) { 
    复制代码
    复制代码
    public boolean evaluateMsg () { 
       // 执行此方法将不会织入切入逻辑 
      return evaluateMsg(String message); 
    } 
    
    @MethodInvokeTimesMonitor("KEY_FILTER_NUM") 
    public boolean evaluateMsg(String message) { 
    复制代码

    原文转载:http://www.cnblogs.com/xiaoxiao7/p/6057724.html#3881742

  • 相关阅读:
    CSP游戏 4
    CSP 交通规划
    CSP 地铁修建
    CSP 通信网络
    CSP URL映射
    CSP 权限查询
    CSP Markdown
    CSP JSON 查询
    SQL里的子查询
    SQL里的操作符
  • 原文地址:https://www.cnblogs.com/AndyAo/p/8509339.html
Copyright © 2020-2023  润新知