java 探針

關(guān)于java agent這里只是做一個(gè) 簡(jiǎn)單的介紹,因?yàn)樵敿?xì)的介紹官網(wǎng)上有很多地址:https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html,為了節(jié)省大家的時(shí)間。所以重點(diǎn)介紹應(yīng)用場(chǎng)景已經(jīng)應(yīng)用方式。

案例:對(duì)一個(gè)應(yīng)用程序的指定方法的調(diào)用增加耗時(shí)監(jiān)控(在不修改原來應(yīng)用代碼的情況下)

premain方式

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

premain 顧名思義是在需要被代理的應(yīng)用main方法執(zhí)行前執(zhí)行。但是個(gè)人認(rèn)為這種方式的局限性太大了。如果需要對(duì)一個(gè)應(yīng)用進(jìn)行處理,需要停止應(yīng)用。這在生產(chǎn)環(huán)境中危險(xiǎn)是很大的。實(shí)用場(chǎng)景較少,所以本文不會(huì)重點(diǎn)對(duì)它進(jìn)行說明。但是也會(huì)貼上一個(gè)簡(jiǎn)單的應(yīng)用的實(shí)現(xiàn)代碼。因?yàn)榭酉鄬?duì)于另一種方式較少。所以只貼代碼不進(jìn)行詳細(xì)說明了。

agentTest工程:


image.png

MyTest:code

public class MyTest {

    public static void main(String[] args) {
        //MyTest myTest = new MyTest();

        sayHello();
        sayHello2("hello world11");


    }

    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();
        }
    }
}

MANIFEST.MF

Manifest-Version: 1.0
Main-Class: test.demo.MyTest

javaagent4工程


image.png

AgentDemo

public class AgentDemo {

    /**
     * 該方法在main方法之前運(yùn)行,與main方法運(yùn)行在同一個(gè)JVM中
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("=========premain方法執(zhí)行1========");
        System.out.println(agentArgs);
        // 添加Transformer
        inst.addTransformer(new MyTransformer());
    }

    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst)
     * 則會(huì)執(zhí)行 premain(String agentArgs)
     *
     */
    public static void premain(String agentArgs) {
        System.out.println("=========premain方法執(zhí)行2========");
        System.out.println(agentArgs);

    }

}

