使用javaagent技術(shù)實(shí)現(xiàn)無侵入監(jiān)聽

什么是 Java Agent

Java Agent是JVM啟動時(shí)給應(yīng)用程序一種機(jī)會去修改class文件的機(jī)制畜疾,在啟動的VM參數(shù)增加-javaagent 再加上自定義的擴(kuò)展來實(shí)現(xiàn)砌滞,這種自定義擴(kuò)展是一種插件開發(fā)機(jī)制。

使用 Java Agent 的步驟大致如下:

  1. 定義一個(gè) MANIFEST.MF 文件哀蘑,在其中添加 premain-class 配置項(xiàng)苫拍。

  2. 創(chuàng)建 premain-class 配置項(xiàng)指定的類瓜富,并在其中實(shí)現(xiàn) premain() 方法,方法簽名如下:

public static void premain(String agentArgs, Instrumentation inst){

   ... 

}
  1. 將 MANIFEST.MF 文件和 premain-class 指定的類一起打包成一個(gè) jar 包。

  2. 使用 -javaagent 指定該 jar 包的路徑即可執(zhí)行其中的 premain() 方法厘托。

如果使用maven打包友雳,可以省去上面繁瑣的操作,配置如下:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <classpathPrefix>lib/</classpathPrefix>
                <mainClass>com.preapm.agent.Bootstrap</mainClass>
            </manifest>
            <manifestEntries>
                <Premain-Class>com.preapm.agent.APMAgentPremain</Premain-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

實(shí)例


public class TestAgent {
    public static void premain(String agentArgs, 
            Instrumentation inst) {
        System.out.println("this is a java agent with two args");
        System.out.println("參數(shù):" + agentArgs + "\n");
    }

    public static void premain(String agentArgs) {
        System.out.println("this is a java agent only one args");
        System.out.println("參數(shù):" + agentArgs + "\n");
    }

}

premain() 方法有兩個(gè)重載铅匹,如下所示押赊,如果兩個(gè)重載同時(shí)存在,【1】將會被忽略包斑,只執(zhí)行【2】

public static void premain(String agentArgs) [1]
public static void premain(String agentArgs, 
      Instrumentation inst); [2]

agentArgs 參數(shù):-javaagent 命令攜帶的參數(shù)流礁。在前面介紹 SkyWalking Agent 接入時(shí)提到,agent.service_name 這個(gè)配置項(xiàng)的默認(rèn)值有三種覆蓋方式罗丰,其中神帅,使用探針配置進(jìn)行覆蓋,探針配置的值就是通過該參數(shù)傳入的萌抵。
inst 參數(shù):java.lang.instrumen.Instrumentation 是 Instrumention 包中定義的一個(gè)接口找御,它提供了操作類定義的相關(guān)方法。

如何用java agent修改class,比如實(shí)現(xiàn)監(jiān)控功能在getNumber前后增加AOP邏輯绍填?

比如有下面這個(gè)類


public class TestClass {
    public int getNumber() { return 1;  }
}

接下來需要編寫一個(gè)Transformer類來實(shí)現(xiàn)對類的修改霎桅,下面以javassist為字節(jié)碼注入工具進(jìn)行演示,在方法前后擇增加統(tǒng)計(jì)時(shí)間讨永。


public class TestAgent {
    public static void premain(String agentArgs, Instrumentation inst) 
              throws Exception {

        // 注冊一個(gè) Transformer滔驶,該 Transformer在類加載時(shí)被調(diào)用
        inst.addTransformer(new Transformer(), true);
        inst.retransformClasses(TestClass.class);
        System.out.println("premain done");
    }

}

Transformer的實(shí)現(xiàn)如下(參考代碼):


class Transformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader l, String className, 
       Class<?> c, ProtectionDomain pd, byte[] b)  {
        ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        className = className.replace('/', '.');

         if (!isNeedLogExecuteInfo(className)) {
            return byteCode;
        }

        if (null == loader) {
            loader = Thread.currentThread().getContextClassLoader();
        }

        byteCode = aopLog(loader, className, byteCode);
        return byteCode;
    }
}

 private byte[] aopLog(ClassLoader loader, String className, byte[] byteCode) {
        try {
            ClassPool cp = ClassPool.getDefault();
            CtClass cc;
            try {
                cc = cp.get(className);
            } catch (NotFoundException e) {
                cp.insertClassPath(new LoaderClassPath(loader));
                cc = cp.get(className);
            }
            byteCode = aopLog(cc, className, byteCode);
        } catch (Exception ex) {
            System.err.println(ex);
        }
        return byteCode;
    }

    private byte[] aopLog(CtClass cc, String className, byte[] byteCode) throws CannotCompileException, IOException {
        if (null == cc) {
            return byteCode;
        }
        if (!cc.isInterface()) {
            CtMethod[] methods = cc.getDeclaredMethods();
            if (null != methods && methods.length > 0) {
                boolean isOpenPojoMonitor = ConfigUtils.isOpenPojoMonitor();
                Set<String> getSetMethods = Collections.emptySet();
                if (!isOpenPojoMonitor) {
                    getSetMethods = PojoDetector.getPojoMethodNames(methods);
                }
                for (CtMethod m : methods) {
                    if (isOpenPojoMonitor || !getSetMethods.contains(m.getName())) {
                        aopLog(className, m);
                    }
                }
                byteCode = cc.toBytecode();
            }
        }
        cc.detach();
        return byteCode;
    }

    private void aopLog(String className, CtMethod m) throws CannotCompileException {
        if (null == m || m.isEmpty()) {
            return;
        }
        boolean isMethodStatic = Modifier.isStatic(m.getModifiers());
        String aopClassName = isMethodStatic ? "\"" + className + "\"" : "this.getClass().getName()";
        final String timeMethodStr
            = ConfigUtils.isUsingNanoTime() ? "java.lang.System.nanoTime()" : "java.lang.System.currentTimeMillis()";

        // 避免變量名重復(fù)
        m.addLocalVariable("dingjsh_javaagent_elapsedTime", CtClass.longType);
        m.insertBefore("dingjsh_javaagent_elapsedTime = " + timeMethodStr + ";");
        m.insertAfter("dingjsh_javaagent_elapsedTime = " + timeMethodStr + " - dingjsh_javaagent_elapsedTime;");
        m.insertAfter(LOG_UTILS + ".log(" + aopClassName + ",\"" + m.getName()
            + "\",(long)dingjsh_javaagent_elapsedTime" + ");");
    }

如何實(shí)現(xiàn)精確配置監(jiān)聽

細(xì)心的讀者可能會發(fā)現(xiàn),雖然上面的方式采用排除法排除了不需要監(jiān)控的類卿闹,但是監(jiān)控范圍還是過廣揭糕,同時(shí),可以看到最后的log實(shí)在是太簡單锻霎,無法實(shí)現(xiàn)一些復(fù)雜的邏輯著角,不具備很好的擴(kuò)展性,那么有沒有更好的方式呢旋恼?

這個(gè)patroller 項(xiàng)目采用精確匹配的方式進(jìn)行監(jiān)控類擴(kuò)展雇寇,配置采用yml配置方式,配置如下:


#基礎(chǔ)的插件包配置蚌铜,會優(yōu)先加載
basePlugins:
  pre-agent-common:
    jarName: pre-agent-common
  pre-zipkin-sdk:
    jarName: pre-zipkin-sdk
#第三方插件包配置
plugins:
  pre-zipkin-plugin:
    jarName: pre-zipkin-plugin
    interceptorNames:
      - com.preapm.agent.plugin.interceptor.ZipkinInterceptor
    loadPatterns:
      - com.preapm.agent.constant.BaseConstants
      

