Java探針-Java代理技術

原文:
Java探針-Java Agent技術
利用JAVA探針分析復雜代碼運維實踐


總結:

使用java代理來實現java字節(jié)碼注入
使用JavaSsist可以對字節(jié)碼進行修改
使用ASM可以修改字節(jié)碼

使用Java代理和ASM字節(jié)碼技術開發(fā)java探針工具可以修改字節(jié)碼

備注:javassist是一個庫,實現ClassFileTransformer接口中的transform()方法。ClassFileTransformer 這個接口的目的就是在class被裝載到JVM之前將class字節(jié)碼轉換掉,從而達到動態(tài)注入代碼的目的。

備注:ASM是一個java字節(jié)碼操縱框架,它能被用來動態(tài)生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。Java class 被存儲在嚴格格式定義的 .class文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。


詳細總結

1 使用Java代理和Java字節(jié)碼注入技術

開發(fā)java探針工具來分析復雜的接口方法,無需修改代碼,簡單部署就可以實時抓取代碼的運行軌跡和方法耗時。

2 基于Java代理和Java字節(jié)碼注入技術的java探針工具技術原理

動態(tài)代理功能實現說明

我們利用Java代理和ASM字節(jié)碼技術開發(fā)java探針工具,實現原理如下:

jdk1.5以后引入了Java代理技術,Java代理是運行方法之前的攔截器。我們利用Java代理和ASM字節(jié)碼技術,在JVM加載class二進制文件的時候,利用ASM動態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計時器功能,用于計算監(jiān)控方法耗時,同時將方法耗時及內部調用情況放入處理器,處理器利用棧先進后出的特點對方法調用先后順序做處理,當一個請求處理結束后,將耗時方法軌跡和入參map輸出到文件中,然后根據map中相應參數或耗時方法軌跡中的關鍵代碼區(qū)分出我們要抓取的耗時業(yè)務。最后將相應耗時軌跡文件取下來,轉化為xml格式并進行解析,通過瀏覽器將代碼分層結構展示出來,方便耗時分析,如圖所示:

java探針工具原理圖

1:在JVM加載class二進制文件的時候,利用ASM動態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計時器功能,用于計算監(jiān)控方法耗時;
2:將監(jiān)控的相關方法 和 耗時及內部調用情況,按照順序放入處理器;
3:處理器利用棧先進后出的特點對方法調用先后順序做處理,當一個請求處理結束后,將耗時方法軌跡入參map輸出到文件中;
4:然后區(qū)分出耗時的業(yè)務,轉化為xml格式進行解析和分析。

Java探針工具功能點:

1、支持方法執(zhí)行耗時范圍抓取設置,根據耗時范圍抓取系統(tǒng)運行時出現在設置耗時范圍的代碼運行軌跡。

2、支持抓取特定的代碼配置,方便對配置的特定方法進行抓取,過濾出關系的代碼執(zhí)行耗時情況。

3、支持APP層入口方法過濾,配置入口運行前的方法進行監(jiān)控,相當于監(jiān)控特有的方法耗時,進行方法專題分析。

4、支持入口方法參數輸出功能,方便跟蹤耗時高的時候對應的入參數。

5、提供WEB頁面展示接口耗時展示、代碼調用關系圖展示、方法耗時百分比展示、可疑方法凸顯功能。


2.1 Java代理小例子

參考:基于 JavaAgent 的應用實例

JavaAgent 是運行在 main方法之前的攔截器,它內定的方法名叫 premain ,也就是說先執(zhí)行 premain 方法然后再執(zhí)行 main 方法。
那么如何實現一個 Java代理呢?很簡單,只需要增加 premain 方法即可。

源碼:JavaAgent

目錄結構

Test1.pre_MyProgram.java

package agent;

import java.lang.instrument.Instrumentation;

public class pre_MyProgram
{
    /**
     * 該方法在main方法之前運行,與main方法運行在同一個JVM中 并被同一個System ClassLoader裝載
     * 被統(tǒng)一的安全策略(security policy)和上下文(context)管理
     */
    public static void premain(String agentOps, Instrumentation inst)
    {

        System.out.println("====premain 方法執(zhí)行");
        System.out.println(agentOps);

    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst) 則會執(zhí)行 premain(String
     * agentOps)
     */
    public static void premain(String agentOps)
    {

        System.out.println("====premain方法執(zhí)行2====");
        System.out.println(agentOps);
    }

