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 中的一些例子。
- JakeWharton 的 hugo 就是一個(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)容,如下圖所示
從上圖中谒府,我們可以看到,.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)了和 JakeWharton 的 hugo 庫(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)贊和收藏
- Email: jiankunli24@gmail.com
- Home: http://lijiankun24.com
- 簡(jiǎn)書: http://www.reibang.com/u/1abe21b7ff5f
- 微博: http://weibo.com/lijiankun24