從 Java 字節(jié)碼到 ASM 實(shí)踐

1. 概述

AOP(面向切面編程)的概念現(xiàn)在已經(jīng)應(yīng)用的非常廣泛了,下面是從百度百科上摘抄的一段解釋前域,比較淺顯易懂

在軟件業(yè),AOP為Aspect Oriented Programming的縮寫捷枯,意為:面向切面編程崇败,通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù)肉渴,是軟件開發(fā)中的一個(gè)熱點(diǎn)公荧,也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型同规。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離循狰,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性券勺,同時(shí)提高了開發(fā)的效率绪钥。

AOP 是一種編程思想,但是它的實(shí)現(xiàn)方式有很多关炼,比如:Spring程腹、AspectJ、JavaAssist儒拂、ASM 等寸潦。由于我是做 Android 開發(fā)的色鸳,所以會(huì)用 Android 中的一些例子。

  • JakeWhartonhugo 就是一個(gè)典型的應(yīng)用见转,其利用了自定義 Gradle 插件 + AspectJ 的方式命雀,將有特定注解的方法的參數(shù)、返回結(jié)果和執(zhí)行時(shí)間打印到 Logcat 中斩箫,方便開發(fā)調(diào)試
  • 由于最近在學(xué)習(xí) Java 字節(jié)碼和 ASM 方面的知識(shí)吏砂,所以也照貓畫虎,寫了一個(gè) Koala乘客,實(shí)現(xiàn)了和 hugo 同樣的功能狐血,將特定注解的方法的參數(shù)、返回結(jié)果和執(zhí)行時(shí)間打印到 Logcat 中易核,方便開發(fā)調(diào)試匈织,不過我使用的是 自定義 Gradle 插件 + ASM 的方式

那 ASM 是什么呢?這兒有一篇介紹 ASM 的文章耸成,寫的不錯(cuò) AOP 的利器:ASM 3.0 介紹报亩,摘抄其中一段:

ASM 是一個(gè) Java 字節(jié)碼操控框架。它能被用來(lái)動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能井氢。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件弦追,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class 文件里花竞,這些類文件擁有足夠的元數(shù)據(jù)來(lái)解析類中的所有元素:類名稱劲件、方法、屬性以及 Java 字節(jié)碼(指令)约急。ASM 從類文件中讀入信息后零远,能夠改變類行為,分析類信息厌蔽,甚至能夠根據(jù)用戶要求生成新類牵辣。

簡(jiǎn)單點(diǎn)說,通過 javac 將 .java 文件編譯成 .class 文件奴饮,.class 文件中的內(nèi)容雖然不同纬向,但是它們都具有相同的格式,ASM 通過使用訪問者(visitor)模式戴卜,按照 .class 文件特有的格式從頭到尾掃描一遍 .class 文件中的內(nèi)容逾条,在掃描的過程中,就可以對(duì) .class 文件做一些操作了投剥,有點(diǎn)黑科技的感覺

二. Java 字節(jié)碼 & 虛擬機(jī)

2.1 Java 字節(jié)碼

提到 Java 字節(jié)碼师脂,可能很多人都不是很熟悉,大概都知道使用 javac 可以將 .java 文件編譯成 .class 文件,.class 文件中存放的就是該 .java 文件對(duì)應(yīng)的字節(jié)碼內(nèi)容吃警,比如如下一段 Demo.java 代碼很簡(jiǎn)單:

package com.lijiankun24.classpractice;

public class Demo {

