Jave Agent(java代理 java探针 字节码插庄)

java探针、字节码插庄都是指的agent技术,agent技术可以构建一个独立于应用程序的代理程序(即为Agent),用来协助监测、运行甚至替换其他JVM上的程序(即替换字节码)。使用它可以实现虚拟机级别的AOP功能。
实现java agent,有两种类型的:
1.运行在主程序之前通过命令加载agent的jar包。
2.运行在主程序之后通过VirtualMachine来加载agent。

1 运行在主程序之前

agent程序需要实现premain方法,premain方法有两种签名,虚拟机会首先尝试运行第一个方法,如果没有才会运行第二个方法。

/**
 * @param agentArgs 加载agent时传递给agent的参数
 * @param inst 提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码,
 * 以搜集各种工具所使用的数据
 */
public static void premain(String agentArgs, Instrumentation inst);
    
public static void premain(String agentArgs);

agent作为一个单独的jar包存在,程序打包需要有manifest文件,即jar包里有META-INF目录,目录下有MANIFEST.MF文件,并且需要在文件种指定agent主程序,通过Premain-Class参数指定,如:
Premain-Class: org.example.javaagent.MyJavaAgent
如果是maven程序可在pom.xml种配置如下参数打包时生成MANIFEST.MF文件。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
                <!-- 参数方式启动agent需要这个 -->
                <Premain-Class>
                    org.example.javaagent.MyJavaAgent
                </Premain-Class>
                <!-- 启动后附加启动agent需要这个 -->
                <Agent-Class>
                    org.example.javaagent.MyJavaAgent
                </Agent-Class>
                <!-- 是否可以重新转换类 -->
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
                <!-- 是否可以重新定义类 -->
                <Can-Redefine-Classes>
                    true
                </Can-Redefine-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

1.1 加载Agent程序

启动时加入命令

java -javaagent:E:\self\learn\javaagent-demo-1.0-SNAPSHOT.jar=hellojavaagent -jar 主程序.jar org.example.javaagent.App

-javaagent 必须写在-jar之前否则不生效,javaagent的参数:E:\self\learn\javaagent-demo-1.0-SNAPSHOT.jar是编写的agent程序所在的jar包,等号后的hellojavaagent是传给agent的参数。

1.2 具体实现

1.2.1创建应用程序工程

在这里插入图片描述

public class OrderService {
    
    
    public void order() {
    
    
        System.out.println(Thread.currentThread().getName()+"下了一个订单。。。");
    }
}
public class App {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        for(;;) {
    
    
            Thread.sleep(500);

            new OrderService().order();
        }
    }
}

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>app</artifactId>
    <version>1.0-SNAPSHOT</version>

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

</project>

1.2.2 创建agent工程

在这里插入图片描述

public class MyJavaAgent {
    
    

    /**
     *  这个是静态调用,在启动jvm添加-javaagent参数后会调用
     *  运行在main方法之前,虚拟机会先尝试运行这个
     * @param agentArgs
     * @param inst
     * @return: void
     * @Author: jt-ape
     * @Date: 2021/1/28 23:18
     */
    public static void premain(String agentArgs, Instrumentation inst) {
    
    
        System.out.println("调用方法:premain(String agentArgs, Instrumentation inst)");
        System.out.println("javaagent 参数:"+agentArgs);
        inst.addTransformer(new MyClassFileTransformer(),true);
    }

    /**
     * 运行在main方法之前,不存在上面方法,虚拟机才会运行这个
     * @param agentArgs
     * @return: void
     * @Author: jt-ape
     * @Date: 2021/1/28 23:18
     */
    public static void premain(String agentArgs) {
    
    

    }

}

代码解释

Instrumentation:增强器
(1)add/removeTransformer:添加/删除 ClasFileTransformer;
(2)retransformerClasses:指定哪些类,在已加载的情况下,重新进行转换处理,即触发重新加载类定义;对于重新加载的类不能修改旧有的类声明,比如:不能增加属性、不能修改方法声明等;
(3)redefineClasses:指定哪些类,触发重新加载类定义,与上面不同的是不会重新进行转换处理,而是把处理结果 bytecode 直接给 JVM;
(4)getAllLoadedClasses:获取当前已加载的 Class 集合;
(5)getInitiatedClasses:获取由某个特定 ClassLoader 加载的类定义;
(6)getObjectSize:获得一个对象占用的空间大小;
(7)appendToBootstrapClassLoaderSearch/appentToSystemClassLoaderSearch:增加 BootstrapClassLoader/SystemClassLoader 搜索路径;
(8)isNativeMethodPrefixSupported/SetNativeMethodPrefix:判断 JVM 是否支持拦截 Native Method;

