JavaAgent 與 動(dòng)態(tài)代理

ASM, CGlib, Java Proxy, Javassist都是可以操作字節(jié)碼坛吁,但是這些操作字節(jié)碼都需要等到類加載到JVM中之后再對(duì)字節(jié)碼進(jìn)行重寫谋作。
JavaAgent則是一個(gè)可以在加載前就進(jìn)行重寫,然后再加載的方式教馆。

JavaAgent

JavaAgent 是JDK 1.5 以后引入的馋缅,也可以叫做Java代理扒腕。是運(yùn)行在 main方法之前的攔截器,它內(nèi)定的方法名叫 premain 股囊,也就是說先執(zhí)行 premain 方法然后再執(zhí)行 main 方法袜匿。
那么如何實(shí)現(xiàn)一個(gè) JavaAgent 呢更啄?很簡(jiǎn)單稚疹,只需要增加 premain 方法即可。

public class TestPreMain {
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=========premain方法執(zhí)行========");
        System.out.println(agentOps);
        inst.addTransformer(new FirstAgent());
    }
}

說明一下premain方法祭务。該方法的原理與 main 應(yīng)用程序入口點(diǎn)類似内狗。在 Java 虛擬機(jī) (JVM) 初始化后,每個(gè) premain 方法將按照指定代理的順序調(diào)用义锥,然后將調(diào)用實(shí)際的應(yīng)用程序 main 方法柳沙。

JVMTI(Java Virtual Machine Tool Interface)是一套本地編程接口集合,它提供了一套”代理”程序機(jī)制拌倍,可以支持第三方工具程序以代理的方式連接和訪問 JVM赂鲤,并利用 JVMTI 提供的豐富的編程接口,完成很多跟 JVM 相關(guān)的功能柱恤。關(guān)于 JVMTI 的詳細(xì)信息数初,請(qǐng)參考 Java SE 6 文檔(請(qǐng)參見 參考資源)當(dāng)中的介紹。

java.lang.instrument 包的實(shí)現(xiàn)梗顺,也就是基于這種機(jī)制的:在 Instrumentation 的實(shí)現(xiàn)當(dāng)中泡孩,存在一個(gè) JVMTI 的代理程序,通過調(diào)用 JVMTI 當(dāng)中 Java 類相關(guān)的函數(shù)來完成 Java 類的動(dòng)態(tài)操作寺谤。

Instrumentation 的最大作用仑鸥,就是類定義動(dòng)態(tài)改變和操作。在 Java SE 5 及其后續(xù)版本當(dāng)中变屁,開發(fā)者可以在一個(gè)普通 Java 程序(帶有 main 函數(shù)的 Java 類)運(yùn)行時(shí)眼俊,通過 – javaagent參數(shù)指定一個(gè)特定的 jar 文件(包含 Instrumentation 代理)來啟動(dòng) Instrumentation 的代理程序。


public class FirstAgent implements ClassFileTransformer {
    public final String injectedClassName = "test.agent.wxl";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace("/", ".");
//        System.out.println(className);
        if (className.startsWith(injectedClassName)) {
            CtClass ctclass = null;
            try {
                ctclass = ClassPool.getDefault().get(className);// 使用全稱,用于取得字節(jié)碼類<使用javassist>
                CtMethod[] ctmethods = ctclass.getMethods();
                for (CtMethod ctMethod : ctmethods) {
                    CodeAttribute ca = ctMethod.getMethodInfo2().getCodeAttribute();
                    if (ca == null) {
                        continue;
                    }
                    if (!ctMethod.isEmpty()) {
//                        System.out.println(ctMethod.getName());
                          ctMethod.insertBefore("System.out.println(\"hello Im agent : " + ctMethod.getName() + "\");");
                    }
                }
                return ctclass.toBytecode();
            } catch (Exception e) {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
        }
        return null;
    }
}

