最全选型考量 + 剖析经典AOP开源库实践

Posted 郭霖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最全选型考量 + 剖析经典AOP开源库实践相关的知识,希望对你有一定的参考价值。



今日科技快讯


近日,法拉第未来(FF)准备出售内华达州北拉斯维加斯900英亩优质土地,报价为4000万美元。该地块经过多次修缮改造,其中的700英亩已经完全具备了立即进行工业建设的条件。FF还表示,将在2019年内努力筹集足够的资金,尽快将FF 91推向市场并为今后的大规模量产FF 81做准备。目前,已有多家全球投资人表达了投资FF的兴趣。


作者简介


明天就是周六啦,提前祝大家周末愉快!

本篇来自 FeelsChaotic 的投稿文章,和大家分享了面向切面编程,希望对大家有所帮助!

https://juejin.im/user/58130890bf22ec0068821df3


前言


最全选型考量 + 剖析经典AOP开源库实践

繁多的 AOP 方法该如何选择?应用的步骤过于繁琐,语法概念看得头晕脑胀?

本文将详细展示选型种种考量维度,更是砍掉 2 个经典开源库的枝节,取其主干细细体会 AOP 的应用思想和关键流程。一边实践 AOP 一边还能掌握开源库,岂不快哉!


6个要点选择合适AOP方法


1. 明确你应用 AOP 在什么项目

如果你正在维护一个现有的项目,你要么小范围试用,要么就需要选择一个侵入性小的 AOP 方法(如:APT 代理类生效时机需要手动调用,灵活,但在插入点繁多情况下侵入性过高)。

2. 明确切入点的相似性

第一步,考虑一下切入点的数量和相似性,你是否愿意一个个在切点上面加注解,还是用相似性统一切。

第二步,考虑下这些应用切面的类有没有被 final 修饰,同时相似的方法有没有被 static 或 final 修饰时。 final 修饰的类就不能通过 cglib 生成代理,cglib 会继承被代理类,需要重写被代理方法,所以被代理类和方法不能是 final。

3. 明确织入的粒度和织入时机

我怎么选择织入(Weave)的时机?编译期间织入,还是编译后?载入时?或是运行时?通过比较各大 AOP 方法在织入时机方面的不同和优缺点,来获得对于如何选择 Weave 时机进行判定的准则。

对于普通的情况而言,在编译时进行 Weave 是最为直观的做法。因为源程序中包含了应用的所有信息,这种方式通常支持最多种类的联结点。利用编译时 Weave,我们能够使用 AOP 系统进行细粒度的 Weave 操作,例如读取或写入字段。源代码编译之后形成的模块将丧失大量的信息,因此通常采用粗粒度的 AOP 方法。

同时,对于传统的编译为本地代码的语言如 C++ 来说,编译完成后的模块往往跟操作系统平台相关,这就给建立统一的载入时、运行时 Weave 机制造成了困难。对于编译为本地代码的语言而言,只有在编译时进行 Weave 最为可行。尽管编译时 Weave 具有功能强大、适应面广泛等优点,但他的缺点也很明显。首先,它需要程序员提供所有的源代码,因此对于模块化的项目就力不从心了。

为了解决这个问题,我们可以选择支持编译后 Weave 的 AOP 方法。

新的问题又来了,如果程序的主逻辑部分和 Aspect 作为不同的组件开发,那么最为合理的 Weave 时机就是在框架载入 Aspect 代码之时。

运行时 Weave 可能是所有 AOP 方法中最为灵活的,程序在运行过程中可以为单个的对象指定是否需要 Weave 特定的方面。

选择合适的 Weave 时机对于 AOP 应用来说是非常关键的。针对具体的应用场合,我们需要作出不同的抉择。我们也可以结合多种 AOP 方法,从而获得更为灵活的 Weave 策略。

4. 明确对性能的要求,明确对方法数的要求

除了动态 Hook 方法,其他的 AOP 方法对性能影响几乎可以忽略不计。动态 AOP 本质使用了动态代理,不可避免要用到反射。而 APT 不可避免地要生成大量的代理类和方法。如何权衡,就看你对项目的要求。