    private int m;

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

通過 javac 編譯生成對(duì)應(yīng)的 Demo.class 文件糕篇,使用純文本文件打開 Demo.class,其中的內(nèi)容是以 8 位字節(jié)為基礎(chǔ)單位的二進(jìn)制流酌心,表面來(lái)看就是由十六進(jìn)制符號(hào)組成的娩缰,這一段十六進(jìn)制符號(hào)組成的長(zhǎng)串是遵守 Java 虛擬機(jī)規(guī)范的

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 中的內(nèi)容,如下圖所示

Demo.png

從上圖中谒府,我們可以看到,.class 文件中主要有常量池浮毯、字段表完疫、方法表和屬性表等內(nèi)容。如何從以 8 位字節(jié)為基礎(chǔ)單位的二進(jìn)制流中分析出常量池债蓝、方法表的內(nèi)容呢芳誓?在這篇文章中有詳細(xì)的介紹 認(rèn)識(shí) .class 文件的字節(jié)碼結(jié)構(gòu)
,這篇文章以一個(gè)簡(jiǎn)單的例子赂摆,手把手的分析十六進(jìn)制符合表示的 .class 文件

2.2 Java 虛擬機(jī)類加載機(jī)制

上面一小節(jié)介紹了 .class 文件的結(jié)構(gòu)烟号,但是 .class 文件是靜態(tài)的政恍,它最終是會(huì)被虛擬機(jī)加載才能執(zhí)行的篙耗,那么問題來(lái)了铣焊,.class 文件是什么時(shí)候會(huì)被加載呢?

一般來(lái)說岛蚤,一個(gè) .class 文件就包含一個(gè) Java 類,.class 文件和 Java 類是息息相關(guān)的懈糯。要說 .class 文件的加載時(shí)機(jī)屿储,就不得不提到 Java 類的生命周期了疯潭。想必大家都知道,Java 類的生命周期包含加載相叁、驗(yàn)證準(zhǔn)備赎离、解析舞蔽、初始化颊亮、使用绍在、卸載七個(gè)步驟,在 Java 虛擬機(jī)規(guī)范中并沒有規(guī)定 Java 類的加載時(shí)機(jī)雹有,但是卻規(guī)定了 Java 類 初始化 的時(shí)機(jī)偿渡,而加載又一定是在初始化的前面,所以也可以說是間接地規(guī)定了 .class 文件的加載的時(shí)機(jī)霸奕。

有五種情況卸察,是必須初始化一個(gè)類的,這五種情況被稱為對(duì) Java 類的主動(dòng)引用铅祸,除了 主動(dòng)引用 之外,其他的對(duì) Java 類的引用稱為 被動(dòng)引用合武。

上面也提到了 Java 類的生命周期總共分為加載临梗、驗(yàn)證準(zhǔn)備稼跳、解析盟庞、初始化使用汤善、卸載什猖,其中最重要的是前五個(gè)步驟加載驗(yàn)證红淡、準(zhǔn)備不狮、解析初始化在旱,那在這五個(gè)步驟中都發(fā)生了什么事情呢摇零?

舉一個(gè)簡(jiǎn)單的例子,如下所示桶蝎。下面的 Constant 類中驻仅,有一個(gè)靜態(tài) static 代碼塊,和一個(gè)靜態(tài) static 變量登渣, 是什么時(shí)候給 value 賦值的呢噪服?什么時(shí)候會(huì)執(zhí)行 static 代碼塊呢?答案是在類的 初始化 階段胜茧。

public class Constant {

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

