从 Java 字节码到 ASM 实践

1. 概述

AOP(面向切面编程)的概念现在已经应用的非常广泛了,下面是从百度百科上摘抄的一段解释,比较浅显易懂

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP 是一种编程思想,但是它的实现方式有很多,比如:Spring、AspectJ、JavaAssist、ASM 等。由于我是做 Android 开发的,所以会用 Android 中的一些例子。

  • JakeWhartonhugo 就是一个典型的应用,其利用了自定义 Gradle 插件 + AspectJ 的方式,将有特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试
  • 由于最近在学习 Java 字节码和 ASM 方面的知识,所以也照猫画虎,写了一个 Koala,实现了和 hugo 同样的功能,将特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试,不过我使用的是 自定义 Gradle 插件 + ASM 的方式

那 ASM 是什么呢?这儿有一篇介绍 ASM 的文章,写的不错 AOP 的利器:ASM 3.0 介绍,摘抄其中一段:

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

简单点说,通过 javac 将 .java 文件编译成 .class 文件,.class 文件中的内容虽然不同,但是它们都具有相同的格式,ASM 通过使用访问者(visitor)模式,按照 .class 文件特有的格式从头到尾扫描一遍 .class 文件中的内容,在扫描的过程中,就可以对 .class 文件做一些操作了,有点黑科技的感觉

二. Java 字节码 & 虚拟机

2.1 Java 字节码

提到 Java 字节码,可能很多人都不是很熟悉,大概都知道使用 javac 可以将 .java 文件编译成 .class 文件,.class 文件中存放的就是该 .java 文件对应的字节码内容,比如如下一段 Demo.java 代码很简单:

package com.lijiankun24.classpractice;

public class Demo {

    private int m;

    public int inc() {
        return m + 1;
    }
}

通过 javac 编译生成对应的 Demo.class 文件,使用纯文本文件打开 Demo.class,其中的内容是以 8 位字节为基础单位的二进制流,表面来看就是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守 Java 虚拟机规范的

cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4465 6d6f 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 4465 6d6f
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0600
0100 0d00 0000 0200 0e

如果再使用 javap -verbose Demo.class 查看该 Demo.class 中的内容,如下图所示

Demo.png

从上图中,我们可以看到,.class 文件中主要有常量池、字段表、方法表和属性表等内容。如何从以 8 位字节为基础单位的二进制流中分析出常量池、方法表的内容呢?在这篇文章中有详细的介绍 认识 .class 文件的字节码结构
,这篇文章以一个简单的例子,手把手的分析十六进制符合表示的 .class 文件

2.2 Java 虚拟机类加载机制

上面一小节介绍了 .class 文件的结构,但是 .class 文件是静态的,它最终是会被虚拟机加载才能执行的,那么问题来了,.class 文件是什么时候会被加载呢?

一般来说,一个 .class 文件就包含一个 Java 类,.class 文件和 Java 类是息息相关的。要说 .class 文件的加载时机,就不得不提到 Java 类的生命周期了。想必大家都知道,Java 类的生命周期包含加载验证准备解析初始化使用卸载七个步骤,在 Java 虚拟机规范中并没有规定 Java 类的加载时机,但是却规定了 Java 类 初始化 的时机,而加载又一定是在初始化的前面,所以也可以说是间接地规定了 .class 文件的加载的时机。

有五种情况,是必须初始化一个类的,这五种情况被称为对 Java 类的主动引用,除了 主动引用 之外,其他的对 Java 类的引用称为 被动引用

上面也提到了 Java 类的生命周期总共分为加载验证准备解析初始化使用卸载,其中最重要的是前五个步骤加载验证准备解析初始化,那在这五个步骤中都发生了什么事情呢?

举一个简单的例子,如下所示。下面的 Constant 类中,有一个静态 static 代码块,和一个静态 static 变量, 是什么时候给 value 赋值的呢?什么时候会执行 static 代码块呢?答案是在类的 初始化 阶段。

public class Constant {

    static {
        System.out.println("Constant init!");
    }

    public static String value = "lijiankun24!";
}

在 Java 类中,如果有静态 static 代码块、静态 static 变量的话,编译器会为这个类自动生成一个类构造器(注意,不是实例构造器),在 类构造器 中会执行静态 static 代码块,初始化静态 static 变量,类构造器 就是在类的 初始化 阶段执行的

提到 Java 类的加载,就不得不说起 Java 中的类加载器 ClassLoader 了,双亲委派模型及其好处也是必须要清楚的。

上面只是粗略的介绍,更多想了解五种主动引用、类的生命周期、类构造器、类加载器、双亲委派模型,如果想了解的更详细,请看这篇文章 理解 JVM 中的类加载机制

2.3 Java 虚拟机字节码执行引擎

Java 内存模型中,非常重要的一个区域就是 Java 虚拟机栈。Java 中每一个方法执行的时候都会在 Java 虚拟机栈中压入一个栈帧,方法执行完成之后,也会将该栈帧出栈。
栈帧中最主要的是局部变量表操作数栈这两个概念,在执行一个 Java 方法的字节码时,其实就是调用 Java 字节码指令操纵局部变量表操作数栈,最后将执行的结果返回。如果想学习 Java 字节码指令的话,推荐一篇文章