---
#AOP切面配置,類似sparing aop配置
patterns:
  #==========================================================一下是具體業(yè)務(wù)配置根據(jù)具體項(xiàng)目路徑修改================================     
      
  #業(yè)務(wù)service配置    
  pre-service:
    patterns:
      - com.*.service.impl.*
    excludedPatterns:
    includedPatterns:
      #- key: .*(run\(\)|call\(\))
      #  interceptors:
      #     - com.preapm.agent.plugin.interceptor.JdkThreadInterceptor
      - key: .*
        interceptors:
           - com.preapm.agent.plugin.interceptor.ZipkinInterceptor
    interceptors:
      - com.preapm.agent.plugin.interceptor.ZipkinInterceptor
    track:
           inParam: true #記錄入?yún)?           outParam: true #記錄出參
           time: -1    #不設(shè)置時(shí)間限制   
           serialize: fastjson
    consMethod: false                 
  #agent測試配置    
  test:
      #插件織入匹配方法路徑嫩海,支持正則表達(dá)式
      patterns:
        - com.preapm.agent.Bootstrap
      #排除匹配路徑
      excludedPatterns:  
       - key: com.preapm.agent.Bootstrap\(\)
      includedPatterns:
      - key:  .*
      interceptors:
        - com.preapm.agent.plugin.interceptor.ZipkinInterceptor
      track:
           inParam: false
           outParam: true
           time: -1 

同時(shí)在AOP實(shí)現(xiàn)方面具備很好的擴(kuò)展性冬殃,主要看這個(gè)類

com.preapm.agent.weave.ClassWrapper

代碼如下:


public String beginSrc(ClassLoader classLoader,byte[] classfileBuffer,CtClass ctClass, CtMethod ctMethod) {
        String methodName = ctMethod.getName();
        List<String> paramNameList = Arrays.asList(ReflectMethodUtil.getMethodParamNames(classLoader,classfileBuffer,ctClass, ctMethod));
        try {
             //System.out.println("方法名稱:"+methodName+" 參數(shù)類型大小:"+ctMethod.getParameterTypes().length+" paramNameList:"+paramNameList.toArray());
            
              String template = ctMethod.getReturnType().getName().equals("void")
                ?
                "{\n" +
                "    %s        \n" +  beforAgent(methodName,paramNameList)+" \n"+
                "    try {\n" + 
                "        %s$agent($$);\n" +
                "    } catch (Throwable e) {\n" +
                "        %s\n" +doError(BaseConstants.THROWABLE_NAME_STR)+
                "        throw e;\n" +
                "    }finally{\n" +
                "        %s\n" + afterAgent(null)+" \n"+
                "    }\n" +
                "}"
                :
                "{\n" +
                "    %s        \n" +
                "    Object result=null;\n" +beforAgent(methodName,paramNameList)+" \n"+
                "    try {\n" +
                "        result=($w)%s$agent($$);\n" +
                "    } catch (Throwable e) {\n" +
                "        %s            \n" +doError(BaseConstants.THROWABLE_NAME_STR)+
                "        throw e;\n" +
                "    }finally{\n" +
                "        %s        \n" + afterAgent(BaseConstants.RESULT_NAME_STR)+" \n"+
                "    }\n" +
                "    return ($r) result;\n" +
                "}";

            String insertBeginSrc = this.beginSrc == null ? "" : this.beginSrc;
            String insertErrorSrc = this.errorSrc == null ? "" : this.errorSrc;
            String insertEndSrc = this.endSrc == null ? "" : this.endSrc;
            String result = String.format(template,
                    new Object[] { insertBeginSrc, ctMethod.getName(), insertErrorSrc, insertEndSrc });
            //log.info("result:"+result);
            return result;
        } catch (NotFoundException localNotFoundException) {
            log.severe(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(localNotFoundException));
            throw new RuntimeException(localNotFoundException);
        }
    }

最終實(shí)現(xiàn)是通過如下代碼

package com.preapm.agent.weave.impl.ClassWrapperAroundInterceptor

class ClassWrapperAroundInterceptor
{
    //... 其他邏輯