實(shí)現(xiàn) ClassFileTransformer 這個(gè)接口的目的就是在class被裝載到JVM之前將class字節(jié)碼轉(zhuǎn)換掉粟关,從而達(dá)到動(dòng)態(tài)注入代碼的目的泵琳。其中代碼的修改使用到了javassist。
上段代碼實(shí)現(xiàn)了把特定包下面的class的所有類的所有有body的非native的方法前面添加一句
System.out.println("hello im agent :" + ctMethod.getName());

在pom.xml文件中添加插件:

          <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>agent.TestPreMain</Premain-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

構(gòu)建后會(huì)發(fā)現(xiàn)jar包里面的MANIFEST.MF文件如下

Manifest-Version: 1.0
Premain-Class: agent.TestPreMain
Archiver-Version: Plexus Archiver
Built-By: xiaolong.wei
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_121

最后創(chuàng)建一個(gè)被代理的工程類

public class TestAgentDemo {

    public static void main(String... args) {
        testAgent();

        testAgent1("1", "2");
    }

    public static void testAgent() {
        System.out.println("test agent say hello");
    }

    public static void testAgent1(String one, String two) {
        System.out.println("test agent say hello" + one + two);
    }
}

在啟動(dòng)的時(shí)候使用-javaagent:來指定jar包

javaagent

運(yùn)行:

運(yùn)行結(jié)果

可以看到方法main, testAgent testAgent1 的前面都被加入了代碼.

JavaAgent 實(shí)際應(yīng)用中

1. 可以在加載java文件之前做攔截把字節(jié)碼做修改
2. 獲取所有已經(jīng)被加載過的類
3. 獲取某個(gè)對(duì)象的大小
4. 將某個(gè)jar加入到bootstrapclasspath里作為高優(yōu)先級(jí)被bootstrapClassloader加載
5. 將某個(gè)jar加入到classpath里供AppClassload去加載
6. 設(shè)置某些native方法的前綴,主要在查找native方法的時(shí)候做規(guī)則匹配

動(dòng)態(tài)代理

動(dòng)態(tài)代理的實(shí)現(xiàn):

  1. cglib (Code Generation Library) 實(shí)現(xiàn)
  2. InvocationHandler

代理模式

代理模式就是給某一個(gè)對(duì)象提供一個(gè)代理對(duì)象获列,并由代理對(duì)象控制對(duì)原對(duì)象的引用谷市。通俗的來講代理模式就是我們生活中常見的中介。

如下圖

代理模式

實(shí)際使用中击孩∑扔疲客戶端獲取到的是代理對(duì)象,而代理對(duì)象ProxyObject把請(qǐng)求轉(zhuǎn)發(fā)給RealObject巩梢,ProxyObject是不做業(yè)務(wù)的创泄,實(shí)際工作的其實(shí)只有RealObject 。

如果編寫一個(gè)類的同時(shí)就要一個(gè)代理類括蝠,那這樣系統(tǒng)就會(huì)變得臃腫且難以維護(hù)鞠抑,所以出現(xiàn)了動(dòng)態(tài)代理。在運(yùn)行狀態(tài)中忌警,需要代理的地方搁拙,根據(jù)AbstractObject和RealObject,動(dòng)態(tài)地創(chuàng)建一個(gè)Proxy法绵,用完之后箕速,就會(huì)銷毀,這樣就可以避免了Proxy 角色的class在系統(tǒng)中冗雜的問題了朋譬。

InvocationHandler

在考慮代理類的時(shí)候盐茎,我們無非是想在代理類調(diào)用實(shí)際對(duì)象的前后進(jìn)行一些業(yè)務(wù)處理,如下圖徙赢。


代理

這就造成了代理類的通用性字柠,只需要在invoke的前后加入特定的代碼就可以了。所以就出現(xiàn)了將所有通用的調(diào)用真實(shí)方法的管理器狡赐,讓這個(gè)觸發(fā)管理器統(tǒng)一去管理就叫做Invocation Handler窑业。

JDK通過 java.lang.reflect包下面的ProxyInvocationHandler來支持動(dòng)態(tài)代理。

RealObject這個(gè)類創(chuàng)建一個(gè)動(dòng)態(tài)代理對(duì)象阴汇,JDK主要會(huì)做以下工作:

