Java中的AOP(原理)

这是aop体系化学习过程中的第一篇-原理篇, 偏向aop实现中运用到的原理.接下来将陆续发布应用篇, 整理spring中的aop应用. 最后发布实现篇, 尝试自己手写一个动态代理框架, 实现aop. 文章如有不足之处欢迎讨论哦, 一起进步.


大纲

  • AOP入门介绍
  • AOP分类
    • 静态代理
    • 动态代理
  • AOP实现
    • AspectJ
    • JDK动态代理
    • CGLIB
    • JDK动态代理和CGLIB的区别
    • 三种aop框架对比
  • 更好地理解静态代理和动态代理的区别

AOP入门介绍

aop(aspect oriented programming 面向切面编程)是一种编程思想
典型的应用场景如下: 比如我们写一个银行存款的业务

1.aop 记录日志: a准备向b汇款(AOP类)
2.把a的100块钱存入b的账户(业务类)
3.aop 记录日志:a向b汇款成功/失败(AOP类)

步骤1,3就是aop思想的实现, 我们在一个类中写好汇款业务(步骤2). 在另一个类中运用aop, 在汇款前后记录日志(步骤1,3), 完全不干预存款业务的代码逻辑.
aop还有更多的应用场景, 例如请求到达业务类之前, 判断请求是否合理, 用户是否有权限执行这个请求, 在请求离开业务类, 发送到服务器之前, 使用aop统一编码数据格式, 统一加密返回信息. 数据缓存等等...
对于真正执行对应业务的类来说, 它只负责它应该做的事情, 无需知道请求到来之前已经经过了一系列筛选流程, 所以业务类是独立的.

下图中, 绿色矩形代表切面逻辑(aop), 红色矩形代表用户关注的业务逻辑


aop能做的事情

在实际开发中, 可能基础功能才是首要目的, 所以基础功能应该首先开发完成. 在这之后开发人员再基于业务逻辑开发切面逻辑, 例如加密解密, 过滤器, 权限验证, 日志记录等等(不过目前的开发应用中加密解密使用的比较多的是拦截过滤器). 有人可能会问, 为什么要把这些功能分开? 首先是为了解耦, 将各个业务逻辑分离开有利于开发人员定位错误, 同时也符合了面向对象开发中单一职责的原则.


AOP分类

根据静态编译时和运行时两种环境, aop可以分为静态代理动态代理
静态代理主要涉及AspectJ, 动态代理主要涉及Spring AOP, CGLIB.
静态代理与动态代理的区分点主要在于: aop框架(spring aop, aspectj ...)能否在程序运行时动态地创建aop代理.(后面详细解释这句话)


AOP实现

aop是一种编程思想, 弥补了oop的不足. 在java中, 有三个主要的框架实现了aop思想

1. AspectJ

AspectJ主要作用于编译时增强, 也称之为静态代理, 我们在写完一段独立的业务方法saveData()时, 可以使用aspectJ将切面逻辑织入到saveData()中. 比如日志记录.
在使用aspectJ编译代码之后, 我们的class文件中会多出一段代码, 这段代码是aspectJ在编译时增加的aop代码. AspectJ的这种做法可以被称为静态代理
示例如下

public class DataService {
    public void saveData() {
        //...
        System.out.println(" save data...");
    }
 
    public static void main(String[] args) {
        DataService service = new DataService ();
        service.saveData();
    }
}

以上是我们主要关注的逻辑
以下是使用aspect进行切面逻辑的编写

public aspect DataAspect {
    void around():call(void DataService.saveData()){
        System.out.println("before saving data ...");
        //执行saveData方法
        proceed();
        System.out.println("after saving data ...");
    }
}

在编译上面的DataService之后
class文件发生了变化

public class DataService {
    public void saveData() {
        //...
        System.out.println(" save data...");
    }
 
    public static void main(String[] args) {
        DataService service = new DataService ();
        //这里是发生变化的代码
        saveData_aroundBody1$advice(service, DataAspect.aspectOf(), (AroundClosure)null);
    }
    //这里是aspectJ在编译时添加的代码
    public void ajc$around$com_qcq_aop_DataAspect$1$f54fe983(AroundClosure ajc$aroundClosure) {
        System.out.println("before saving data ...");
        ajc$around$com_qcq_aop_DataAspect$1$f54fe983proceed(ajc$aroundClosure);
        System.out.println("after saving data ...");
    }
}