5. 明确是否需要修改原有类

如果只是想特定地增强能力,可以使用 APT,在编译期间读取 Java 代码,解析注解,然后动态生成 Java 代码。

下图是Java编译代码的流程:

最全选型考量 + 剖析经典AOP开源库实践

可以看到,APT 工作在 Annotation Processing 阶段,最终通过注解处理器生成的代码会和源代码一起被编译成 Java 字节码。不过比较遗憾的是你不能修改已经存在的 Java 文件,比如在已经存在的类中添加新的方法或删除旧方法,所以通过 APT 只能通过辅助类的方式来实现注入,这样会略微增加项目的方法数和类数,不过只要控制好,不会对项目有太大的影响。

6. 明确调用的时机

APT 的时机需要主动调用,而其他 AOP 方法注入代码的调用时机和切入点的调用时机一致。


从开源库剖析AOP


AOP 的实践都写烂了,市面上有太多讲怎么实践 AOP 的博文了。那这篇和其他的博文有什么不同呢?有什么可以让大家受益的呢?

其实 AOP 实践很简单,关键是理解并应用,我们先参考开源库的实践,在这基础上去抽象关键步骤,一边实战一边达成阅读开源库任务,美滋滋!

APT

1. 经典 APT 框架 ButterKnife 工作流程

直接上图说明:

最全选型考量 + 剖析经典AOP开源库实践

在上面的过程中,你可以看到,为什么用 @Bind 、 @OnClick 等注解标注的属性、方法必须是 public 或 protected?

因为ButterKnife 是通过 被代理类引用.this.editText 来注入View的。为什么要这样呢?

答案就是:性能 。如果你把 View 和方法设置成 private,那么框架必须通过反射来注入。

想深入到源码细节了解 ButterKnife 更多?

how-butterknife-actually-works

https://medium.com/@lgvalle/how-butterknife-actually-works-85be0afbc5ab

ButterKnife源码分析

https://www.jianshu.com/p/1c449c1b0fa2

拆 JakeWharton 系列之 ButterKnife

https://juejin.im/post/58f388d1da2f60005d369a09

2. 仿造 ButterKnife,上手 APT

我们去掉细节,抽出关键流程,看看 ButterKnife 是怎么应用 APT 的。

最全选型考量 + 剖析经典AOP开源库实践

可以看到关键步骤就几项:

  1. 定义注解

  2. 编写注解处理器

  3. 扫描注解

  4. 编写代理类内容

  5. 生成代理类

  6. 调用代理类

我们标出重点,也就是我们需要实现的步骤。如下:

最全选型考量 + 剖析经典AOP开源库实践

咦,你可能发现了,最后一个步骤是在合适的时机去调用代理类或门面对象。这就是 APT 的缺点之一,在任意包位置自动生成代码但是运行时却需要主动调用。

APT 手把手实现可参考

JavaPoet - 优雅地生成代码——3.2 一个简单示例

https://blog.csdn.net/xuguobiao/article/details/72775730

3. 工具详解

APT 中我们用到了以下 3 个工具:

(1)Java Annotation Tool

Java Annotation Tool 给了我们一系列 API 支持。

  1. 通过 Java Annotation Tool 的 Filer 可以帮助我们以文件的形式输出JAVA源码。

  2. 通过 Java Annotation Tool 的 Elements 可以帮助我们处理扫描过程中扫描到的所有的元素节点,比如包(PackageElement)、类(TypeElement)、方法(ExecuteableElement)等。

  3. 通过 Java Annotation Tool 的 TypeMirror 可以帮助我们判断某个元素是否是我们想要的类型。

(2)JavaPoet

你当然可以直接通过字符串拼接的方式去生成 java 源码,怎么简单怎么来,一张图 show JavaPoet 的厉害之处。

最全选型考量 + 剖析经典AOP开源库实践

最全选型考量 + 剖析经典AOP开源库实践

(3)APT 插件

注解处理器已经有了,那么怎么执行它?这个时候就需要用到 android-apt 这个插件了,使用它有两个目的:

  1. 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library

  2. 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用

项目引入了 butterknife 之后就无需引入 apt 了,如果继续引入会报 Using incompatible plugins for the annotation processing