1.   獲取 RealObject上的所有接口列表数冬;
2.   確定要生成的代理類的類名,默認(rèn)為:com.sun.proxy.$ProxyXXXX 搀庶;
3.   根據(jù)需要實(shí)現(xiàn)的接口信息拐纱,在代碼中動(dòng)態(tài)創(chuàng)建 該P(yáng)roxy類的字節(jié)碼;
4 .  將對(duì)應(yīng)的字節(jié)碼轉(zhuǎn)換為對(duì)應(yīng)的class 對(duì)象哥倔;
5.   創(chuàng)建InvocationHandler實(shí)例handler秸架,用來處理Proxy所有方法調(diào)用;
6.   Proxy 的class對(duì)象 以創(chuàng)建的handler對(duì)象為參數(shù)咆蒿,實(shí)例化一個(gè)proxy對(duì)象

InvocationHandler东抹,我們需要實(shí)現(xiàn)下列的invoke方法:
在調(diào)用代理對(duì)象中的每一個(gè)方法時(shí)蚂子,在代碼內(nèi)部,都是直接調(diào)用了InvocationHandler 的invoke方法缭黔,而invoke方法根據(jù)代理類傳遞給自己的method參數(shù)來區(qū)分是什么方法食茎。

public interface Subject {
    int getInt(Integer i);
}
public interface Subject2 {
    String getString(String source);
}
public class RealSubject implements Subject, Subject2 {
    @Override
    public int getInt(Integer i) {
        System.out.println("this is method of getInt " + i++);
        return i;
    }

    @Override
    public String getString(String source) {
        System.out.println("source string is :" + source);
        source = source + " suffix";
        return source;
    }
}

代理類

public class SubjectProxy implements InvocationHandler {

    private RealSubject subject;

    public SubjectProxy(RealSubject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method " + method.getName() + "invoke by proxy, start");
        Object o = method.invoke(subject, args);
        System.out.println("method " + method.getName() + "invoke by proxy, end");
        return o;
    }
}

測(cè)試代碼

public class ProxyMain {
    public static void main(String... args) {
        RealSubject subject = new RealSubject();
        //創(chuàng)建一個(gè)Invoke Handler 
        SubjectProxy proxy = new SubjectProxy(subject);
        // 通過Proxy newProxyInstance方法創(chuàng)建代理類
        Object o = Proxy.newProxyInstance(ProxyMain.class.getClassLoader(), subject.getClass().getInterfaces(), proxy);
        Subject realSubject = (Subject) o;
        int result = realSubject.getInt(12);
        System.out.println("result:" + result);
        Subject2 s2 = (Subject2) o;
        s2.getString("hello");
    }
}

看一下代碼運(yùn)行結(jié)果


image.png

當(dāng)然JDK的InvokeHandler也是生成了一個(gè)代理類。

JDK提供了sun.misc.ProxyGenerator.generateProxyClass(String proxyName,class[] interfaces) 底層方法來產(chǎn)生動(dòng)態(tài)代理類的字節(jié)碼:

cglib —— 通過類繼承

JDK提供的生成動(dòng)態(tài)代理的方式有個(gè)明顯的特點(diǎn)就是被代理的類它必須要有實(shí)現(xiàn)的接口才行馏谨。既RealObject必須實(shí)現(xiàn)了某個(gè)接口或者某幾個(gè)接口别渔,才能代理接口這些接口的方法。
還好有CGLib惧互。CGLIB(Code Generation Library)哎媚,是一個(gè)強(qiáng)大的,高性能喊儡,高質(zhì)量的Code生成類庫拨与,它可以在運(yùn)行期擴(kuò)展Java類與實(shí)現(xiàn)Java接口。而CGLIB底層使用的是ASM庫艾猜。

cglib 創(chuàng)建某個(gè)類A的動(dòng)態(tài)代理類的模式是:

1.   查找A上的所有非final 的public類型的方法定義买喧;
2.   將這些方法的定義轉(zhuǎn)換成字節(jié)碼;
3.   將組成的字節(jié)碼轉(zhuǎn)換成相應(yīng)的代理的class對(duì)象箩朴;
4.   實(shí)現(xiàn) MethodInterceptor接口岗喉,用來處理 對(duì)代理類上所有方法的請(qǐng)求(這個(gè)接口和JDK動(dòng)態(tài)代理InvocationHandler的功能和角色是一樣的)