MyTransformer
```java
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>> classMapName = new ConcurrentHashMap<>();


    public MyTransformer(){
        add("test.demo.MyTest.sayHello");
        add("test.demo.MyTest.sayHello2");
    }

    private  void  add(String className){
        String classNameStr = className.substring(0,className.lastIndexOf("."));
        String methodName = className.substring(className.lastIndexOf(".")+1);
        List<String> lists = classMapName.get(classNameStr);
        if(null == lists){
            lists = new ArrayList<>();
            classMapName.put(classNameStr,lists);
        }
        lists.add(methodName);
    }
    
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 該路徑顯示方式
        className = className.replace("/",".");
        // 判斷傳入的類路徑是否在監(jiān)控中
        if( classMapName.containsKey(className)) {
            CtClass ctclass = null;
            try{
                // 根據(jù)類全名獲取字節(jié)碼類信息
                ctclass = ClassPool.getDefault().get(className);
                for (String methodName : classMapName.get(className)) {
                    String outputStr = "\nSystem.out.println(\"this method " + methodName
                            + " cost:\" +(endTime - startTime) +\"ms.\");";
                    System.out.println(outputStr);
                    // 根據(jù)方法名得到這方法實(shí)例
                    CtMethod ctMethod = ctclass.getDeclaredMethod(methodName);
                    // 新定義一個(gè)方法叫做比如sayHello$old
                    String newMethodName = methodName + "$old";
                    // 將原來的方法名字修改
                    ctMethod.setName(newMethodName);
                    // 創(chuàng)建新的方法,復(fù)制原來的方法,名字為原來的名字
                    CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);
                    // 構(gòu)建新的方法體
                    StringBuilder bodyStr = new StringBuilder();
                    bodyStr.append("{");
                    bodyStr.append(prefix);
                    // 調(diào)用原有代碼,類似于method();($$)表示所有的參數(shù)
                    bodyStr.append(newMethodName + "($$);\n");
                    bodyStr.append(postfix);
                    bodyStr.append(outputStr);
                    bodyStr.append("}");
                    // 替換新方法
                    newMethod.setBody(bodyStr.toString());
                    // 增加新方法
                    ctclass.addMethod(newMethod);
                }

                return ctclass.toBytecode();

            }catch (Exception e){
                System.out.println(e.getMessage());
                
            }
        }
        return null;
    }
}

MANIFEST.MF

Manifest-Version: 1.0
Created-By: 0.0.1 (Demo Inc.)
Premain-Class: agent.AgentDemo  

Premain-Class:指定步驟 1 當(dāng)中編寫的那個(gè)帶有 premain 的 Java 類
用如下方式運(yùn)行帶有 Instrumentation 的 Java 程序:

java -javaagent:jar 文件的位置 [= 傳入 premain 的參數(shù) ]

agentmain方式

優(yōu)勢(shì):premain是靜態(tài)修改,在類加載之前修改; attach是動(dòng)態(tài)修改,在類加載后修改要使premain生效重啟應(yīng)用,而attach不重啟應(yīng)用即可修改字節(jié)碼并讓其重新加載。
和premain類似 agentmain也有兩個(gè)類似的方法

public static void agentmain (String agentArgs, Instrumentation inst);     //     [1] 
public static void agentmain (String agentArgs);         //   [2]
//[1] 的優(yōu)先級(jí)比 [2] 高,將會(huì)被優(yōu)先執(zhí)行

agentmain 與 premain 不同在于agentmain需要在 main 函數(shù)開始運(yùn)行后才啟動(dòng),既然是要在main函數(shù)開始運(yùn)行后才啟動(dòng),那他的啟動(dòng)時(shí)機(jī)如何確定,這就需要引出一個(gè)概念 Java SE 6 當(dāng)中提供的 Attach API。
Attach API 很簡(jiǎn)單,只有 2 個(gè)主要的類,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一個(gè) Java 虛擬機(jī),也就是程序需要監(jiān)控的目標(biāo)虛擬機(jī),提供了 JVM 枚舉,Attach 動(dòng)作和 Detach 動(dòng)作(Attach 動(dòng)作的相反行為,從 JVM 上面解除一個(gè)代理)等等 ; VirtualMachineDescriptor 則是一個(gè)描述虛擬機(jī)的容器類,配合 VirtualMachine 類完成各種功能。整個(gè)過程其實(shí)和premain方式類似,主要的區(qū)別在于執(zhí)行時(shí)機(jī)的不同。
先貼代和效果圖,最后在來說在實(shí)現(xiàn)過程中遇到的坑,以及解決方案。
兩個(gè)應(yīng)用的結(jié)構(gòu)非常簡(jiǎn)單,因?yàn)橹攸c(diǎn)不是這里所以隨意了些


image.png

MyApplication

public class MyApplication {

    private static Logger logger = LogManager.getLogger(MyApplication.class);


    public void run() throws  Exception{
        logger.info("run 運(yùn)行...");
        Run run = new Run();
        for(;;){
            run.run();
        }
    }

}

Launcher

public class Launcher {
    // 主函數(shù)
    public static void main(String[] args) throws Exception {
        MyApplication myApplication = new MyApplication();
        myApplication.run();
    }
}

Run

public class Run {
    
    private static final Logger logger = LogManager.getLogger(Run.class);

    public void run() throws InterruptedException{
        long sleep = (long)(Math.random() * 1000 + 200);
        Thread.sleep(sleep);
        logger.info("run in [{}] millis!", sleep);
    }
}

MANIFEST.MF

Main-Class: com.demo.application.Launcher

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demo</groupId>
    <artifactId>agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <finalName>myAgent</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <!--避免MANIFEST.MF被覆蓋-->
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                    <descriptorRefs>
                        <!--打包時(shí)加入依賴-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <!-- Project dependencies -->
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>
    </dependencies>

</project>
##打包命令
mvn clean package
##執(zhí)行命令
java   -jar myAgent-jar-with-dependencies.jar

這個(gè)工程就是作為我們?cè)谏a(chǎn)上運(yùn)行的應(yīng)用實(shí)例,雖然不會(huì)這么簡(jiǎn)單。這里沒有什么問題。我們甚至可以用springboot構(gòu)建,只是表現(xiàn)形式不同而已。接下來重點(diǎn)來了

image.png

先貼代碼:
Launcher

public class Launcher {

    private static Logger logger = LogManager.getLogger(Launcher.class);


    public static void main(String[] args) {
        //指定jar路徑
        String agentFilePath = "myAcctach-jar-with-dependencies.jar";

        //需要attach的進(jìn)程標(biāo)識(shí)
        String applicationName = "myAgent";

        //查到需要監(jiān)控的進(jìn)程
        Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
                .stream()
                .filter(jvm -> {
                    logger.info("jvm:{}", jvm.displayName());
                    return jvm.displayName().contains(applicationName);
                })
                .findFirst().get().id());

        if(!jvmProcessOpt.isPresent()) {
            logger.error("Target Application not found");
            return;
        }
        File agentFile = new File(agentFilePath);
        try {
            String jvmPid = jvmProcessOpt.get();
            logger.info("Attaching to target JVM with PID: " + jvmPid);
            VirtualMachine jvm = VirtualMachine.attach(jvmPid);
            jvm.loadAgent(agentFile.getAbsolutePath());
            jvm.detach();
            logger.info("Attached to target JVM and loaded Java agent successfully");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

MyInstrumentationAgent

public class MyInstrumentationAgent {

    private static Logger logger = LogManager.getLogger(MyInstrumentationAgent.class);

    public static void agentmain(String agentArgs, Instrumentation inst) {
        logger.info("[Agent] In agentmain method");

        //需要監(jiān)控的類
        String className = "com.demo.application.Run";
        transformClass(className, inst);
    }

    private static void transformClass(String className, Instrumentation instrumentation) {
        Class<?> targetCls = null;
        ClassLoader targetClassLoader = null;
        // see if we can get the class using forName
        try {
            targetCls = Class.forName(className);
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            return;
        } catch (Exception ex) {
            logger.error("Class [{}] not found with Class.forName");
        }
        // otherwise iterate all loaded classes and find what we want
        for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
            if(clazz.getName().equals(className)) {
                targetCls = clazz;
                targetClassLoader = targetCls.getClassLoader();
                transform(targetCls, targetClassLoader, instrumentation);
                return;
            }
        }
        throw new RuntimeException("Failed to find class [" + className + "]");
    }

    private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
        MyTransformer dt = new MyTransformer(clazz.getName(), classLoader);
        instrumentation.addTransformer(dt, true);
        try {
            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
        }
    }
}

MyTransformer

public class MyTransformer implements ClassFileTransformer {

    private static Logger logger = LogManager.getLogger(MyTransformer.class);

    //需要監(jiān)控的方法
    private static final String WITHDRAW_MONEY_METHOD = "run";

    /** The internal form class name of the class to transform */
    private String targetClassName;
    /** The class loader of the class we want to transform */
    private ClassLoader targetClassLoader;

    public MyTransformer(String targetClassName, ClassLoader targetClassLoader) {
        this.targetClassName = targetClassName;
        this.targetClassLoader = targetClassLoader;
    }



    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); //replace . with /
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
            logger.info("[Agent] Transforming class" + className);
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);

                // 開始時(shí)間
                m.addLocalVariable("startTime", CtClass.longType);
                m.insertBefore("startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                // 結(jié)束時(shí)間
                m.addLocalVariable("endTime", CtClass.longType);
                endBlock.append("endTime = System.currentTimeMillis();");

                // 時(shí)間差
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append("opTime = endTime-startTime;");

                // 打印方法耗時(shí)
                endBlock.append("logger.info(\"completed in:\" + opTime + \" millis!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Exception e) {
                logger.error("Exception", e);
            }
        }
        return byteCode;
    }
}

MANIFEST.MF

Main-Class: com.acttach.agent.Launcher
Agent-Class: com.acttach.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Permissions: all-permissions

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.acttch</groupId>
    <artifactId>acttch</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <finalName>myAcctach</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>

                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <!--避免MANIFEST.MF被覆蓋-->
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                    <descriptorRefs>
                        <!--打包時(shí)加入依賴-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

    </build>

    <!-- Project dependencies -->
    <dependencies>
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>

        </dependency>


        <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.1-GA</version>
        </dependency>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>

    </dependencies>

</project>
##打包命令
mvn clean package
##執(zhí)行命令
java -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar

坑點(diǎn)1:
在打包完執(zhí)行jar包時(shí),最開始我是直接用
java -jar myAcctach-jar-with-dependencies.jar
出現(xiàn)了下面的錯(cuò)誤

D:\litter\acttch\target>java  -jar myAcctach-jar-with-dependencies.jar
Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/tools/attach/VirtualMachine
        at com.acttach.agent.Launcher.main(Launcher.java:31)
Caused by: java.lang.ClassNotFoundException: com.sun.tools.attach.VirtualMachine
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 1 more

tools.jar因?yàn)槭莏re環(huán)境中的本地包,所以我們?cè)诖蛲臧螅瑢?shí)際上這個(gè)jar包是沒有被打進(jìn)去的。所以在執(zhí)行的時(shí)候要指定-Djava.ext.dirs 在網(wǎng)上找了很多文章他們都是這樣寫的
-Djava.ext.dirs=${JAVA_HOME}\lib -jar 說對(duì)于linux windows都可以。我也不知道他們有沒有驗(yàn)證,反正這種方式在windows上行不通的。 我的windows上 只有 ava -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar 這樣才行,至于為什么需要在%JAVA_HOME%\lib外層加上引號(hào),因?yàn)槲业膉dk路徑是在C:\Program Files 大家發(fā)現(xiàn)沒有中間有一個(gè)空格,如果你不加引號(hào)當(dāng)做一個(gè)整體,windows下會(huì)給你切分。


image.png

前面那一部分沒有了。

坑2:

VirtualMachine jvm = VirtualMachine.attach(jvmPid);
// 要注意這里是加載自身的jar進(jìn)去 來對(duì)需要代理的應(yīng)用進(jìn)行處理。這里不要弄混了。
// 本人就是在這個(gè)地方被磨了很久,一直報(bào)找不到j(luò)ar.....~~~~(>_<)~~~~
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
image.png

上面的截圖是隨機(jī)休眠一段時(shí)間并打印睡眠時(shí)間的方法

public class Run {


    private static final Logger logger = LogManager.getLogger(Run.class);

    public void run() throws InterruptedException{
        long sleep = (long)(Math.random() * 1000 + 200);
        Thread.sleep(sleep);
        logger.info("run in [{}] millis!", sleep);
    }
}

現(xiàn)在有一個(gè)需求在不改原來的代碼基礎(chǔ)上增加監(jiān)控統(tǒng)計(jì)開始結(jié)束時(shí)間


image.png

這是在執(zhí)行了另外一個(gè)應(yīng)用之后產(chǎn)生的效果。

需要注意的地方基本上就是上面這幾個(gè)了。其實(shí)仔細(xì)想想這個(gè)技術(shù)還是挺有應(yīng)用場(chǎng)景的。有興趣的不妨去學(xué)學(xué)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容