    public static String value = "lijiankun24!";
}

在 Java 類中粘优,如果有靜態(tài) static 代碼塊、靜態(tài) static 變量的話,編譯器會(huì)為這個(gè)類自動(dòng)生成一個(gè)類構(gòu)造器(注意敬飒,不是實(shí)例構(gòu)造器)邪铲,在 類構(gòu)造器 中會(huì)執(zhí)行靜態(tài) static 代碼塊,初始化靜態(tài) static 變量无拗,類構(gòu)造器 就是在類的 初始化 階段執(zhí)行的

提到 Java 類的加載带到,就不得不說起 Java 中的類加載器 ClassLoader 了,雙親委派模型及其好處也是必須要清楚的英染。

上面只是粗略的介紹揽惹,更多想了解五種主動(dòng)引用、類的生命周期四康、類構(gòu)造器搪搏、類加載器、雙親委派模型闪金,如果想了解的更詳細(xì)疯溺,請(qǐng)看這篇文章 理解 JVM 中的類加載機(jī)制

2.3 Java 虛擬機(jī)字節(jié)碼執(zhí)行引擎

Java 內(nèi)存模型中,非常重要的一個(gè)區(qū)域就是 Java 虛擬機(jī)棧哎垦。Java 中每一個(gè)方法執(zhí)行的時(shí)候都會(huì)在 Java 虛擬機(jī)棧中壓入一個(gè)棧幀囱嫩,方法執(zhí)行完成之后,也會(huì)將該棧幀出棧漏设。
棧幀中最主要的是局部變量表墨闲、操作數(shù)棧這兩個(gè)概念,在執(zhí)行一個(gè) Java 方法的字節(jié)碼時(shí)郑口,其實(shí)就是調(diào)用 Java 字節(jié)碼指令操縱局部變量表鸳碧、操作數(shù)棧,最后將執(zhí)行的結(jié)果返回犬性。如果想學(xué)習(xí) Java 字節(jié)碼指令的話瞻离,推薦一篇文章

除了方法的執(zhí)行過程乒裆,還需要了解一下 Java 中的方法調(diào)用琐脏。方法調(diào)用就是指通過 .class 文件中方法的符號(hào)引用,確認(rèn)方法的直接引用的過程缸兔,這個(gè)過程有可能發(fā)生在加載階段日裙,也有可能發(fā)生在運(yùn)行階段。
有一些方法是在加載階段就已經(jīng)確定了方法的直接引用惰蜜,比如:靜態(tài)方法昂拂、私有方法、實(shí)例構(gòu)造器方法抛猖,這類方法的調(diào)用稱為 解析格侯;除了解析鼻听,方法的 靜態(tài)分派 也是在加載階段就確定了方法的直接引用,這類方法常見的就是 重載 的方法联四。
有一些方法是在運(yùn)行階段確認(rèn)方法的直接引用的撑碴,比如:重寫 的方法,調(diào)用重寫 的方法時(shí)朝墩,需要具體到對(duì)象的實(shí)際類型醉拓,所以需要特定的 Java 字節(jié)碼 invokevirtual 去確定合適的方法慰丛。

Java 虛擬機(jī)是基于棧的解釋執(zhí)行的笑跛,這里所說的 就是 Java 虛擬機(jī)棧,解釋執(zhí)行時(shí)相對(duì)于編譯執(zhí)行而言的热凹,解釋執(zhí)行就是指:代碼通過編譯生成字節(jié)碼指令集之后鹿霸,通過解釋器解釋執(zhí)行的排吴。這個(gè)不用了解的太深,明白這幾個(gè)定義就好

上面介紹了 Java 虛擬機(jī)棧中的 棧幀懦鼠、方法調(diào)用钻哩、解析靜態(tài)分派肛冶、動(dòng)態(tài)分派 和 Java 虛擬機(jī)基于棧的解釋執(zhí)行街氢,詳細(xì)的內(nèi)容可以參考 虛擬機(jī)字節(jié)碼執(zhí)行引擎

三. 訪問者模式 & ASM

3.1 訪問者模式

ASM 庫(kù)是一款基于 Java 字節(jié)碼層面的代碼分析和修改工具淑趾,那 ASM 和訪問者模式有什么關(guān)系呢?訪問者模式主要用于修改和操作一些數(shù)據(jù)結(jié)構(gòu)比較穩(wěn)定的數(shù)據(jù)忧陪,通過前面的學(xué)習(xí)扣泊,我們知道 .class 文件的結(jié)構(gòu)是固定的,主要有常量池嘶摊、字段表延蟹、方法表、屬性表等內(nèi)容叶堆,通過使用訪問者模式在掃描 .class 文件中各個(gè)表的內(nèi)容時(shí)阱飘,就可以修改這些內(nèi)容了。在學(xué)習(xí) ASM 之前虱颗,可以通過這篇文章學(xué)習(xí)一下訪問者模式訪問者模式和 ASM沥匈。

3.2 ASM 庫(kù)的介紹和使用

ASM 可以直接生產(chǎn)二進(jìn)制的 .class 文件,也可以在類被加載入 JVM 之前動(dòng)態(tài)修改類行為忘渔。ASM 庫(kù)的介紹和使用 文章介紹了 ASM 庫(kù)的結(jié)構(gòu)和幾個(gè)重要的 Core Api高帖,包括 ClassVisitor、ClassReader畦粮、ClassWriter散址、MethodVisitor 和 AdviceAdapter 等乖阵,并且通過兩個(gè)簡(jiǎn)單的例子,分別介紹了如何修改 Java 類中方法的字節(jié)碼和修改屬性的字節(jié)碼预麸。

在剛開始使用的時(shí)候瞪浸,可能對(duì)字節(jié)碼的執(zhí)行不是很清楚,使用 ASM 會(huì)比較困難吏祸,ASM 官方也提供了一個(gè)幫助工具 ASMifier对蒲,我們可以先寫出目標(biāo)代碼,然后通過 javac 編譯成 .class 文件犁罩,然后通過 ASMifier 分析此 .class 文件就可以得到需要插入的代碼對(duì)應(yīng)的 ASM 代碼了齐蔽。

上面提到的內(nèi)容,ASM 庫(kù)的 Core Api 和 ASMifier 的使用具體請(qǐng)參閱這篇文章ASM 庫(kù)的介紹和使用 床估。

四. Koala

最后含滴,學(xué)習(xí)完理論知識(shí)以后,為了練手丐巫,寫了一個(gè)小項(xiàng)目谈况,使用自定義 Gradle 插件 + ASM 的方式實(shí)現(xiàn)了和 JakeWhartonhugo 庫(kù)同樣的功能的庫(kù),叫做 Koala递胧,將特定注解的方法的傳入?yún)?shù)碑韵、返回結(jié)果和執(zhí)行時(shí)間打印到 Logcat 中,方便開發(fā)調(diào)試缎脾。

