AOP原理

概念

  • AOP(Aspect-OrientedProgramming,面向方面编程),当我们需要为分散的对象引入公共行为的时候,OOP显得无能为力。

  • OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。

  • 对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

  • 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 事务

术语

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

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

实现原理

JDK动态代理

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

接口动态代理

如何使用动态代理来实现AOP

下面的例子演示在方法执行前织入一段记录日志的代码,其中Business是代理类,LogInvocationHandler是记录日志的切面,IBusiness, IBusiness2是代理类的接口,Proxy.newProxyInstance是织入器。

public static void main(String[] args) {   
   // TODO Auto-generated method stub
        BusinessImp business = new BusinessImp();
        // 1.获取对应的ClassLoader
        ClassLoader classLoader = business.getClass().getClassLoader();
        // 2.获取ElectricCar 所实现的所有接口
        Class[] interfaces = business.getClass().getInterfaces();
        // 3.设置一个来自代理传过来的方法调用请求处理器,处理所有的代理对象上的方法调用
        InvocationHandler handler = new LogInvocationHandler(business);
        /*
         * 4.根据上面提供的信息,创建代理对象 在这个过程中, a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
         * b.然后根据相应的字节码转换成对应的class, c.然后调用newInstance()创建实例
         */
        Object o = Proxy.newProxyInstance(classLoader, interfaces, handler);
        ((InfBusiness1)o).fun1();
        ((InfBusiness2)o).fun2();
}   
  
/**  
* 打印日志的切面  
*/   
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 {   
       System.out.println("You are going to invoke " + method.getName()
                    + " ...");
            // 执行原有逻辑
            Object rev = method.invoke(target, args);
            // 执行织入的日志,你可以控制哪些方法执行切入逻辑
            if (method.getName().equals("fun1")) {
                System.out.println("记录日志");
            }
            System.out.println(method.getName()
                    + " invocation Has Been finished...");
            return rev;   
    }   
} 

接口IBusiness和IBusiness2定义省略。下面是业务类,需要代理的类。

public class BusinessImp implements IBusiness, IBusiness2 {   
  @Override
        public void fun2() {
            // TODO Auto-generated method stub
            System.out.println("business fun2");
        }

        /*
         * (non-Javadoc) <p>Description: </p>
         * 
         * @see taop.InfBusiness1#fun1()
         */
        @Override
        public void fun1() {
            // TODO Auto-generated method stub

            System.out.println("business fun1");

        }
}   

动态代理原理

本节将结合动态代理的源代码讲解其实现原理。动态代理的核心其实就是代理对象的生成,即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里。

 // 缓存的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);
        }

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

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

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

//添加接口中定义的方法,此时方法体为空   
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方法。 生成的代理类源码:

public class ProxyBusinessImp 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;   
}   
}   

小结

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

动态字节码生成Cglib

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

动态字节码

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

public static void main(String[] args) {   
        byteCodeGe();   
    }   
  
    public static void byteCodeGe() {   
        //创建一个织入器   
        Enhancer enhancer = new Enhancer();   
        //设置父类   
        enhancer.setSuperclass(BusinessImp.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;   
        }   
    }   

自定义类加载器

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

自定义类加载器

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

类加载器

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

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

其中

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) {   
            }   
        }   
    }   

小结

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

字节码转换

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

构建字节码转换器

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

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

注册转换器

使用premain函数注册字节码转换器,该方法在main函数之前执行。
public class MyClassFileTransformer implements ClassFileTransformer {   
    public static void premain(String options, Instrumentation ins) {   
        //注册我自己的字节码转换器   
        ins.addTransformer(new MyClassFileTransformer());   
}   
}   

配置和执行

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

Manifest-Version: 1.0
Premain-Class: bci. MyClassFileTransformer
然后在JVM的启动参数里加上。javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容