java instrument机制分析与实践

简介: 1. 原理介绍java instrument是一种字节码增强技术,在jdk1.5开始已引入,其核心功能实现依赖java.lang.instrument.Instrumentation接口,通过实现该接口,我们可以对已加载和未加载的类进行修改。java instrumentation最常用的一种使用方式是通过jvm的启动参数:-javaagent来启动,例如:java -javaagent:myag

1. 原理介绍

java instrument是一种字节码增强技术,在jdk1.5开始已引入,其核心功能实现依赖java.lang.instrument.Instrumentation接口,通过实现该接口,我们可以对已加载和未加载的类进行修改。

java instrumentation最常用的一种使用方式是通过jvm的启动参数:-javaagent来启动,例如:java -javaagent:myagent.jar MyMain。为了能让jvm识别到agent的入口类,需要在MANIFEST.MF文件中指定Premain-Class等配置,例如:(最后需要留一个空行,否则会报错)

通过-javaagent来启动,实际上是一种静态的代理,其处理流程如下:

在我们实现的Agent类中,有一个premain方法,顾名思义,他会在java启动入口方法main之前被调用,在premain中我们可以进行类的字节码增强,例如通过实现ClassFileTransformer接口,可以将指定规则的类的字节码进行替换,最终jvm的类加载器会去加载已经被agent修改后的类字节码。

这种静态的agent只能在jar包启动时进行代理,存在比较大的局限性,jdk1.6开始引入了动态的Attach Agent方式,可以在jvm启动之后的任意时刻通过Attach API远程加载Agent的jar,比如阿里开源的arthas工具就是基于Attach API实现的。

JVM Attach API的实现主要是基于UNIX域套接字的,与TCP,UDP不同,UNIX域套接字主要应用于同一个主机上的进程间通信,虽然理论上使用localhost也可以在同一个主机上实现进程间通信,但是UNIX域套接字更可靠,效率也更高。(因为它不用进行协议处理,不需要计算序列号,也不用发送确认报文,只需读写数据即可,因为在同一个主机上进程之间实际上就是处于一个高度可信可靠的通信环境)详见:Unix域套接字简介

Attach API的执行过程如下:

2. 静态agent使用示例

首先新建一个InstrumentTest类,如下:

package com.xycode.techlecture.instrument;

/**
* 测试javaagent的入口类
* @author: xycode
* @email: lianguang.xy@alibaba-inc.com
* @date: 2022/8/17
*/
public class InstrumentTest {
    public void foo(){
        System.out.println("execute foo");
        bar();
    }

    public void bar(){
        System.out.println("execute bar");
    }

    public static void main(String[] args) {
        new InstrumentTest().foo();
    }

}

期望在InstrumentTest类实例方法执行前后添加log,我们可以写出如下代码:

package com.xycode.agent;

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

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;

/**
 * java agent入口类
 *
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/8/17
 */
public class AgentMain {

    public static class MyClassFileTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            //只处理com.xycode.techlecture.instrument.InstrumentTest
            if (!"com/xycode/techlecture/instrument/InstrumentTest".equals(className)) {
                return classfileBuffer;
            }
            try {
                //asm字节码操作
                ClassReader classReader = new ClassReader(classfileBuffer);
                ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
                ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
                    @Override
                    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                        String[] exceptions) {
                        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature,
                            exceptions);
                        if (name.equals("<init>")) {
                            return methodVisitor;
                        }
                        return new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {
                            @Override
                            protected void onMethodEnter() {
                                //getstatic #2               //Field java/lang/System.out:Ljava/io/PrintStream;
                                this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                                //ldc #3                     //String ">> enter " + name
                                this.mv.visitLdcInsn(">> enter " + name);
                                //invokevirtual  #4          //Method java/io/PrintStream.println:(Ljava/lang/String;)V
                                this.mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
                                    "(Ljava/lang/String;)V", false);
                                super.onMethodEnter();
                            }

                            @Override
                            protected void onMethodExit(int opcode) {
                                super.onMethodExit(opcode);
                                this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                                this.mv.visitLdcInsn(">> exit " + name);
                                this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
                                    "(Ljava/lang/String;)V", false);

                            }
                        };
                    }
                };

                classReader.accept(classVisitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
                return classWriter.toByteArray();
            } catch (Exception exception) {
                exception.printStackTrace();
            }
            return classfileBuffer;
        }
    }

    //notice: javaagent jar入口方法为premain
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("premain");
        //注册类文件转换器
        instrumentation.addTransformer(new MyClassFileTransformer(), true);
    }
}

接着将AgentMain编译成jar,我们还需要配置MANIFEST.MF,目录结构与内容如下:

pom文件:(需要指定编译参数以及MANIFEST.MF文件路径)

<?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">

    <groupId>com.xycode</groupId>
    <artifactId>testJavaAgent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <modelVersion>4.0.0</modelVersion>


    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <compilerArguments>
                        <!-- 将jdk的依赖jar打入项目中,这样项目中使用的jdk的依赖就可以正常使用 -->
                        <bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
                    </compilerArguments>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <!-- 指定MANIFEST.MF文件路径-->
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

