<返回更多

Java 探针技术原理及实践

2021-09-09    悠闲鲸鱼L
加入收藏

1 背景

近日在给公司同事分享Arthas 工具使用时候,被它强悍的功能震撼到了就好奇研究了下它的原理及底层实现,其实它是通过JAVA agent 来实现的,也就深入地学习了一下Java agent 技术觉得蛮有意思,也得到了一些启发,通过Java agent 我们可以做到很多意想不到的事情,在我们日常开发过程中你可能已经无形地接触到了它,例如一些APM 工具如:pinpoint,skywalking,cat, arthas,BTrace... 甚至我们的IDEA debug工具无时无刻都有java agent 的身影...如果你是一个Java 开发者,你一定很有必要去研究一下它。


2 Java agent 介绍

Java agent 又名Java 探针活Java 代理,也有人称它为 “插桩”,说的都是一个意思,只是大家给它起了一个比较通俗易懂的名字而已。“探针” 这个说法我感觉非常形象,JVM 一旦跑起来,对于外界来说,它就是一个黑盒子。而 Java Agent 可以像一支针一样插到 JVM 内部,探到我们想要的东西,并且可以注入东西进去。就像我们生病时去医院看医生,医生怎么诊断你身体的健康状况呢,这时候往往借助一个听诊器,把它放在你的胸口去听诊,就能大概了解你的健康状态了,怎么样,是不是很形象? 那又如何理解代理呢?比方说我们需要了解目标 JVM 的一些运行指标,我们可以通过 Java Agent 来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标 JVM ,但是我们是通过 Java Agent 来获取的,对于目标 JVM 来说,它就像是一个代理。

Java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。

2.1 java agent 技术的主要功能

java agent技术的主要功能如下:

  • 可以在加载java文件之前做拦截把字节码做修改
  • 可以在运行期将已经加载的类的字节码做变更
  • 还有其他的一些小众的功能如:

获取所有已经被加载过的类

获取所有已经被初始化过了的类

获取某个对象的大小

将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载

将某个jar加入到classpath里供AppClassloard去加载

2.2 java Instrumentation API

通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。

有两种方式拿到Instrumentation对象:

  1. jvm启动时指定agent,Instrumentation对象会通过agent的premain方法传递。它是Java 5 开始提供的方式。
  2. jvm启动后通过jvm提供的机制加载agent,Instrumentation对象会通过agent的agentmain方法传递。它是java6 开始提供的方式

Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:

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

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:

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

这两组方法的第一个参数AgentArgs是随同 “–javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。inst是Instrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。

 

3 两个小demo

下面我将用两种方式分别对premain 和 agentmain 两种方式介绍。

简单起见,我就对我的目标程序做一个耗时统计,在方法体前后声明两个变量并计算耗时。

3.1 premain 的方式

这里隐藏了一些公司的敏感的信息用“xxx” 代替

我的pom文件

<plugin>
   <groupId>org.Apache.maven.plugins</groupId>
   <artifactId>maven-jar-plugin</artifactId>
   <configuration>
      <archive>
         <manifest>
            <addClasspath>true</addClasspath>
         </manifest>
         <manifestEntries>
            <Premain-Class>com.xxx.xxx.xxx.PreMainAgent</Premain-Class>
         </manifestEntries>
      </archive>
   </configuration>
</plugin>
package com.xxx.xxx.capital;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * Created on 2021/9/5 10:07 下午. <br/>
 * Description: <br/>
 * description for class template.
 *
 * @author danniel.l
 */
public class PreMainAgent {

    private static Instrumentation instrumentation;
    public static void premain(String agentArgs, Instrumentation inst) {
        instrumentation = inst;
        System.err.println("我在main启动之前启动");

        inst.addTransformer(new MyTransformer());
    }

}
package com.xxx.xxx.capital;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

/**
 * Created on 2021/9/5 11:49 下午. <br/>
 * Description: <br/>
 * description for class template.
 *
 * @author danniel.l
 */
public class MyTransformer implements ClassFileTransformer {

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

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

        if (!className.startsWith("com/xxx/xxx/xxx/agenttest")) {
            return null;
        }

        className = className.replace("/", ".");
        CtClass ctclass = null;
        try {
            ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
            for(CtMethod ctMethod : ctclass.getDeclaredMethods()){
                String methodName = ctMethod.getName();
                String newMethodName = methodName + "$old";// 新定义一个方法叫做比如sayHello$old
                ctMethod.setName(newMethodName);// 将原来的方法名字修改

                // 创建新的方法,复制原来的方法,名字为原来的名字
                CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);

                // 构建新的方法体
                StringBuilder bodyStr = new StringBuilder();
                bodyStr.append("{");
                bodyStr.append("System.out.println("==============Enter Method: " + className + "." + methodName + " ==============");");
                bodyStr.append(prefix);
                bodyStr.append(newMethodName + "($$);n");// 调用原有代码,类似于method();($$)表示所有的参数
                bodyStr.append(postfix);
                bodyStr.append("System.out.println("==============Exit Method: " + className + "." + methodName + " Cost:" +(endTime - startTime) +"ms " + "===");");
                bodyStr.append("}");

                newMethod.setBody(bodyStr.toString());// 替换新方法
                ctclass.addMethod(newMethod);// 增加新方法
            }
            return ctclass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}