    public static void main(String[] args)
    {
    }

}

在 src 目錄下添加 META-INF/MANIFEST.MF 文件

Manifest-Version: 1.0
Premain-Class: agent.pre_MyProgram
Can-Redefine-Classes: true

打包代碼為 pre_MyProgram.jar;注意打包的時候選擇我們自己定義的 MANIFEST.MF

Test2.MyProgram.java

package alibaba;

public class MyProgram
{
    public static void main(String[] args)
    {
        System.out.println("====MyProgram====");
    }
}
Manifest-Version: 1.0
Main-Class: alibaba.MyProgram

導出main的jar包命名為:MyProgram.jar


結果

命令中的Hello1為我們傳遞給 premain 方法的字符串參數,這就是一個簡單的Java代理

2.2 基于 JavaAgent 的應用實例

JDK5中只能通過命令行參數在啟動JVM時指定javaagent參數來設置代理類,而JDK6中已經不僅限于在啟動JVM時通過配置參數來設置代理類,JDK6中通過 Java Tool API 中的 attach 方式,我們也可以很方便地在運行過程中動態(tài)地設置加載代理類,以達到 instrumentation 的目的。

Instrumentation 的最大作用,就是類定義動態(tài)改變和操作。

最簡單的一個例子,計算某個方法執(zhí)行需要的時間,不修改源代碼的方式,使用Instrumentation 代理來實現這個功能,給力的說,這種方式相當于在JVM級別做了AOP支持,這樣我們可以在不修改應用程序的基礎上就做到了AOP,是不是顯得略吊。

創(chuàng)建一個 ClassFileTransformer 接口的實現類 MyTransformer實現 ClassFileTransformer 這個接口的目的就是在class被裝載到JVM之前將class字節(jié)碼轉換掉,從而達到動態(tài)注入代碼的目的。那么首先要了解MonitorTransformer 這個類的目的,就是對想要修改的類做一次轉換,這個用到了javassist對字節(jié)碼進行修改,可以暫時不用關心jaavssist的原理,用ASM同樣可以修改字節(jié)碼,只不過比較麻煩些。

項目結構

源碼:JavaAgent

MyAgent.MyAgent.java

package agent;

import java.lang.instrument.Instrumentation;

public class MyAgent

{
    /**
     * 該方法在main方法之前運行,與main方法運行在同一個JVM中 并被同一個System ClassLoader裝載
     * 被統(tǒng)一的安全策略(security policy)和上下文(context)管理
     */
    public static void premain(String agentOps, Instrumentation inst)
    {
        System.out.println("=========premain方法執(zhí)行========");
        System.out.println(agentOps);
        // 添加Transformer
        inst.addTransformer(new MyTransformer());

    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst) 則會執(zhí)行 premain(String
     * agentOps)
     */
    public static void premain(String agentOps)
    {

        System.out.println("====premain方法執(zhí)行2====");
        System.out.println(agentOps);
    }

    public static void main(String[] args)
    {

    }

}

MyAgent.MyTransformer

package agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
/**
 * 檢測方法的執(zhí)行時間
 */
public class MyTransformer implements ClassFileTransformer
{

    final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
    final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";

    // 被處理的方法列表
    final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();

    public MyTransformer()
    {
        add("com.shanhy.demo.TimeTest.sayHello");
        add("com.shanhy.demo.TimeTest.sayHello2");
    }

    private void add(String methodString)
    {
        String className = methodString.substring(0, methodString.lastIndexOf("."));
        String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
        List<String> list = methodMap.get(className);
        if (list == null)
        {
            list = new ArrayList<String>();
            methodMap.put(className, list);
        }
        list.add(methodName);
    }

