Java AOP利劍之ASM卢肃,真正的AOP

在前面有一篇博客中講了如何通過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)

code_structure

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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末度帮,一起剝皮案震驚了整個(gè)濱河市歼捏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌笨篷,老刑警劉巖瞳秽,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異率翅,居然都是意外死亡练俐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門安聘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痰洒,“玉大人,你說我怎么就攤上這事浴韭∏鹩鳎” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵念颈,是天一觀的道長泉粉。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么嗡靡? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任跺撼,我火速辦了婚禮,結(jié)果婚禮上讨彼,老公的妹妹穿的比我還像新娘歉井。我一直安慰自己,他們只是感情好哈误,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布哩至。 她就那樣靜靜地躺著,像睡著了一般蜜自。 火紅的嫁衣襯著肌膚如雪菩貌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天重荠,我揣著相機(jī)與錄音箭阶,去河邊找鬼。 笑死戈鲁,一個(gè)胖子當(dāng)著我的面吹牛仇参,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播婆殿,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼冈敛,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了鸣皂?” 一聲冷哼從身側(cè)響起抓谴,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寞缝,沒想到半個(gè)月后癌压,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡荆陆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年滩届,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片被啼。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡帜消,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浓体,到底是詐尸還是另有隱情泡挺,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布命浴,位于F島的核電站娄猫,受9級(jí)特大地震影響贱除,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜媳溺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一月幌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悬蔽,春花似錦扯躺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至难衰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逗栽,已是汗流浹背盖袭。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彼宠,地道東北人鳄虱。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像凭峡,于是被迫代替她去往敵國和親拙已。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

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

  • 小編費(fèi)力收集:給你想要的面試集合 1.C++或Java中的異常處理機(jī)制的簡單原理和應(yīng)用摧冀。 當(dāng)JAVA程序違反了JA...
    八爺君閱讀 4,580評(píng)論 1 114
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程倍踪,因...
    小菜c閱讀 6,373評(píng)論 0 17
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,805評(píng)論 0 11
  • JAVA面試題 1、作用域public,private,protected,以及不寫時(shí)的區(qū)別答:區(qū)別如下:作用域 ...
    JA尐白閱讀 1,146評(píng)論 1 0
  • 美食紀(jì)錄片向來是我的最愛康谆,舌尖和尋味順德都反復(fù)看過幾遍领斥。 鏡頭下的食物配合著旁邊,引誘著人的口水沃暗,深夜看一場(chǎng)月洛,手上...
    徐歡眉閱讀 410評(píng)論 0 5