除了方法的执行过程,还需要了解一下 Java 中的方法调用。方法调用就是指通过 .class 文件中方法的符号引用,确认方法的直接引用的过程,这个过程有可能发生在加载阶段,也有可能发生在运行阶段。
有一些方法是在加载阶段就已经确定了方法的直接引用,比如:静态方法、私有方法、实例构造器方法,这类方法的调用称为 解析;除了解析,方法的 静态分派 也是在加载阶段就确定了方法的直接引用,这类方法常见的就是 重载 的方法。
有一些方法是在运行阶段确认方法的直接引用的,比如:重写 的方法,调用重写 的方法时,需要具体到对象的实际类型,所以需要特定的 Java 字节码 invokevirtual 去确定合适的方法。

Java 虚拟机是基于栈的解释执行的,这里所说的 就是 Java 虚拟机栈,解释执行时相对于编译执行而言的,解释执行就是指:代码通过编译生成字节码指令集之后,通过解释器解释执行的。这个不用了解的太深,明白这几个定义就好

上面介绍了 Java 虚拟机栈中的 栈帧方法调用解析静态分派动态分派 和 Java 虚拟机基于栈的解释执行,详细的内容可以参考 虚拟机字节码执行引擎

三. 访问者模式 & ASM

3.1 访问者模式

ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。在学习 ASM 之前,可以通过这篇文章学习一下访问者模式访问者模式和 ASM

3.2 ASM 库的介绍和使用

ASM 可以直接生产二进制的 .class 文件,也可以在类被加载入 JVM 之前动态修改类行为。ASM 库的介绍和使用 文章介绍了 ASM 库的结构和几个重要的 Core Api,包括 ClassVisitor、ClassReader、ClassWriter、MethodVisitor 和 AdviceAdapter 等,并且通过两个简单的例子,分别介绍了如何修改 Java 类中方法的字节码和修改属性的字节码。

在刚开始使用的时候,可能对字节码的执行不是很清楚,使用 ASM 会比较困难,ASM 官方也提供了一个帮助工具 ASMifier,我们可以先写出目标代码,然后通过 javac 编译成 .class 文件,然后通过 ASMifier 分析此 .class 文件就可以得到需要插入的代码对应的 ASM 代码了。

上面提到的内容,ASM 库的 Core Api 和 ASMifier 的使用具体请参阅这篇文章ASM 库的介绍和使用

四. Koala

最后,学习完理论知识以后,为了练手,写了一个小项目,使用自定义 Gradle 插件 + ASM 的方式实现了和 JakeWhartonhugo 库同样的功能的库,叫做 Koala,将特定注解的方法的传入参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。

4.1 添加 Koala Gradle Plugin 依赖

在项目工程的 build.gradle 中添加如下代码:

    buildscript {
        repositories {
            maven {
                url "https://plugins.gradle.org/m2/"
            }
        }
        dependencies {
            classpath "gradle.plugin.com.lijiankun24:buildSrc:1.1.1"
        }
    }

在需要使用的 module 中的 build.gradle 中添加如下代码:

    apply plugin: "com.lijiankun24.koala-plugin"

4.2 添加 Koala 依赖

Gradle:

    compile 'com.lijiankun24:koala:1.1.2'

Maven:

    <dependency>
        <groupId>com.lijiankun24</groupId>
        <artifactId>koala</artifactId>
        <version>1.1.2</version>
        <type>pom</type>
    </dependency>

4.3 使用

使用起来还是非常简单的,在 Java 的方法上添加 @KoalaLog 注解,如下所示:

    @KoalaLog
    public String getName(String first, String last) {
        SystemClock.sleep(15); // Don't ever really do this!
        return first + " " + last;
    }

当上述方法被调用的时候,Logcat 中的输出如下所示:

09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/0KoalaLog: ┌───────────────────────────────────------───────────────────────────────────------
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/1KoalaLog: │ The class's name: com.lijiankun24.practicedemo.MainActivity
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/2KoalaLog: │ The method's name: getName(java.lang.String, java.lang.String)
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/3KoalaLog: │ The arguments: [li, jiankun]
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/4KoalaLog: │ The result: li jiankun
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/5KoalaLog: │ The cost time: 15ms
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/6KoalaLog: └───────────────────────────────────------───────────────────────────────────------

4.4 混淆规则

 -keep class com.lijiankun24.koala.** { *; }

欢迎 star 和 fork Koala,也欢迎点赞和收藏

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,830评论 25 707
  • 最近带啥出去就丢啥,感觉脑子被抽真空了!希望是真的破财免灾吧!我要好好的,不能出岔子啦!已经破财了,一定要免...
    瑜伽馆专业预售阅读 227评论 0 0
  • 为了迎接明天的儿童节,小朋友特别提出要帮我洗碗,我当然要满足他啰。洗完之后跟我一起下楼的时候偷偷跟我讲:“以后我们...
    yoly0915阅读 186评论 0 0
  • 你遇见的搞笑的试卷答案是什么?小编就带领大家来看看别人的答案。 【1】 【2】 【3】 【4】 【5】 【6】 【...
    梦想远航9阅读 318评论 0 0
  • 你爸爸提出离婚已经2个月,在这期间即痛苦也是高兴(过后)的。前一个月我一直在改变着自己,因为我意识到自己作为妻子和...
    娜哈啊哈阅读 983评论 0 0