在前面有一篇博客中講了如何通過Java的動(dòng)態(tài)代理來實(shí)現(xiàn)AOP編程幻锁。之前也講過其實(shí)動(dòng)態(tài)代理并不能算代碼層面的AOP編程具篇,其實(shí)質(zhì)是在運(yùn)行時(shí)動(dòng)態(tài)生成一個(gè)類然后以靜態(tài)代理的方式來實(shí)現(xiàn)AOP編程的拳氢。
今天要講的ASM是直接通過對(duì)代碼進(jìn)行修改沒有使用代理來進(jìn)行AOP編程的募逞。
ASM簡介
這篇博客主要是將如何使用ASM來實(shí)現(xiàn)AOP的,ASM不是重點(diǎn)馋评,因此ASM在這里就只做一些簡單的介紹放接。
ASM是一個(gè)用來生成字節(jié)碼
或者修改字節(jié)碼
的開源庫。官網(wǎng)地址
org.objectweb.asm
包是一些基本的ASM類型留特,比如ClassReader纠脾、ClassWritter都在這里面。依賴于ASM的訪問者模式
可以很輕易的就在ClassReader蜕青、ClassWritter之間添加新的訪問者進(jìn)行字節(jié)碼的修改
org.objectweb.asm.tree
包下類主要的作用是將輸入的字節(jié)碼以樹的形式
進(jìn)行保存苟蹈,這樣做好處是可以很清晰地在對(duì)應(yīng)的節(jié)點(diǎn)進(jìn)行字節(jié)碼的修改,用著也比直接中間插入訪問者方便右核。
簡單來說慧脱,第一種在ClassReader、ClassWritter的方式類似于xml的sax解析贺喝;而使用樹形式則類似于dom解析菱鸥。
這篇博客只會(huì)用到上面兩個(gè)包下的類宗兼,其它的包有興趣的可以自己去看一下這里就不一一介紹了。
更詳細(xì)ASM的入門可以看Instrumenting Java Bytecode with ASM氮采,里面介紹的例子是使用的ClassReader殷绍、ClassWritter之間夾訪問者的方式。
本篇博客使用的樹節(jié)點(diǎn)的方式進(jìn)行字節(jié)碼的修改實(shí)現(xiàn)AOP鹊漠。
準(zhǔn)備
首先去官網(wǎng)的這個(gè)地方的asm和asm-tree目錄下下載7.1版本的jar包主到。
代碼結(jié)構(gòu)
common包下的代碼就是前面中common包下一樣的代碼這里就不介紹了。
asm包下的代碼會(huì)對(duì)LinuxPC躯概、LinuxService的字節(jié)碼進(jìn)行修改登钥,進(jìn)行SS(由于簡書貌似會(huì)屏蔽全稱,這里使用簡稱)屬性的添加以及代理的執(zhí)行楞陷。
common
pc
package common.pc;
/**
* 代表一臺(tái)個(gè)人電腦
*/
public interface PC {
void searchByGoogle();
}
package common.pc;
/**
* Linux系統(tǒng)的個(gè)人電腦
*/
public class LinuxPC implements PC {
@Override
public void searchByGoogle() {
System.out.println("Searching with Google by LinuxPC");
}
}
service
package common.service;
/**
* 代表一臺(tái)服務(wù)器
*/
public interface Service {
void searchByGoogle();
}
package common.service;
/**
* 一臺(tái)Linux服務(wù)器
*/
public class LinuxService implements Service {
@Override
public void searchByGoogle() {
System.out.println("Searching with Google by LinuxService");
}
}
SS
package common;
/**
* 現(xiàn)實(shí)中SS是要分平臺(tái)的怔鳖,這里為了方便就想成全平臺(tái)用一個(gè)SS」潭辏總之就想成服務(wù)端實(shí)現(xiàn)代理的一個(gè)軟件吧结执。
* 而且現(xiàn)實(shí)中不僅服務(wù)端需要安裝SS,客戶端也是需要的艾凯。這里客戶端也忽略掉献幔。
*/
public class SS {
public void startProxy() {
System.out.println("start proxy");
}
public void stopProxy() {
System.out.println("stop proxy");
}
}
asm
package asm;
import common.SS;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import static org.objectweb.asm.Opcodes.*;
public class InstallSS {
private static final String SS_DESCRIPTER = Type.getDescriptor(SS.class);
private static final String SS = Type.getInternalName(SS.class);
private static String FIELD_OWNER = "";
public static void main(String[] args) {
FIELD_OWNER = args[1].substring(0, args[1].length() - 6);
// 其中arg[0]是源字節(jié)碼文件,args[1]是目標(biāo)字節(jié)碼文件
installSS(args[0], args[1]);
}
/**
* 進(jìn)行SS的安裝
* @param src 源字節(jié)碼
* @param dst 目標(biāo)字節(jié)碼
*/
public static void installSS(String src, String dst) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
byte[] outputByteCode;
fis = new FileInputStream(src);
// ClassReader讀入字節(jié)碼
ClassReader cr = new ClassReader(fis);
// ClassNode將字節(jié)碼以節(jié)點(diǎn)樹的形式表示
ClassNode cn = new ClassNode(ASM7);
// SKIP_FRAMES用于避免訪問幀內(nèi)容趾诗,因?yàn)楦淖冏止?jié)碼的過程中幀內(nèi)容會(huì)被改變蜡感,比如局部變量、操作數(shù)棧都可能改變恃泪。
cr.accept(cn, ClassReader.SKIP_FRAMES);
// 進(jìn)行SS屬性的添加
addSSField(cn.fields);
for (MethodNode methodNode : cn.methods) {
if (methodNode.name.equals("<init>")) {
// 構(gòu)造器中對(duì)SS屬性進(jìn)行初始化
initSSField(methodNode);
} else if (methodNode.name.equals("searchByGoogle")) {
// searchByGoogle方法中添加SS的調(diào)用
addSSExecute(methodNode);
}
}
// COMPUTE_FRAMES表示ASM會(huì)自動(dòng)計(jì)算所有內(nèi)容郑兴,visitFrame和visitMaxs方法都會(huì)被忽略掉
// 還有一個(gè)COMPUTE_MAXS是會(huì)自定計(jì)算局部變量表和操作數(shù)棧的大小,visitMaxs會(huì)被忽略掉贝乎。
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cn.accept(cw);
// 生成的字節(jié)碼寫入目標(biāo)文件中
outputByteCode = cw.toByteArray();
fos = new FileOutputStream(dst);
fos.write(outputByteCode);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 給src中的字節(jié)碼添加SS的屬性
* @param fields
*/
private static void addSSField(List<FieldNode> fields) {
boolean isHaveSSFiled = false;
for (FieldNode fieldNode : fields) {
if (fieldNode.desc.equals(SS_DESCRIPTER)) {
isHaveSSFiled = true;
break;
}
}
if (!isHaveSSFiled) {
fields.add(new FieldNode(ACC_PRIVATE, "SS", SS_DESCRIPTER, null, null));
}
}
/**
* 在構(gòu)造方法中對(duì)SS屬性進(jìn)行初始化
* @param methodNode 表示該字節(jié)碼一個(gè)方法節(jié)點(diǎn)的值
*/
private static void initSSField(MethodNode methodNode) {
AbstractInsnNode[] nodes = methodNode.instructions.toArray();
int length = nodes.length;
// 初始化相關(guān)的字節(jié)碼指令
InsnList insnList = new InsnList();
insnList.add(new VarInsnNode(ALOAD, 0));
insnList.add(new TypeInsnNode(NEW, SS));
insnList.add(new InsnNode(DUP));
insnList.add(new MethodInsnNode(INVOKESPECIAL, SS, "<init>", "()V", false));
insnList.add(new FieldInsnNode(PUTFIELD, FIELD_OWNER, "SS", SS_DESCRIPTER));
methodNode.instructions.insertBefore(nodes[length - 1], insnList);
}
/**
* 在searchByGoogle方法調(diào)用中進(jìn)行SS的startProxy和stopProxy調(diào)用情连。
* @param methodNode 表示該字節(jié)碼一個(gè)方法節(jié)點(diǎn)的值
*/
private static void addSSExecute(MethodNode methodNode) {
AbstractInsnNode[] nodes = methodNode.instructions.toArray();
int length = nodes.length;
// searchByGoogle方法前面添加上SS的startProxy方法調(diào)用
InsnList startInsnList = new InsnList();
startInsnList.add(new VarInsnNode(ALOAD, 0));
startInsnList.add(new FieldInsnNode(GETFIELD, FIELD_OWNER, "SS", SS_DESCRIPTER));
startInsnList.add(new MethodInsnNode(INVOKEVIRTUAL, SS, "startProxy", "()V", false));
methodNode.instructions.insertBefore(nodes[0], startInsnList);
// searchByGoogle方法的后面加上SS的stopProxy方法的調(diào)用
InsnList endInsnList = new InsnList();
endInsnList.add(new VarInsnNode(ALOAD, 0));
endInsnList.add(new FieldInsnNode(GETFIELD, FIELD_OWNER, "SS", SS_DESCRIPTER));
endInsnList.add(new MethodInsnNode(INVOKEVIRTUAL, SS, "stopProxy", "()V", false));
methodNode.instructions.insertBefore(nodes[length - 1], endInsnList);
}
}
字節(jié)碼修改的過程這里就不詳解了,重點(diǎn)注釋已經(jīng)寫出來了览效。
這里對(duì)字節(jié)碼的修改主要使用了的org.objectweb.asm.tree包下的內(nèi)容却舀,比如說ClassNode、MethodNode锤灿、FieldNode都是這個(gè)包下的挽拔。
當(dāng)然也有org.objectweb.asm包下的,主要就是ClassReader但校、ClassWriter用于對(duì)字節(jié)碼輸入以及輸出螃诅。
主包
import common.pc.LinuxPC;
import common.service.LinuxService;
public class Main {
public static void main(String[] args) {
LinuxPC linuxPC = new LinuxPC();
LinuxService linuxService = new LinuxService();
linuxPC.searchByGoogle();
linuxService.searchByGoogle();
}
}
很簡單就是基本的調(diào)用。
然后主包下的兩個(gè)jar包是在準(zhǔn)備階段的時(shí)候進(jìn)行下載的。
編譯執(zhí)行
然后在主包下依次輸入下面的命令:
# 編譯兩種不同類型的國外電腦
javac common/pc/LinuxPC.java
javac common/service/LinuxService.java
# 對(duì)上面編譯出來的字節(jié)碼做一個(gè)備份
cp common/pc/LinuxPC.class common/pc/LinuxPC.class.bak
cp common/service/LinuxService.class common/service/LinuxService.class.bak
# 對(duì)修改字節(jié)碼進(jìn)行SS添加并調(diào)用的類進(jìn)行編譯
javac -cp asm-tree-7.1.jar:asm-7.1.jar:. asm/InstallSS.java
# 執(zhí)行InstallSS對(duì)LinuxPC术裸、LinuxService字節(jié)碼進(jìn)行修改(從備份文件中讀取修改的字節(jié)碼方法對(duì)應(yīng)的.class結(jié)尾的文件)
java -cp .:asm-7.1.jar:asm-tree-7.1.jar asm.InstallSS common/pc/LinuxPC.class.bak common/pc/LinuxPC.class
java -cp .:asm-7.1.jar:asm-tree-7.1.jar asm.InstallSS common/service/LinuxService.class.bak common/service/LinuxService.class
# 編譯并調(diào)用Main
javac Main.java
java Main
執(zhí)行之后可以看到如下執(zhí)行結(jié)果:
start proxy
Searching with Google by LinuxPC
stop proxy
start proxy
Searching with Google by LinuxService
stop proxy
其實(shí)修改后的LinuxPC和LinuxService分別如下:
修改后的LinuxPC.java:
package common.pc;
import common.SS;
public class LinuxPC implements PC {
private SS SS;
public LinuxPC() {
SS = new SS();
}
@Override
public void searchByGoogle() {
SS.startProxy();
System.out.println("Searching with Google by LinuxPC");
SS.stopProxy();
}
}
修改后的LinuxService.java:
package common.service;
import common.SS;
public class LinuxService implements Service {
private SS SS;
public LinuxService() {
SS = new SS();
}
@Override
public void searchByGoogle() {
SS.startProxy();
System.out.println("Searching with Google by LinuxService");
SS.stopProxy();
}
}
可以看到使用ASM這里沒有用靜態(tài)代理或動(dòng)態(tài)代理空执,而是通過直接修改字節(jié)碼來實(shí)現(xiàn)了不同類型的類相同功能的添加。
總結(jié)
本篇博客就一個(gè)目標(biāo)通過ASM使用樹節(jié)點(diǎn)的形式來實(shí)現(xiàn)AOP編程
然后就是ASM修改字節(jié)碼的兩種方式:
- 借助于訪問者模式穗椅,在ClassReader、ClassWritter之間添加自己實(shí)現(xiàn)的ClassVisitor來實(shí)現(xiàn)奶栖∑ケ恚可以參考Instrumenting Java Bytecode with ASM中的例子
- 通過樹節(jié)點(diǎn)的形式來進(jìn)行字節(jié)碼修改的實(shí)現(xiàn)。本篇博客使用的方式宣鄙。
然后兩個(gè)方式的優(yōu)缺點(diǎn):
- 添加ClassVisitor的方式比較簡便袍镀,而且只用引org.objectweb.asm這一個(gè)jar包就行。但是修改起來邏輯不太清晰冻晤。
- 通過樹節(jié)點(diǎn)的方式苇羡,類、屬性鼻弧、方法等都被抽象為一個(gè)個(gè)樹節(jié)點(diǎn)设江。要對(duì)某個(gè)方法、某個(gè)屬性進(jìn)行修改十分的清新攘轩,而且更好控制叉存。不過需要引用org.objectweb.asm和org.objectweb.asm.tree兩個(gè)jar,稍微麻煩點(diǎn)