一文应用 AOP | 最全选型考量 + 边剖析经典开源库边实践,美滋滋

AOP系列思维导图

前言


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

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

一、6 个要点帮你选择合适的 AOP 方法


在上文 最全面 AOP 方法探讨 中,我们分析对比了最热门的几种 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编译代码的流程:

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

6. 明确调用的时机

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

二、从开源库剖析 AOP


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

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

APT

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

直接上图说明。

APT之ButterKnife工作流程

在上面的过程中,你可以看到,为什么用 @Bind 、 @OnClick 等注解标注的属性、方法必须是 public 或 protected?
因为ButterKnife 是通过 被代理类引用.this.editText 来注入View的。为什么要这样呢?
答案就是:性能 。如果你把 View 和方法设置成 private,那么框架必须通过反射来注入。

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

2. 仿造 ButterKnife,上手 APT

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

APT工作流程

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

  1. 定义注解
  2. 编写注解处理器
  3. 扫描注解
  4. 编写代理类内容
  5. 生成代理类
  6. 调用代理类

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

APT工作流程重点

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

APT 手把手实现可参考 JavaPoet - 优雅地生成代码——3.2 一个简单示例

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 的厉害之处。

生成同样的类,使用JavaPoet前,字符串拼接

生成同样的类,使用JavaPoet后,以面向对象的方式来生成源码
(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

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

AspectJ

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

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

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

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

Aspect之hugo工作流程

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

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

有没有发现什么?

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

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

函数名 类型 入参 说明
execution() 方法切点函数 方法匹配模式字符串 表示所有目标类中满足某个匹配模式的方法连接点,例如 execution(* helloWorld(..)) 表示所有目标类中的 helloWorld 方法,返回值和参数任意
within() 目标类切点函数 类名匹配模式字符串 表示满足某个匹配模式的特定域中的类的所有连接点,例如 within(com.feelschaotic.demo.*) 表示 com.feelschaotic.demo 中的所有类的所有方法

想详细入门 AspectJ 语法?

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

我们引入 hugo 只需要 3 步。

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

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

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

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

Aspect侵入式工作流程
Aspect非侵入式工作流程

前面选择合适的 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 的执行原理是什么?注入的代码和目标代码是怎么连接的?请戳:会用就行了?你知道 AOP 框架的原理吗?

三、应用篇


Javassist

为什么用 Javassist 来实践?

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

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

1. 入门概念

(1)Gradle

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

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

Task执行流程
(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. 小结

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

Javassist应用流程

3. 实战 —— 自动TryCatch

代码里到处都是防范性catch

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

我们照着流程图,一步步来实现这个自动 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'
}
  1. 新建 groovy 目录


  2. 新建 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 插件。


跑一下:


(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 后跑一下

打印了扫描到的类和方法

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

在 app module 中定义一个注解

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

接着我们要在 TryCatchInjecttraverseMethod方法 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 遛一遛:

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

完整demo请戳

后记


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

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

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

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

推荐阅读更多精彩内容