4.1 添加 Koala Gradle Plugin 依賴

在項(xiàng)目工程的 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 使用

使用起來(lái)還是非常簡(jiǎn)單的祝闻,在 Java 的方法上添加 @KoalaLog 注解,如下所示:

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

當(dāng)上述方法被調(diào)用的時(shí)候遗菠,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 混淆規(guī)則

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

歡迎 star 和 fork Koala联喘,也歡迎點(diǎn)贊和收藏

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辙纬,隨后出現(xiàn)的幾起案子豁遭,更是在濱河造成了極大的恐慌,老刑警劉巖贺拣,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蓖谢,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡譬涡,警方通過查閱死者的電腦和手機(jī)闪幽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)涡匀,“玉大人沟使,你說我怎么就攤上這事≡ò希” “怎么了腊嗡?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵着倾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我燕少,道長(zhǎng)卡者,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任客们,我火速辦了婚禮崇决,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘底挫。我一直安慰自己恒傻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布建邓。 她就那樣靜靜地躺著盈厘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪官边。 梳的紋絲不亂的頭發(fā)上沸手,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音注簿,去河邊找鬼契吉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛诡渴,可吹牛的內(nèi)容都是我干的捐晶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼妄辩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惑灵!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起恩袱,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泣棋,失蹤者是張志新(化名)和其女友劉穎胶哲,沒想到半個(gè)月后畔塔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸯屿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年澈吨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寄摆。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谅辣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出婶恼,到底是詐尸還是另有隱情桑阶,我是刑警寧澤柏副,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站蚣录,受9級(jí)特大地震影響割择,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萎河,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一荔泳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虐杯,春花似錦玛歌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至确憨,卻和暖如春译荞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背休弃。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工吞歼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人塔猾。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓篙骡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親丈甸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子糯俗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,510評(píng)論 25 707
  • 最近帶啥出去就丟啥,感覺腦子被抽真空了睦擂!希望是真的破財(cái)免災(zāi)吧得湘!我要好好的,不能出岔子啦顿仇!已經(jīng)破財(cái)了淘正,一定要免...
    瑜伽館專業(yè)預(yù)售閱讀 226評(píng)論 0 0
  • 為了迎接明天的兒童節(jié),小朋友特別提出要幫我洗碗臼闻,我當(dāng)然要滿足他啰鸿吆。洗完之后跟我一起下樓的時(shí)候偷偷跟我講:“以后我們...
    yoly0915閱讀 186評(píng)論 0 0
  • 你遇見的搞笑的試卷答案是什么惩淳?小編就帶領(lǐng)大家來(lái)看看別人的答案。 【1】 【2】 【3】 【4】 【5】 【6】 【...
    夢(mèng)想遠(yuǎn)航9閱讀 316評(píng)論 0 0
  • 你爸爸提出離婚已經(jīng)2個(gè)月乓搬,在這期間即痛苦也是高興(過后)的思犁。前一個(gè)月我一直在改變著自己代虾,因?yàn)槲乙庾R(shí)到自己作為妻子和...
    娜哈啊哈閱讀 980評(píng)論 0 0