使用mvn编译出jar包:testJavaAgent-0.0.1-SNAPSHOT.jar,然后我们可以通过命令行运行,如下:

java -javaagent:${your_path}/testJavaAgent-0.0.1-SNAPSHOT.jar com.xycode.techlecture.instrument.InstrumentTest

也可以选择在IDEA中运行,只需在运行InstrumentTest时指定vm参数即可,如下:(在IEDA中可以使用idea的debug能力, 只需在java agent类中添加断点即可)

运行效果:

3. 动态agent使用示例

先创建一个AttachMainTest类,如下:

package com.xycode.techlecture.instrument.attach;

import java.util.concurrent.TimeUnit;

/**
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/8/26
 */
public class AttachMainTest {

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            System.out.println(foo());
            TimeUnit.SECONDS.sleep(2);
        }
    }

    public static int foo() {
        return 100;
    }
}

我们这里希望在jvm运行中动态修改foo方法的返回值,代码如下:

package com.xycode.agent;

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

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;

/**
 * attachAgent
 *
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/8/29
 */
public class AttachAgentMain {

    public static class MyClassFileTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            //指定处理com.xycode.techlecture.instrument.attach.AttachMainTest
            if (!"com/xycode/techlecture/instrument/attach/AttachMainTest".equals(className)) {
                return classfileBuffer;
            }

            //ASM字节码操作
            ClassReader classReader = new ClassReader(classfileBuffer);
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);

            ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
                @Override
                public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                    String[] exceptions) {
                    MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
                    //转换foo方法, 修改其返回值
                    if ("foo".equals(name)) {
                        return new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {
                            @Override
                            protected void onMethodEnter() {
                                //在方法开始处插入return 50;
                                this.mv.visitIntInsn(Opcodes.BIPUSH, 50); //bipush 50
                                this.mv.visitInsn(Opcodes.IRETURN); //ireturn
                            }
                        };
                    }
                    return methodVisitor;
                }
            };

            classReader.accept(classVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);

            return classWriter.toByteArray();
        }
    }

    //notice: attach agent jar入口方法为agentmain
    public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {
        System.out.println("agentMain");
        //注册类文件转换器
        instrumentation.addTransformer(new MyClassFileTransformer(),true);


        for (Class loadedClass : instrumentation.getAllLoadedClasses()) {
            //因为动态更改jvm中的指定类, 因此这里需要重新加载
            //notice: 注意这里的类名, 格式与asm中的类名不一样
            // 分别是com.xycode.techlecture.instrument.attach.AttachMainTest 与 com/xycode/techlecture/instrument/attach/AttachMainTest
            if (loadedClass.getName().equals("com.xycode.techlecture.instrument.attach.AttachMainTest")) {
                System.out.println("reloading " + loadedClass.getName());
                instrumentation.retransformClasses(loadedClass);
                break;
            }
        }
    }

}

目录结构与MANIFEST.MF如下:

pom文件如下:

<?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.xycode</groupId>
    <artifactId>testAttachAgent</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <compilerArguments>
                        <!-- 将jdk的依赖jar打入项目中,这样项目中使用的jdk的依赖就可以正常使用 -->
                        <bootclasspath>${java.home}/lib/rt.jar</bootclasspath>
                    </compilerArguments>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <!-- 指定MANIFEST.MF文件路径-->
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

然后编译出testAttachAgent-0.0.1-SNAPSHOT.jar,并运行AttachMainTest类,并找到其pid,如下

pid为19854,新建AttachClient类,并指定attach的pid,如下:

package com.xycode.techlecture.instrument.attach;

import java.io.IOException;

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

/**
 * @author: xycode
 * @email: lianguang.xy@alibaba-inc.com
 * @date: 2022/8/29
 */
public class AttachClient {

    public static void main(String[] args) throws IOException, AttachNotSupportedException {
        //attach上指定pid的java进程
        VirtualMachine virtualMachine = VirtualMachine.attach("19854");

        try {
            virtualMachine.loadAgent("/Users/xycode/IdeaProjects/techlecture/techlecture/testAttachAgent/target/testAttachAgent-0.0.1-SNAPSHOT.jar");
        }catch (Exception e){
            virtualMachine.detach();
        }
    }
}

最终效果如下:(可以看出,类已被重新加载,并更改了foo的返回值)

目录
相关文章
|
12天前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
18 2
|
4天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
8天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
SQL Java 数据库连接
从理论到实践:Hibernate与JPA在Java项目中的实际应用
本文介绍了Java持久层框架Hibernate和JPA的基本概念及其在具体项目中的应用。通过一个在线书店系统的实例,展示了如何使用@Entity注解定义实体类、通过Spring Data JPA定义仓库接口、在服务层调用方法进行数据库操作,以及使用JPQL编写自定义查询和管理事务。这些技术不仅简化了数据库操作,还显著提升了开发效率。
21 3
|
7天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
8天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
13天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
31 2
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。