(4)AutoService

想要运行注解处理器,需要繁琐的步骤:

  1. 在 processors 库的 main 目录下新建 resources 资源文件夹;

  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;

  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;

  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

Google 开发的 AutoService 可以减少我们的工作量,只需要在你定义的注解处理器上添加 @AutoService(Processor.class) ,就能自动完成上面的步骤,简直不能再方便了。

4. 代理执行

虽然前面有说过 APT 并不能像 Aspectj 一样实现代码插入,但是可以使用变种方式实现。用注解修饰一系列方法,由 APT 来代理执行。此部分可参考CakeRun(https://github.com/lizhaoxuan/CakeRun

APT 生成的代理类按照一定次序依次执行修饰了注解的初始化方法,并且在其中增加了一些逻辑判断,来决定是否要执行这个方法。从而绕过发生 Crash 的类。

AspectJ

1. 经典 Aspectj 框架 hugo 工作流程

J 神的框架一如既往小而美,想啃开源库源码,可以先从 J 神的开源库先读起。

回到正题,hugo是 J 神开发的 Debug 日志库,包含了优秀的思想以及流行的技术,例如注解、AOP、AspectJ、Gradle 插件、android-maven-gradle-plugin 等。在进行 hugo 源码解读之前,你需要首先对这些知识点有一定的了解。

先上工作流程图,我们再讲细节:

最全选型考量 + 剖析经典AOP开源库实践

2. 解惑之一个打印日志逻辑怎么织入的?

只需要一个 @DebugLog注解,hugo就能帮我们打印入参出参、统计方法耗时。自定义注解很好理解,我们重点看看切面 Hugo 是怎么处理的。

最全选型考量 + 剖析经典AOP开源库实践

有没有发现什么?

最全选型考量 + 剖析经典AOP开源库实践

最全选型考量 + 剖析经典AOP开源库实践

没错,切点表达式帮助我们描述具体要切入哪里。

AspectJ 的切点表达式由关键字和操作参数组成,以切点表达式 execution(* helloWorld(..))为例,其中 execution 是关键字,为了便于理解,通常也称为函数,而* helloWorld(..)是操作参数,通常也称为函数的入参。切点表达式函数的类型很多,如方法切点函数,方法入参切点函数,目标类切点函数等,hugo 用到的有两种类型:

最全选型考量 + 剖析经典AOP开源库实践

想详细入门 AspectJ 语法?

看AspectJ在Android中的强势插入

https://blog.csdn.net/eclipsexys/article/details/54425414

深入理解Android之AOP

https://blog.csdn.net/innost/article/details/49387395

3. 解惑之 AspectJ in Android 为何如此丝滑?

我们引入 hugo 只需要 3 步。

最全选型考量 + 剖析经典AOP开源库实践

不是说 AspectJ 在 Android 中很不友好?!说好的需要使用 andorid-library gradle 插件在编译时做一些 hook,使用 AspectJ 的编译器(ajc,一个java编译器的扩展)对所有受 aspect 影响的类进行织入,在 gradle 的编译 task 中增加一些额外配置,使之能正确编译运行等等等呢?

这些 hugo 已经帮我们做好了(所以步骤 2 中,我们引入 hugo 的同时要使用 hugo 的 Gradle 插件,就是为了 hook 编译)。

最全选型考量 + 剖析经典AOP开源库实践

最全选型考量 + 剖析经典AOP开源库实践

最全选型考量 + 剖析经典AOP开源库实践

4. 抽丝剥茧 Aspect 的重点流程

抽象一下 hugo 的工作流程,我们得到了 2 种Aspect工作流程:

最全选型考量 + 剖析经典AOP开源库实践

最全选型考量 + 剖析经典AOP开源库实践

前面选择合适的 AOP 方法第 2 点我们提到,以 Pointcut 切入点作为区分,AspectJ 有两种用法:

  1. 用自定义注解修饰切入点,精确控制切入点,属于侵入式

//方法一:一个个在切入点上面加注解
protected void onCreate(Bundle savedInstanceState) {
    //...
    followTextView.setOnClickListener(view -> {
        onClickFollow();
    });
    unFollowTextView.setOnClickListener(view -> {
        onClickUnFollow();
    });
}

@SingleClick(clickIntervalTime = 1000)
private void onClickUnFollow() {
}

@SingleClick(clickIntervalTime = 1000)
private void onClickFollow() {
}

@Aspect
public class AspectTest {
    @Around("execution(@com.feelschaotic.aspectj.annotations.SingleClick * *(..))")
    public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //...
    }
}

  1. 不需要在切入点代码中做任何修改,统一按相似性来切(比如类名,包名),属于非侵入式