从以上示例我们可以看出:
Aspect在编译期, 为被代理方法织入我们在aspect中定义好的切面逻辑, 以添加字节码的方式(强行添加代码)

2. JDK动态代理

jdk动态代理使用jdk自带的反射机制来完成aop的动态代理, 使用jdk自带的动态代理有如下要求:
1.被代理类(我们的业务类)需要实现统一接口
2.代理类要实现reflect包里面的接口InvocationHandler
3.通过jdkProxy提供的静态方法newProxyInstance(xxx)动态创建代理类
下面是具体例子
下面定义了一个统一被代理类接口

public interface IService {
    void save();
}

下面是接口实现类

public class UserService implements IService{
    @Override
    public void save() {
        System.out.println("save a user ...");
    }
}

下面是InvocationHandler的实现

public class MyHandler implements InvocationHandler {
    private Object target;

    MyHandler(Object target) {
        this.target = target;
    }
    //在这里实现我们的切面逻辑
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before ...");
        Object result = method.invoke(target, args);
        System.out.println("after ...");
        return result;
    }
}

下面是客户端代码

    public static void main(String[] args) {
        //创建被代理类
        IService service = new UserService();
        //新建代理类,将被代理类注册到里面
        MyHandler aop = new MyHandler(service);
        //Proxy为MyHandler动态创建一个符合某一接口的代理实例
                //第一个参数:获取被代理类的class loader
                //第二个参数:获取被代理类实现的接口, 由此可见被代理类需要实现统一的接口
                //第三个参数:InvocationHandler的实例, 也就是我们做切面逻辑的类
        IService proxy = (IService) Proxy
                .newProxyInstance(service.getClass().getClassLoader(),
                        service.getClass().getInterfaces(),
                        aop);
                //调用代理方法
        proxy.save();
    }

以下是输出结果



相信这样子的例子能让我们更好地理解jdk动态代理的使用机制

1.代理类和被代理类使用同样的对象引用, 如上客户端代码所示的serviceproxy(统一性), 因此我们可以神不知鬼不觉地使用我们的真实业务类, 而无需关注在它周围的切面逻辑(独立性), 这样子做的最大好处是, 我们在实际开发过程中, 最先实现基础功能, 然后使用aop编程实现我们的切面逻辑, 例如基于基础功能的日志记录, 缓存管理, 权限验证等等...
2.如果我们调用service里面的方法, 那么方法前后不会有切面逻辑的实现, 如果调用proxy里面的方法, 由于proxy是jdk动态生成的代理类, 与service属于同一个类, 但是里面的方法被调用时, jdk自动为我们实现切面逻辑(动态性)
3.被代理类需要实现统一代理接口, 如上IService(局限性)

3. CGLIB

CGLIB的全称: Code Generation Library
翻译过来就是代码生产库, 与AspectJ的机制不一样, CGLIB可以在代码运行时动态生成某个类的子类, 因此使用CGLIB时, 我们不需要像jdk动态代理一样建立统一代理接口.
先来看一看CGLIB的使用机制
1.定义被代理类(业务逻辑)

public class UserService{
    public void save() {
        System.out.println("save a user ...");
    }
}

2.实现MethodInterceptor接口(切面逻辑)

public class UserProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //方法执行前的逻辑
        System.out.println("before ...");
        //参数o是被代理对象, invokeSuper方法调用父类方法, 在这前后我们可以实现自定义切面逻辑
        Object result = methodProxy.invokeSuper(o, objects);
        //方法执行后的逻辑
        System.out.println("after ...");
        return result;
    }
}

3.获取被代理类Factory工厂, 该工厂专门生产加入了切面逻辑的被代理类

public class UserServiceFactory {
    public static UserService getInstance(UserProxy proxy) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        //设置回调类,强化类调用回调类中的intercept方法来执行我们定义好的切面逻辑
        enhancer.setCallback(proxy);
        //返回一个添加了切面逻辑的增强类
        return (UserService) enhancer.create();
    }
}

Enhancer是一个字节码增强器
4.客户端调用

    public static void main(String[] args) {
        UserProxy proxy=new UserProxy();
        //通过工厂方法模式获取增强类
        UserService service = UserServiceFactory.getInstance(proxy);
        service.save();
    }

执行结果如下


执行结果
4.JDK动态代理和CGLIB的区别

