为了账号安全,请及时绑定邮箱和手机立即绑定

会用就行了?你知道 AOP 框架的原理吗?

标签:
Android

一、引入

敲一个小 Demo 来引入主题,假设我想不依赖任何 AOP 方法,在特定方法的执行前后加上日志打印。

第一种方式:写死代码

定义一个目标类接口

webp

webp

把 before() 和 after() 方法写死在 execute() 方法体中,非常不优雅,我们改进一下。

第二种方式:静态代理

webp

但是存在一个问题,随着打印日志的需求增多,Proxy 类越来越多,我们能不能保持只有一个代理呢?这时候我们就需要用到 JDK 动态代理了。

第三种方式:动态代理

新建动态代理类

webp

客户端调用

webp

这又引出一个问题,日志打印和业务逻辑耦合在一起,我们希望把前置和后置抽离出来,作为单独的增强类。

第四种方式:动态代理 + 分离增强类

新建增强类接口和实现类

webp

用反射代替写死方法,解耦代理和操作者

webp

客户端调用

webp

但是用了反射性能太差了,而且动态代理用起来也不方便,有没有更好的办法?

我们发现 Demo 存在种种问题

  • 静态代理每次都要自己新建个代理类,太繁琐,重用性又差,一个代理不能同时代理多种类;

  • 动态代理可以重用,但性能太差;

  • 代理类耦合进被代理类的调用阶段,万一我需要改下 before、after 的方法名,可能会点燃一个炸弹;

  • 代理拦截了一个类,就会拦截这个类的所有方法,难道我还要在代理类里加个 if-else 判断特定方法过滤拦截?我们可以不可以只拦截特定的方法?

  • 如果我既要打印日志,又要计算方法执行用时,每次都要去改增强类吗?

我们的诉求很简单:1. 性能高;2. 松耦合;3. 步骤方便;4. 灵活性高。

那主流的 AOP 框架是怎么解决这个问题的呢?我们赶紧来看看!

二、AOP 方法

不同的 AOP 方法原理略微有些不同,我们先看下 AOP 实现方式有哪些:

AOP方式机制说明
静态织入静态代理直接修改原类,比如编译期生成代理类的 APT
静态织入自定义类加载器使用类加载器启动自定义的类加载器,并加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,以 Javassist 为代表
动态织入动态代理字节码加载后,为接口动态生成代理类,将切面植入到代理类中,以 JDK Proxy 为代表
动态织入动态字节码生成字节码加载后,通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用织入逻辑。属于子类代理,以 CGLIB 为代表

所有 AOP 方法本质就是:拦截、代理、反射(动态情况下),实现原理可以看作是代理 / 装饰设计模式的泛化,为什么这么说?我们来详细分析一下。

三、静态织入原理,以 AspectJ 为例

静态织入原理就是静态代理,我们以 AspectJ 为例。

1. AspectJ 设计思路

前面说到 Demo 存在的种种问题,AspectJ 是怎么解决的呢?AspectJ 提供了两套强大的机制:

(1)切面语法 | 解决业务和切面的耦合

AspectJ 中的切面,就解决了这个问题。

@Before("execution(* android.view.View.OnClickListener.onClick(..))")

我们可以通过切面,将增强类与拦截匹配条件(切点)组合在一起,从而生成代理。这把是否要使用切面的决定权利还给了切面,我们在写切面时就可以决定哪些类的哪些方法会被代理,从而逻辑上不需要侵入业务代码

而普通的代理模式并没有做到切面与业务代码的解耦,虽然将切面的逻辑独立进了代理类,但是决定是否使用切面的权利仍然在业务代码中。这才导致了 Demo 中种种的麻烦。

AspectJ 提供了两套对切面的描述方法:

  1. 我们常用的基于 java 注解切面描述的方法,写起来十分方便,兼容 Java 语法;

@Aspectpublic class AnnoAspect {    @Pointcut("execution(...)")    public void jointPoint() {
    }    @Before("jointPoint()")    public void before() {        //...
    }    @After("jointPoint()")    public void after() {        //...
    }
}
  1. 基于 aspect 文件的切面描述方法,这种语法不兼容 Java 语法。

public aspect AnnoAspect {    pointcut XX():            execution(...);
    before(): XX() {        //...
    }
    after(): XX() {        //...
    }
}
(2)织入工具 | 解决代理手动调用的繁琐

那么切面语法让切面从逻辑上与业务代码解耦,但是我要怎么找到特定的业务代码织入切面呢?

两种解决思路:一种就是提供注册机制,通过额外的配置文件指明哪些类受到切面的影响,不过这还是需要干涉对象创建的过程;另外一种解决思路就是在编译期或类加载期先扫描切面,并将切面代码通过某种形式插入到业务代码中。

那 AspectJ 织入方式有两种:一种是 ajc 编译,可以在编译期将切面织入到业务代码中。另一种就是 aspectjweaver.jar 的 agent 代理,提供了一个 Java agent 用于在类加载期间织入切面。

2. 通过 class 反推 AspectJ 实现机制

(1)@Before 机制

国际惯例写个 Demo

  1. 自定义 AutoLog 注解

webp

  1. 编写 LogAspect 切面

webp

  1. 在切入点中加上注解

webp

反编译后(请点开大图查看)

webp

发现 AspectJ 会把调用切面的方法插入到切入点中,且封装了切入点所在的方法名、所在类、入参名、入参值、返回值等等信息,传递给切面,这样就建立了切面和业务代码的关联

我们跟进 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint); 一探究竟。

webp



作者:FeelsChaotic
链接:https://www.jianshu.com/p/cfa16f4cf375



点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 1
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消