    public String beforAgent(String methodName, List<String> argNameList) {

            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("com.preapm.agent.common.bean.MethodInfo preMethondInfo = new com.preapm.agent.common.bean.MethodInfo();").append(line());
            //stringBuilder.append("com.preapm.agent.common.bean.MethodInfo preMethondInfo = com.preapm.agent.common.context.AroundInterceptorContext.loader(Thread.currentThread().getContextClassLoader());").append(line());
            stringBuilder.append("preMethondInfo.setTarget(this);").append(line());
            stringBuilder.append("preMethondInfo.setMethodName(" + toStr(methodName) + ");").append(line());
            if (argNameList != null && argNameList.size() != 0) {
                stringBuilder.append("preMethondInfo.setArgs($args);").append(line());
                stringBuilder.append("String preMethodArgsStr = ").append(toStr(StringUtils.join(argNameList, ",")))
                        .append(";").append(line());
                stringBuilder.append("preMethondInfo.setArgsName(preMethodArgsStr.split(" + toStr(",") + "));")
                        .append(line());
            }
            setSerialize(stringBuilder);
            setPlugin(stringBuilder);
            setTrack(stringBuilder);

            stringBuilder.append("com.preapm.agent.common.context.AroundInterceptorContext.start(Thread.currentThread().getContextClassLoader(),preMethondInfo);")
                    .append(line());
            return stringBuilder.toString();

    }
}

最后我們看看AroundInterceptorContext

public static void start(ClassLoader classLoader,MethodInfo methodInfo, String... names) {
    for (AroundInterceptor i : get(classLoader,names)) {
        i.before(methodInfo);
    }
}

可以看到叁怪,最終是通過遍歷我們配置的AroundInterceptor接口來實(shí)現(xiàn)擴(kuò)展的功能

終極方案

大家可以發(fā)現(xiàn)以上方案已經(jīng)是非常好了审葬,我們可以通過精確配置需要監(jiān)控的類,同時(shí)可以通過擴(kuò)展將監(jiān)控?cái)?shù)據(jù)使用擴(kuò)展的方式存儲到任何地方,但是這種方案也有其局限性涣觉,首先AroundInterceptor的邏輯實(shí)現(xiàn)可能非常復(fù)雜痴荐,特別是對于一些復(fù)雜調(diào)用鏈的情況,其次這種上報(bào)方式不是很好調(diào)度官册,容易引發(fā)性能問題生兆,同時(shí)對于一些復(fù)雜的采樣,合計(jì)膝宁,匯總等監(jiān)控方式還是需要實(shí)現(xiàn)復(fù)雜的代碼才能實(shí)現(xiàn)鸦难,那么有沒有更好的方案呢?

答案是當(dāng)然有员淫,我推薦skywalking合蔽,如果需要更進(jìn)一步的學(xué)習(xí),可以參考github或者官方文檔介返,原理和上面所講的是類似的拴事,但是上面只是講了客戶的采集,對于服務(wù)端的實(shí)現(xiàn)還需要讀者進(jìn)一步挖掘圣蝎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刃宵,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捅彻,更是在濱河造成了極大的恐慌组去,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件步淹,死亡現(xiàn)場離奇詭異从隆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)缭裆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門键闺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人澈驼,你說我怎么就攤上這事辛燥。” “怎么了缝其?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵挎塌,是天一觀的道長。 經(jīng)常有香客問我内边,道長榴都,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任漠其,我火速辦了婚禮嘴高,結(jié)果婚禮上竿音,老公的妹妹穿的比我還像新娘。我一直安慰自己拴驮,他們只是感情好春瞬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著套啤,像睡著了一般宽气。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纲岭,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天抹竹,我揣著相機(jī)與錄音,去河邊找鬼止潮。 笑死窃判,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喇闸。 我是一名探鬼主播袄琳,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼燃乍!你這毒婦竟也來了唆樊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤刻蟹,失蹤者是張志新(化名)和其女友劉穎逗旁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舆瘪,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡片效,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了英古。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淀衣。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖召调,靈堂內(nèi)的尸體忽然破棺而出膨桥,到底是詐尸還是另有隱情,我是刑警寧澤唠叛,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布只嚣,位于F島的核電站荷愕,受9級特大地震影響全肮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奋岁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一澳厢、第九天 我趴在偏房一處隱蔽的房頂上張望环础。 院中可真熱鬧,春花似錦剩拢、人聲如沸线得。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贯钩。三九已至,卻和暖如春办素,著一層夾襖步出監(jiān)牢的瞬間角雷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工性穿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勺三,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓需曾,卻偏偏與公主長得像吗坚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子呆万,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

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