//方法二:根据相似性统一切,不需要再使用注解标记了
protected void onCreate(Bundle savedInstanceState) {
    //...
    followTextView.setOnClickListener(view -> {
        //...
    });
    unFollowTextView.setOnClickListener(view -> {
        //...
    });
}

@Aspect
public class AspectTest {
    @Around("execution(* android.view.View.OnClickListener.onClick(..))")
    public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //...
    }
}

5. AspectJ 和 APT 最大的不同

APT 决定是否使用切面的权利仍然在业务代码中,而 AspectJ 将决定是否使用切面的权利还给了切面。在写切面的时候就可以决定哪些类的哪些方法会被代理,从逻辑上不需要侵入业务代码。

但是AspectJ 的使用需要匹配一些明确的 Join Points,如果 Join Points 的函数命名、所在包位置等因素改变了,对应的匹配规则没有跟着改变,就有可能导致匹配不到指定的内容而无法在该地方插入自己想要的功能。

那 AspectJ 的执行原理是什么?注入的代码和目标代码是怎么连接的?下一篇我将会详细讲讲,先挖个坑,跑路。


应用篇


Javassist

为什么用 Javassist 来实践?

因为实践过程中我们可以顺带掌握字节码插桩的技术基础,就算是后续学习热修复、应用 ASM,这些基础都是通用的。虽然 Javassist 性能比 ASM 低,但对新手很友好,操纵字节码却不需要直接接触字节码技术和了解虚拟机指令,因为 Javassist 实现了一个用于处理源代码的小型编译器,可以接收用 Java 编写的源代码,然后将其编译成 Java 字节码再内联到方法体中。

话不多说,我们马上上手,在上手之前,先了解几个概念:

1. 入门概念

(1)Gradle

Javassist 修改对象是编译后的 class 字节码。那首先我们得知道什么时候编译完成,才能在 .class 文件被转为 .dex 文件之前去做修改。

大多 Android 项目使用 Gradle 构建,我们需要先理解 Gradle 的工作流程。Gradle 是通过一个一个 Task 执行完成整个流程的,依次执行完 Task 后,项目就打包完成了。 其实 Gradle 就是一个装载 Task 的脚本容器。

最全选型考量 + 剖析经典AOP开源库实践

(2) Plugin

那 Gralde 里面那么多 Task 是怎么来的呢,谁定义的呢?是Plugin!

我们回忆下,在 app module 的 build.gradle 文件中的第一行,往往会有 apply plugin : 'com.android.application',lib 的 build.gradle 则会有 apply plugin : 'com.android.library',就是 Plugin 为项目构建提供了 Task,不同的 plugin 里注册的 Task 不一样,使用不同 plugin,module 的功能也就不一样。

可以简单地理解为, Gradle 只是一个框架,真正起作用的是 plugin,是plugin 往 Gradle 脚本中添加 Task。

(3)Task

思考一下,如果一个 Task 的职责是将 .java 编译成 .class,这个 Task 是不是要先拿到 java 文件的目录?处理完成后还要告诉下一个 Task class 的目录?

没错,从 Task 执行流程图可以看出,Task 有一个重要的概念:inputs 和 outputs。 Task 通过 inputs 拿到一些需要的参数,处理完毕之后就输出 outputs,而下一个 Task 的 inputs 则是上一个 Task 的outputs。

这些 Task 其中肯定也有将所有 class 打包成 dex 的 Task,那我们要怎么找到这个 Task ?在之前插入我们自己的 Task 做代码注入呢?用 Transfrom!

(4)Transform