    // 重寫此方法
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
    {
        className = className.replace("/", ".");
        if (methodMap.containsKey(className))
        {
            // 判斷加載的class的包路徑是不是需要監(jiān)控的類
            CtClass ctclass = null;
            try
            {
                ctclass = ClassPool.getDefault().get(className);// 使用全稱,用于取得字節(jié)碼類<使用javassist>
                for (String methodName : methodMap.get(className))
                {
                    String outputStr = "\nSystem.out.println(\"this method " + methodName
                            + " cost:\" +(endTime - startTime) +\"ms.\");";

                    CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 得到這方法實例
                    String newMethodName = methodName + "$old";// 新定義一個方法叫做比如sayHello$old
                    ctmethod.setName(newMethodName);// 將原來的方法名字修改

                    // 創(chuàng)建新的方法,復制原來的方法,名字為原來的名字
                    CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);

                    // 構建新的方法體
                    StringBuilder bodyStr = new StringBuilder();
                    bodyStr.append("{");
                    bodyStr.append(prefix);
                    bodyStr.append(newMethodName + "($$);\n");// 調用原有代碼,類似于method();($$)表示所有的參數
                    bodyStr.append(postfix);
                    bodyStr.append(outputStr);
                    bodyStr.append("}");

                    newMethod.setBody(bodyStr.toString());// 替換新方法
                    ctclass.addMethod(newMethod);// 增加新方法
                    System.err.println(outputStr);
                }
                return ctclass.toBytecode();
            }
            catch (Exception e)
            {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
        }
        return null;
    }
}

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist-3.12.1-GA.jar

package alibaba;

public class TimeTest
{

    public static void main(String[] args)
    {
        System.err.println("======TimeTest執(zhí)行========");
        sayHello();
        sayHello2("hello world222222222");
    }

    public static void sayHello()
    {
        try
        {
            Thread.sleep(2000);
            System.out.println("hello world!!");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public static void sayHello2(String hello)
    {
        try
        {
            Thread.sleep(1000);
            System.out.println(hello);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Main-Class: alibaba.TimeTest

結果:

結果

3 使用 spring-loaded 實現 jar 包熱部署

在項目開發(fā)中我們可以把一些重要但又可能會變更的邏輯封裝到某個 logic.jar 中,當我們需要隨時更新實現邏輯的時候,可以在不重啟服務的情況下讓修改后的 logic.jar 被重新加載生效。

spring-loaded是一個開源項目,項目地址:https://github.com/spring-projects/spring-loaded

使用方法:

在啟動主程序之前指定參數
-javaagent:C:/springloaded-1.2.5.RELEASE.jar -noverify
123

如果你想讓 Tomat 下面的應用自動熱部署,只需要在 catalina.sh 中添加:

set JAVA_OPTS=-javaagent:springloaded-1.2.5.RELEASE.jar -noverify1

這樣就完成了 spring-loaded 的安裝,它能夠自動檢測Tomcat 下部署的webapps ,在不重啟Tomcat的情況下,實現應用的熱部署。

通過使用 -noverify 參數,關閉 Java 字節(jié)碼的校驗功能。
使用參數 -Dspringloaded=verbose;explain;watchJars=tools.jar 指定監(jiān)視的jar (verbose;explain; 非必須),多個jar用“冒號”分隔,如 watchJars=tools.jar:utils.jar:commons.jar

當然,它也有一些小缺限:

  1. 目前官方提供的1.2.4 版本在linux上可以很好的運行,但在windows還存在bug,官網已經有人提出:https://github.com/spring-projects/spring-loaded/issues/145
  2. 對于一些第三方框架的注解的修改,不能自動加載,比如:spring mvc的@RequestMapping
  3. log4j的配置文件的修改不能即時生效。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 文章圖片上傳不正常,如需文檔,可聯系微信:1017429387 目錄 1 安裝... 4 1.1 配置探針... ...
    Mrhappy_a7eb閱讀 6,950評論 0 5
  • 一、基礎知識篇:Http Header之User-AgentUser Agent中文名為用戶代理,是Http協議中...
    iPhone閱讀 16,300評論 0 13
  • 昨天跟好朋友約出來聊天,談到大學這幾年,都開始感慨了。 大一入校,我們這些人都是一樣的起點,都是一樣的成長環(huán)境,可...
    太陽太大閱讀 704評論 5 2
  • 一位憂家憂己的小透明 一個高冷神經質的巨嬰 一朵敏感老大粗的柳絮 一只貪婪沒定性的鸚鵡 但是,我就是我。即使是顏色...
    讀木舟_momo閱讀 191評論 0 2
  • 姓名:劉衛(wèi)師: 公司:寧波大發(fā)化纖有限公司 《六項精進》289期反省二組紀律委員 【日精進打卡第18天】 【知~學...
    劉偉師閱讀 242評論 0 0

友情鏈接更多精彩內容