通过Instrumentation实例添加了一个转换器 ClassFileTransformer,该转换器用于改变运行时的字节码(class File),这个改变发生在jvm加载这个类之前,对所有的类加载器有效,即jvm加载某个类之前会调用ClassFileTransformer的transform方法
如果一个transformer不想改变任何代码,那么返回null否则,应该创建一个新的byte[],不能修改classfileBuffer
存在多个transformers时,每个transformer会进行链式调用。一个transformer抛出异常,后续的transformer依然会执行,抛异常和返回Null效果相同。
在向Instrumentation#addTransformer添加转换器的时候,会指定canRetransform(默认为false),决定retransformation是否可用。
一旦一个transformer被注册到instrumentation中,每当一个类被定义(ClassLoader.defineClass)或被重新定义(Instrumentation.redefineClasses)时,它都会被调用。
如果retransformation可用,那么一个类被retransformation(Instrumentation.retransformClasses)时,transformer也会被调用。
是否可以被重新转换也受到MANIFEST.MF文件种Can-Retransform-Classes参数的控制,为true则可以。
是否可以被重新定义也受到MANIFEST.MF文件中Can-Redefine-Classes参数的控制,为true则可以。

public class MyClassFileTransformer implements ClassFileTransformer {
    
    
    /**
     * 字节码被加载到虚拟机前会调用这个方法,通过javassist字节码技术改变class
     * @param loader 类加载器
     * @param className 类的全限定名
     * @param classBeingRedefined 如果这是由重新定义或重新转换触发的,则这个类存在重新定义或重新转换,否则为null
     * @param protectionDomain 正在定义或重新定义的类的保护域
     * @param classfileBuffer 类文件格式的输入字节缓冲区-不能修改
     * return byte[] 不想改变任何代码,那么返回null。否则,应该创建一个新的byte[]
     */
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    
    
        if (className.equals("org/example/javaagent/service/OrderService")) {
    
    
            System.out.println(className + ":进入ClassFileTransformer");
            try {
    
    
                ClassPool classPool = ClassPool.getDefault();
                classPool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = classPool.makeClass(new ByteArrayInputStream(classfileBuffer), false);

                //重写toString方法,将sex属性加入返回结果中。
                CtMethod method = clazz.getDeclaredMethod("order");
                method.insertBefore("System.out.print(\"我在这个方法前面加点东西哈\");");
                return clazz.toBytecode();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
        return null;
    }
}

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>javaagent-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <!-- 参数方式启动agent需要这个 -->
                            <Premain-Class>
                                org.example.javaagent.MyJavaAgent
                            </Premain-Class>

                            <!-- 启动后附加启动agent需要这个 -->
                            <Agent-Class>
                                org.example.javaagent.MyJavaAgent
                            </Agent-Class>

                            <!-- 是否可以重新转换类 -->
                            <Can-Retransform-Classes>
                                true
                            </Can-Retransform-Classes>

                            <!-- 是否可以重新定义类 -->
                            <Can-Redefine-Classes>
                                true
                            </Can-Redefine-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

1.2.3 测试运行

将agent工程打包放到指定目录,运行App的main方法
在这里插入图片描述
在这里插入图片描述
可以看到通过agent改变了OrderService的代码。

2 运行在主程序之后

这种方式agent程序需要实现agentmain方法,agentmain方法有两种签名,虚拟机会首先尝试运行第一个方法,如果没有才会运行第二个方法。

/**
 * @param agentArgs 加载agent时传递给agent的参数
 * @param inst 提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码,
 * 以搜集各种工具所使用的数据
 */
public static void agentmain(String agentArgs, Instrumentation inst);
    
public static void agentmain(String agentArgs);

agent程序打包需要有manifest文件,即jar包里有META-INF目录,目录下有MANIFEST.MF文件,并且需要在文件种指定agent主程序,通过Agent-Class参数指定,如:
Agent-Class: org.example.javaagent.MyJavaAgent
如果maven程序,配置如上。

2.1 加载Agent程序

这种方式加载Agent程序需要通过VirtualMachine类attach到主程序的jvm上执行,VirtualMachine在tools.jar里所以需要引入tools.jar的依赖,tools.jar的路径:JAVA_HOME\lib\tools.jar

VirtualMachine attach = VirtualMachine.attach(jvm);
// 传入agent程序的jar包路径,第二个参数时传给agent程序的参数
VirtualMachine attach.loadAgent("E:\\self\\learn\\javaagent-demo-1.0-SNAPSHOT.jar","agentagent");

2.2 具体实现

2.2.1 创建应用程序工程

同1.2.1章节

2.2.2 创建agent工程

同1.2.2章节,需要修改MyJavaAgent.java

public class MyJavaAgent {
    
    