Transfrom 是 Gradle 1.5以上新出的一个 API,其实它也是 Task。

  • gradle plugin 1.5 以下,preDex 这个 Task 会将依赖的 module 编译后的 class 打包成 jar,然后 dex 这个 Task 则会将所有 class 打包成dex;

    想要监听项目被打包成 .dex 的时机,就必须自定义一个 Gradle Task,插入到 predex 或者 dex 之前,在这个自定义的 Task 中使用 Javassist ca class 。

  • gradle plugin 1.5 以上,preDex 和 Dex 这两个 Task 已经被 TransfromClassesWithDexForDebug 取代

    Transform 更为方便,我们不再需要插入到某个 Task 前面。Tranfrom 有自己的执行时机,一经注册便会自动添加到 Task 执行序列中,且正好是 class 被打包成dex之前,所以我们自定义一个 Transform 即可。

(5)Groovy
  1. Gradle 使用 Groovy  语言实现,想要自定义 Gradle 插件就需要使用 Groovy  语言。

  2. Groovy 语言 = Java语言的扩展 + 众多脚本语言的语法,运行在 JVM 虚拟机上,可以与 Java 无缝对接。Java 开发者学习 Groovy 的成本并不高。

2. 小结

所以我们需要怎么做?流程总结如下:

最全选型考量 + 剖析经典AOP开源库实践

3. 实战 —— 自动TryCatch

最全选型考量 + 剖析经典AOP开源库实践

既然说了这么多,是时候实战了,每次看到项目代码里充斥着防范性 try-catch,我就

最全选型考量 + 剖析经典AOP开源库实践

我们照着流程图,一步步来实现这个自动 try-Catch 功能:

(1)自定义 Plugin
  1. 新建一个 module,选择 library module,module 名字必须为 buildSrc

  2. 删除 module 下所有文件,build.gradle 配置替换如下:

apply plugin: 'groovy'

repositories {
    jcenter()
}

dependencies {
    compile 'com.android.tools.build:gradle:2.3.3'
    compile 'org.javassist:javassist:3.20.0-GA'
}

3. 新建 groovy 目录

最全选型考量 + 剖析经典AOP开源库实践

4. 新建 Plugin 类

需要注意: groovy 目录下新建类,需要选择 file且以.groovy作为文件格式。

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension

class PathPlugin implements Plugin<Project{
    @Override
    void apply(Project project) {
        project.logger.debug "================自定义插件成功!=========="
    }
}

为了马上看到效果,我们提前走流程图中的步骤 4,在 app module下的 buiil.gradle 中添加 apply 插件。

最全选型考量 + 剖析经典AOP开源库实践

跑一下:

最全选型考量 + 剖析经典AOP开源库实践

(2)自定义 Transfrom
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project

class PathTransform extends Transform {

    Project project
    TransformOutputProvider outputProvider

    // 构造函数中我们将Project对象保存一下备用
    public PathTransform(Project project) {
        this.project = project
    }

    // 设置我们自定义的Transform对应的Task名称,TransfromClassesWithPreDexForXXXX
    @Override
    String getName() {
        return "PathTransform"
    }

    //通过指定输入的类型指定我们要处理的文件类型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        //指定处理所有class和jar的字节码
        return TransformManager.CONTENT_CLASS
    }

    // 指定Transform的作用范围
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs,
                   Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental)

            throws IOException, TransformException, InterruptedException 
{
        this.outputProvider = outputProvider
        traversalInputs(inputs)
    }

    /**
     * Transform的inputs有两种类型:
     *  一种是目录, DirectoryInput
     *  一种是jar包,JarInput
     *  要分开遍历
     */

    private ArrayList<TransformInput> traversalInputs(Collection<TransformInput> inputs) 
{
        inputs.each {
            TransformInput input ->
                traversalDirInputs(input)
                traversalJarInputs(input)
        }
    }

    /**
     * 对类型为文件夹的input进行遍历
     */

    private ArrayList<DirectoryInput> traversalDirInputs(TransformInput input) {
        input.directoryInputs.each {
            /**
             * 文件夹里面包含的是
             *  我们手写的类
             *  R.class、
             *  BuildConfig.class
             *  R$XXX.class
             *  等
             *  根据自己的需要对应处理
             */

            println("it == ${it}")

            //TODO:这里可以注入代码!!

            // 获取output目录
            def dest = outputProvider.getContentLocation(it.name
                    , it.contentTypes, it.scopes, Format.DIRECTORY)

            // 将input的目录复制到output指定目录
            FileUtils.copyDirectory(it.file, dest)
        }
    }

    /**
     * 对类型为jar文件的input进行遍历
     */

    private ArrayList<JarInput> traversalJarInputs(TransformInput input) {
        //没有对jar注入的需求,暂不扩展
    }
}