被代理類

public class ElectricCar {
    public boolean charging(Double fee) {
        System.out.println("charging ~~ Fee:" + fee);
        return true;
    }

    public boolean driver(String van, Integer color) {
        System.out.println(van + " is runing ");
        return true;
    }
}

代理方法

public class CarInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("intercept in car before");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("intercept in car after");
        return result;
    }
}

測(cè)試方法

public class CgLibMain {

    public static void main(String... args) {
        //生成class文件 保存
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\work\\agentDemo\\target");
      
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ElectricCar.class);
        CarInterceptor carInterceptor = new CarInterceptor();
        //設(shè)置代理前后的Interceptor
        enhancer.setCallback(carInterceptor);
        ElectricCar electricCar = (ElectricCar) enhancer.create();
        electricCar.driver("川A12131", 1);
        electricCar.charging(12d);
    }
}

代碼運(yùn)行結(jié)果:


image.png

參考:
https://blog.csdn.net/luanlouis/article/details/24589193
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秋度,一起剝皮案震驚了整個(gè)濱河市炸庞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌荚斯,老刑警劉巖埠居,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異事期,居然都是意外死亡滥壕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門兽泣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绎橘,“玉大人,你說我怎么就攤上這事唠倦〕屏郏” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵稠鼻,是天一觀的道長(zhǎng)冈止。 經(jīng)常有香客問我,道長(zhǎng)候齿,這世上最難降的妖魔是什么熙暴? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任闺属,我火速辦了婚禮,結(jié)果婚禮上周霉,老公的妹妹穿的比我還像新娘掂器。我一直安慰自己,他們只是感情好俱箱,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布唉匾。 她就那樣靜靜地躺著,像睡著了一般匠楚。 火紅的嫁衣襯著肌膚如雪巍膘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天芋簿,我揣著相機(jī)與錄音峡懈,去河邊找鬼。 笑死与斤,一個(gè)胖子當(dāng)著我的面吹牛肪康,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撩穿,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼磷支,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了食寡?” 一聲冷哼從身側(cè)響起雾狈,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎抵皱,沒想到半個(gè)月后善榛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呻畸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年移盆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伤为。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡咒循,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绞愚,到底是詐尸還是另有隱情叙甸,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布爽醋,位于F島的核電站蚁署,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蚂四。R本人自食惡果不足惜光戈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一哪痰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧久妆,春花似錦晌杰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至烂琴,卻和暖如春爹殊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奸绷。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工梗夸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人号醉。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓反症,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親畔派。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铅碍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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

  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陳阿飛閱讀 865評(píng)論 1 1
  • 一、基本概念 1.什么是代理线椰? 在闡述JDK動(dòng)態(tài)代理之前胞谈,我們很有必要先來弄明白代理的概念。代理這個(gè)詞本身并不是計(jì)...
    小李彈花閱讀 16,443評(píng)論 2 40
  • 思考:尊重自己的感覺悔叽,就是把一切交給身體莱衩,也就是感受來做,這比頭腦下決定要舒服很多娇澎。 太過于理性的東西笨蚁,給人一種過...
    楊雪雪閱讀 186評(píng)論 0 0
  • 打造品牌,也許并不是每天企業(yè)正在做的事情趟庄,但必然是企業(yè)未來的發(fā)展方向括细,對(duì)于打造品牌各個(gè)企業(yè)也有自己的見解,也在根據(jù)...
    housheng閱讀 417評(píng)論 0 0
  • 早已經(jīng)習(xí)慣了孤獨(dú)的活著戚啥,喜歡一個(gè)人安安靜靜的完成所有事情奋单,喜歡獨(dú)處,我想是因?yàn)檎也坏酵耆就篮系呐笥寻桑?越發(fā)現(xiàn)...
    LoveQMT閱讀 151評(píng)論 0 1