    /**
     *  动态调用,虚拟机启动之后执行
     *  虚拟机会先尝试运行这个
     * @param agentArgs
     * @param inst
     * @return: void
     * @Author: jt-ape
     * @Date: 2021/1/28 23:54
     */
    public static void agentmain(String agentArgs, Instrumentation inst){
    
    
        System.out.println("调用方法:agentmain(String agentArgs, Instrumentation inst)");
        System.out.println("javaagent 参数:"+agentArgs);
        // 添加转换器,并设置为可以重新转换
        inst.addTransformer(new MyClassFileTransformer(),true);

        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class allLoadedClass : allLoadedClasses) {
    
    
            if (allLoadedClass.getName().equals("org.example.javaagent.service.OrderService")) {
    
    
                try {
    
    
                    // 重新转换目标类,由于目标类已经加载了,所以需要重新转换,才会调用用ClassFileTransformer的transform方法
                    inst.retransformClasses(allLoadedClass);
                    break;
                } catch (UnmodifiableClassException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     *  不存在上面的agentmain方法才会调用本方法
     * @param agentArgs
     * @return: void
     * @Author: jt-ape
     * @Date: 2021/1/30 14:27
     */
    public static void agentmain(String agentArgs){
    
    

    }
}

由于目标程序已经启动类已经加载,所以添加转换器后,还需要调用Instrumentation的retransformClasses方法重新转换目标类。否则将不会调用转换器ClassFileTransformer的transform方法。

同样需要将javaagent-demo工程打包放到指定目录

2.2.3 创建工程使目标jvm加载agent程序

在这里插入图片描述

public class Attach {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 查找所有JVM经常
        List<VirtualMachineDescriptor> attachs = VirtualMachine.list();
        attachs.stream().forEach(jvm -> {
    
    
            System.out.println(jvm.displayName()+":"+ jvm.id());
        });

        System.out.println("请输入jvm进程ID");
        Scanner scanner = new Scanner(System.in);
        String s = scanner.nextLine();

        for(VirtualMachineDescriptor jvm:attachs){
    
    
           if(s.equals(jvm.id())) {
    
    
               VirtualMachine attach = null;
               try {
    
    
                  // 获取目标jvm
                   attach = VirtualMachine.attach(jvm);
                  // 加载agent,第二个参数为传递给agent的参数
                   attach.loadAgent("E:\\self\\learn\\javaagent-demo-1.0-SNAPSHOT.jar","agentagent");
                  
                  attach.detach();
               } catch (AttachNotSupportedException e) {
    
    
                   e.printStackTrace();
               } catch (IOException e) {
    
    
                   e.printStackTrace();
               } catch (AgentLoadException e) {
    
    
                   e.printStackTrace();
               } catch (AgentInitializationException e) {
    
    
                   e.printStackTrace();
               }

              break;
           }
        }


    }
}

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>javaagent-dync</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>jdk.tools</groupId>
            <artifactId>jdk.tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>D:\jdk\jdk1.8.0_211-64\lib\tools.jar</systemPath>
        </dependency>
    </dependencies>

</project>

2.2.4 测试运行

首先运行主程序App的main方法
在这里插入图片描述
运行Attach.java,输入App的进程ID

在这里插入图片描述
查看App的控制台结果

在这里插入图片描述
可以看到执行了agent程序。

参考链接:
https://www.jianshu.com/p/f5efc53ced5d
https://www.cnblogs.com/jhxxb/p/11570503.html
https://blog.csdn.net/ljz2016/article/details/83309599/

猜你喜欢

转载自blog.csdn.net/qq_36706941/article/details/113443423