(3)向自定义的 Plugin 注册 Transfrom

回到我们刚刚定义的 PathPlugin,在 apply 方法中注册 PathTransfrom:

def android = project.extensions.findByType(AppExtension)
android.registerTransform(new PathTransform(project))

clean 项目,再跑一次,确保没有报错。

(4)代码注入

接着就是重头戏了,我们新建一个 TryCatchInject 类,先把扫描到的方法和类名打印出来:

这个类不同于前面定义的类,无需继承指定父类,无需实现指定方法,所以我以短方法+有表达力的命名代替了注释,如果有疑问请一定要反馈给我,我好反思是否写得不够清晰。

import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtMethod
import javassist.bytecode.AnnotationsAttribute
import javassist.bytecode.MethodInfo
import java.lang.annotation.Annotation

class TryCatchInject {
    private static String path
    private static ClassPool pool = ClassPool.getDefault()
    private static final String CLASS_SUFFIX = ".class"

    //注入的入口
    static void injectDir(String path, String packageName) {
        this.path = path
        pool.appendClassPath(path)
        traverseFile(packageName)
    }

    private static traverseFile(String packageName) {
        File dir = new File(path)
        if (!dir.isDirectory()) {
            return
        }
        beginTraverseFile(dir, packageName)
    }

    private static beginTraverseFile(File dir, packageName) {
        dir.eachFileRecurse { File file ->

            String filePath = file.absolutePath
            if (isClassFile(filePath)
{
                int index = filePath.indexOf(packageName.replace(".", File.separator))
                boolean isClassFilePath = index != -1
                if (isClassFilePath) {
                    transformPathAndInjectCode(filePath, index)
                }
            }
        }
    }

    private static boolean isClassFile(String filePath) {
        return filePath.endsWith(".class") && !filePath.contains('R') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")
    }

    private static void transformPathAndInjectCode(String filePath, int index) {
        String className = getClassNameFromFilePath(filePath, index)
        injectCode(className)
    }

    private static String getClassNameFromFilePath(String filePath, int index) {
        int end = filePath.length() - CLASS_SUFFIX.length()
        String className = filePath.substring(index, end).replace('\\''.').replace('/''.')
        className
    }

    private static void injectCode(String className) {
        CtClass c = pool.getCtClass(className)
        println("CtClass:" + c)
        defrostClassIfFrozen(c)
        traverseMethod(c)

        c.writeFile(path)
        c.detach()
    }

    private static void traverseMethod(CtClass c) {
        CtMethod[] methods = c.getDeclaredMethods()
        for (ctMethod in methods) {
            println("ctMethod:" + ctMethod)
            //TODO: 这里可以对方法进行操作
        }
    }

    private static void defrostClassIfFrozen(CtClass c) {
        if (c.isFrozen()) {
            c.defrost()
        }
    }
}

在 PathTransfrom 里的 TODO 标记处调用注入类

//请注意把 com\\feelschaotic\\javassist 替换为自己想扫描的路径
 TryCatchInject.injectDir(it.file.absolutePath, "com\\feelschaotic\\javassist")

我们再次 clean 后跑一下

最全选型考量 + 剖析经典AOP开源库实践

我们可以直接按方法的包名切,也可以按方法的标记切(比如:特殊的入参、方法签名、方法名、方法上的注解……),考虑到我们只需要对特定的方法捕获异常,我打算用自定义注解来标记方法。

在 app module 中定义一个注解

//仅支持在方法上使用
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoTryCatch {
    //支持业务方catch指定异常
    Class[] value() default Exception.class;
}

接着我们要在 TryCatchInject 的 traverseMethod方法 TODO 中,使用 Javassist 获取方法上的注解,再获取注解的 value。