CGLIB使用了底层的字节码技术, 通过字节码处理框架ASM, 在程序运行时为一个类创建子类, 在子类中重写父类的方法, 为父类方法加入切面逻辑(无法代理final修饰的方法). CGLIB创建代理类的代价相对大, 但是运行切面逻辑的速度较快, 适合单例模式.
JDK动态代理使用jdk原生提供的反射机制, 位于sun.reflect包中. 代理类生成过程较快, 但切面逻辑运行速度没有CGLIB动态代理快. 而且被代理类必须基于统一接口, 存在局限性, 实际中并不常用这种方式.

三种aop框架对比
框架 代理机制 作用时间范围 作用原理 优点 缺点 使用频率
AspectJ 静态代理 编译期 编译期嵌入代码 运行速度最快 扩展性差, 无法在运行时创建新代理 不常用
JDK动态代理 动态代理 运行时 被代理类加载进jvm之后, 根据声明的统一接口动态生成代理类, 同时将切面逻辑织入代理类, 也称为java的反射机制 代理类生成速度快 1.需要声明统一代理接口 , 扩展性相对CGLIB差2.相对于直接调用来说, 切面逻辑运行速度慢 3.使用大量反射如果不释放代理对象的引用, 会造成GC负荷大 不常用
CGLIB 动态代理 运行时 通过字节码框架ASM动态生成子类, 重写父类方法, 同时嵌入切面逻辑 1.切面逻辑运行速度快 2.被代理类无需实现统一接口 3.扩展性强 1.代理类生成代价高 2.文档少, 难以深入学习 3.使用大量反射如果不释放代理对象的引用, 会造成GC负荷大 常用

更好地理解静态代理和动态代理的区别

从AspectJ的运行机制中我们可以看出, 我们并没有真正地获取到一个基于被代理对象的代理类, 而是写完切面逻辑之后, 通过AspectJ的编译器, 在程序编译阶段在业务逻辑方法前后添加了相应的字节码, 从而实现我们的aop. 使用AspectJ框架, 并不能在程序运行时进行代理操作, 因为没有代理类, 而且代码已经被写死在业务逻辑中, 谈何动态呢? 因此AspectJ属于静态代理.
jdk动态代理和CGLIB就属于动态代理了
jdk获取代理对象的代码

IService proxy = (IService) Proxy
                .newProxyInstance(service.getClass().getClassLoader(),
                        service.getClass().getInterfaces(),
                        aop);

CGLIB获取增加了切面逻辑的对象(也可称为代理对象)

UserProxy proxy=new UserProxy();
 //通过工厂方法模式获取增强类
UserService service = UserServiceFactory.getInstance(proxy);

我们可以发现: 在编译完成后程序进入运行时状态时, 代理对象还存在于java堆中, 这些代理对象引用可能被GC, 但是我们可以再通过Proxy, 或者工厂来继续生成代理对象, 因此代理类可以在运行时供我们再次使用, 在请求转发到达业务逻辑类时, 我们可以动态创建代理, 代理对象帮我们实现切面逻辑, 请求结束时, 释放代理对象的引用, 反复这个流程, 实现动态性. 而AspectJ编译完形成class文件之后, 并没有所谓的动态代理对象在java堆中, 只是静态的, 不可动态创建的增加了切面逻辑的对象.

动态代理模式并没有那么难以理解, 以上便是java中静态代理和动态代理框架的主要运行机制. 笔者没有介绍设计模式中的代理模式, 感兴趣的同学可以去看看.

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

推荐阅读更多精彩内容

  • 本文主要讲实现AOP的 代理模式原理,以及静态代理,动态代理的区别和具体实现。 对SpringAOP的概念和使用,...
    _Zy阅读 746评论 0 1
  • 前言 只有光头才能变强 上一篇已经讲解了Spring IOC知识点一网打尽!,这篇主要是讲解Spring的AOP模...
    Java3y阅读 6,878评论 8 181
  • 1.AOP概述 1.1.AOP到底是什么 AOP只适合那些具有横切面逻辑的应用场合,如性能监测,访问控制,事务管理...
    小螺钉12138阅读 699评论 0 0
  • 接触嵌入式半年以来学习地非常零碎,没有一个系统的知识体系,因此决定进行一个系统的学习并做记录,如果有错误希望各位指...
    东方未曦阅读 7,807评论 2 9
  • 我成功地被感动了,当我在天寒地冻并且天黑准备开车回家时,因为我看到,车玻璃已经处理得干干净净!回家的路上心情那个美...
    niuniuma阅读 125评论 0 0