目标增强的类

package com.xxx.xxx.xxx.agenttest;

import org.springframework.web.bind.annotation.RestController;

/**
 * Created on 2021/9/6 12:20 上午. <br/>
 * Description: <br/>
 * description for class template.
 *
 * @author danniel.l
 */
@RestController
public class AgentTest {

    public void test1(){
        System.out.println("this is test1");
    }

    public void test2(){
        System.out.println("this is test2");
    }


}

 

启动时增加如下参数

-javaagent:/Users/user/IdeaProjects/bc/xxx-xxx/xxx-xxx-web/target/xxx-xxx-web-2.0.0-SNAPSHOT.jar

 

3.2 agentmain 的方式

3.2.1 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>org.example</groupId>
    <artifactId>agent-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.0-GA</version>
        </dependency>

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0</version>
            <scope>system</scope>
            <systemPath>/Library/Java/JavaVirtualmachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar</systemPath>
        </dependency>
    </dependencies>

    <build>
        <finalName>agentmain-test</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>agent.test.myagent.MyAgentTransformer</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.2.2

package agent.test.target;

/**
 * Created on 2021/9/7 3:24 下午. <br/>
 * Description: <br/>
 * 需要增强的目标应用程序入口
 *
 * @author danniel.l
 */
public class TargetMain {

    public static void main(String[] args) {
        TargetTest targetTest = new TargetTest();
        while (true) {
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
            targetTest.test();
            targetTest.method();
        }
    }

}

 

package agent.test.target;
/**
 * Created on 2021/9/7 3:25 下午. <br/>
 * Description: <br/>
 * 需要增强的目标应用程序类
 *
 * @author danniel.l
 */

public class TargetTest {

    public void test(){
        System.out.println("this is a test!");
    }

    public void method() {
        System.err.println("this is a method!");
    }
}

 

package agent.test.myagent;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

/**
 * Created on 2021/9/7 3:44 下午. <br/>
 * Description: <br/>
 * Agent 程序入口.
 *
 * @author danniel.l
 */
public class MyAgentMain {

    public static void main(String[] args) throws Exception {
        // 需要增强的目标程序的名称,可根据args 参数传递进来
        String targetApplicationName = "TargetMain";

        // 需要增强的目标类,可根据args 参数传递进来
        String targetClassName = "agent.test.target.TargetTest";

        // 获取本机已启动的应用程序名称集合列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            if (vmd.displayName().endsWith(targetApplicationName)) {
                // 通过VirtualMachine.attach() 附着上目标程序
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());

                // 把探针代理程序插桩到目标程序里去。。
                virtualMachine.loadAgent("/Users/user/IdeaProjects/test/agent-test/target/agentmain-test.jar", targetClassName);
                System.out.println("Attached target application successfully!");
                virtualMachine.detach();
            }
        }
    }

}

 

package agent.test.myagent;

import agent.test.target.TargetTest;
import javassist.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

/**
 * Created on 2021/9/5 10:30 下午. <br/>
 * Description: <br/>
 * Agent 增强逻辑处理.
 *
 * @author danniel.l
 */
public class MyAgentTransformer {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {
        System.out.println("agentmain starts...."+ agentArgs);
        instrumentation.addTransformer(new Transformer(agentArgs), true);

        // 允许修改TargetTest类
        instrumentation.retransformClasses(TargetTest.class);
    }

    private static class Transformer implements ClassFileTransformer {
        private final String targetClassName;

        public Transformer(String targetClassName) {
            this.targetClassName = targetClassName;
        }

        @Override
        public byte[] transform(ClassLoader loader,
                                String className,
                                Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain,
                                byte[] classfileBuffer) {
            if (className == null) {
                return null;
            }

            className = className.replace("/", ".");
            if (!className.equals(targetClassName)) {
                return null;
            }
            System.out.println("transform className=: " + className);

            ClassPool classPool = ClassPool.getDefault();

            // 将要修改的类的classpath加入到ClassPool中,否则找不到该类
            classPool.appendClassPath(new LoaderClassPath(loader));
            try {
                CtClass ctClass = classPool.get(className);
                for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
                    if (Modifier.isPublic(ctMethod.getModifiers()) && !ctMethod.getName().equals("test")) {
                        // 修改字节码
                        ctMethod.addLocalVariable("begin", CtClass.longType);
                        ctMethod.addLocalVariable("end", CtClass.longType);

                        ctMethod.insertBefore("begin = System.currentTimeMillis();");
                        ctMethod.insertAfter("Thread.sleep(1000L);");
                        ctMethod.insertAfter("end = System.currentTimeMillis();");

                        ctMethod.insertAfter("System.out.println("方法" + ctMethod.getName() + "耗时"+ (end - begin) +"ms");");
                    }
                }
                ctClass.detach();
                return ctClass.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return classfileBuffer;
        }
    }

}

整个目录结构如下

Java 探针技术原理及实践

 

 

先执行TargetMain 这个目标程序启动效果如下:

Java 探针技术原理及实践

 

再启动MyAgentMain 代理程序

 

Java 探针技术原理及实践

 

 

最后再回到TargetMain 效果已经出来了

 

Java 探针技术原理及实践

 

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>