   private static void traverseMethod(CtClass c) {
        CtMethod[] methods = c.getDeclaredMethods()
        for (ctMethod in methods) {
            println("ctMethod:" + ctMethod)
            traverseAnnotation(ctMethod)
        }
    }

    private static void traverseAnnotation(CtMethod ctMethod) {
        Annotation[] annotations = ctMethod.getAnnotations()

        for (annotation in annotations) {
            def canonicalName = annotation.annotationType().canonicalName
            if (isSpecifiedAnnotation(canonicalName)
{
                onIsSpecifiedAnnotation(ctMethod, canonicalName)
            }
        }
    }

    private static boolean isSpecifiedAnnotation(String canonicalName) {
        PROCESSED_ANNOTATION_NAME.equals(canonicalName)
    }

    private static void onIsSpecifiedAnnotation(CtMethod ctMethod, String canonicalName) {
        MethodInfo methodInfo = ctMethod.getMethodInfo()
        AnnotationsAttribute attribute = methodInfo.getAttribute(AnnotationsAttribute.visibleTag)

        javassist.bytecode.annotation.Annotation javassistAnnotation = attribute.getAnnotation(canonicalName)
        def names = javassistAnnotation.getMemberNames()
        if (names == null || names.isEmpty()) {
            catchAllExceptions(ctMethod)
            return
        }
        catchSpecifiedExceptions(ctMethod, names, javassistAnnotation)
    }

    private static catchAllExceptions(CtMethod ctMethod) {
        CtClass etype = pool.get("java.lang.Exception")
        ctMethod.addCatch('{com.feelschaotic.javassist.Logger.print($e);return;}', etype)
    }

    private static void catchSpecifiedExceptions(CtMethod ctMethod, Set names, javassist.bytecode.annotation.Annotation javassistAnnotation) {
        names.each { def name ->

            ArrayMemberValue arrayMemberValues = (ArrayMemberValue) javassistAnnotation.getMemberValue(name)
            if (arrayMemberValues == null) {
                return
            }
            addMultiCatch(ctMethod, (ClassMemberValue[]) arrayMemberValues.getValue())
        }
    }

    private static void addMultiCatch(CtMethod ctMethod, ClassMemberValue[] classMemberValues) {
        classMemberValues.each { ClassMemberValue classMemberValue ->
            CtClass etype = pool.get(classMemberValue.value)
            ctMethod.addCatch('{ com.feelschaotic.javassist.Logger.print($e);return;}', etype)
        }
    }

完成!写个 demo 遛一遛:

最全选型考量 + 剖析经典AOP开源库实践

可以看到应用没有崩溃,logcat 打印出异常了。

最全选型考量 + 剖析经典AOP开源库实践

完整demo请戳

https://github.com/feelschaotic/AopAutoTryCatch


后记


完成本篇过程曲折,最终成稿已经完全偏离当初拟定的大纲,本来想详细记录下 AOP 的应用,把每种方法都一步步实践一遍,但在写作的过程中,我不断地质疑自己,这种步骤文全网都是,于自己于大家又有什么意义? 想着把写作方向改为 AOP 开源库源码分析,但又难以避免陷入大段源码分析的泥潭中。

本文的初衷在于 AOP 的实践,既然是实践,何不抛弃语法细节,抽象流程,图示步骤,毕竟学习完能真正吸收的一是魔鬼的细节,二是精妙的思想。

写作本身就是一种思考,谨以警示自己。


推荐阅读:


长按上图,识别图中二维码即可关注

以上是关于最全选型考量 + 剖析经典AOP开源库实践的主要内容,如果未能解决你的问题,请参考以下文章

微服务快速实践3-java下开源组件选型

架构技术选型考量因素

最全面解析Spring框架IOC容器和AOP面向切面

最全面解析Spring框架IOC容器和AOP面向切面

最全面解析Spring框架IOC容器和AOP面向切面

最全面解析Spring框架IOC容器和AOP面向切面