但是存在一个问题,随着打印日志的需求增多,Proxy 类越来越多,我们能不能保持只有一个代理呢?这时候我们就需要用到 JDK 动态代理了。
第三种方式:动态代理
新建动态代理类
客户端调用
这又引出一个问题,日志打印和业务逻辑耦合在一起,我们希望把前置和后置抽离出来,作为单独的增强类。
第四种方式:动态代理 + 分离增强类
新建增强类接口和实现类
用反射代替写死方法,解耦代理和操作者
客户端调用
但是用了反射性能太差了,而且动态代理用起来也不方便,有没有更好的办法?
我们发现 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 提供了两套对切面的描述方法:
- 我们常用的基于 java 注解切面描述的方法,写起来十分方便,兼容 Java 语法;
@Aspect
public class AnnoAspect {
@Pointcut(“execution(…)”)
public void jointPoint() {
}
@Before(“jointPoint()”)
public void before() {
//…
}
@After(“jointPoint()”)
public void after() {
//…
}
}
- 基于 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
- 自定义 AutoLog 注解
- 编写 LogAspect 切面
- 在切入点中加上注解
反编译后(请点开大图查看)
发现 AspectJ 会把调用切面的方法插入到切入点中,且封装了切入点所在的方法名、所在类、入参名、入参值、返回值等等信息,传递给切面,这样就建立了切面和业务代码的关联。
我们跟进 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint);
一探究竟。
我们发现了什么?其实 Before 和 After 的插入就是在匹配到的 JoinPoint 调用前后插入 Advise 方法,以此来达到拦截目标 JoinPoint 的作用。 如下图所示:
(2)@Around
机制
- 自定义 SingleClick 注解
- 编写 SingleClickAspect 切面
- 业务方加上注解
打开编译后的 class 文件(请点开大图查看)
我们发现和 Before、After 织入不一样了!前者的织入只是在匹配的 JoinPoint 前后插入 Advise 方法,仅仅是插入。而 Around 拆分了业务代码和 Advise 方法,把业务代码迁移到新函数中,通过一个单独的闭包拆分来执行,相当于对目标 JoinPoint 进行了一个代理,所以 Around 情况下我们除了编写切面逻辑,还需要手动调用 joinPoint.proceed() 来调用闭包执行原方法。
我们看下 proceed() 都做了些什么
那这个 arc 是什么?什么时候拿到的呢?
继续回溯
在 AroundClosure 闭包中,会把运行时对象和当前连接点 joinPoint 对象传入,调用 linkClosureAndJoinPoint() 绑定两端,这样在 Around 中就可以通过 ProceedingJoinPoint.proceed() 调用 AroundClosure,进而调用到目标方法了。
那么一图总结 Around 机制:
我们从 AspectJ 编译后的 class 文件可以明显看出执行的逻辑,proceed 方法就是回调执行被代理类中的方法。
所以 AspectJ 做的事情如下:
最后
简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。
选定你想去的几家公司后,先去一些小的公司练练,学习下面试技巧,总结下,也算是熟悉下面试氛围,平时和同事或者产品PK时可以讲得头头是道,思路清晰至极,到了现场真的不一样,怎么描述你所做的一切,这绝对是个学术性问题!
面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
droid高级架构师进阶必备的一些学习技能。**
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-nRfFpIV7-1720122140425)]