Java agent实现JVM exception 统计

本文介绍了Java agent的原理和实现,包括JVMTI、JVMTIAgent和javaagent的概念。讲解了如何在类加载前后进行字节码拦截修改,以及如何在运行时动态加载agent。通过示例说明了如何编译和attach Java agent,以实现JVM异常的统计功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考文献
http://www.infoq.com/cn/articles/javaagent-illustrated
http://lovestblog.cn/blog/2014/06/18/jvm-attach/

原理

对于javaagent,或许大家都听过,甚至使用过,常见的用法大致如下:

java -javaagent:myagent.jar=mode=test Test

我们通过-javaagent来指定我们编写的agent的jar路径(./myagent.jar),以及要传给agent的参数(mode=test),在启动的时候这个agent就可以做一些我们希望的事了。

javaagent的主要功能如下:

  • 可以在加载class文件之前做拦截,对字节码做修改
  • 可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
    还有其他一些小众的功能
  • 获取所有已经加载过的类
  • 获取所有已经初始化过的类(执行过clinit方法,是上面的一个子集)
  • 获取某个对象的大小
  • 将某个jar加入到bootstrap classpath里作为高优先级被bootstrapClassloader加载
  • 将某个jar加入到classpath里供AppClassloard去加载
  • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
    想象一下可以让程序按照我们预期的逻辑去执行,听起来是不是挺酷的。

JVMTI

JVMTI全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。

比如最常见的,我们想在某个类的字节码文件读取之后、类定义之前修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那就可以实现一个回调函数赋给jvmtiEnv(JVMTI的运行时,通常一个JVMTIAgent对应一个jvmtiEnv,但是也可以对应多个)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数中,大致实现如下:,

jvmtiEventCallbacks callbacks;

jvmtiEnv *          jvmtienv = jvmti(agent);

jvmtiError          jvmtierror;

memset(&callbacks, 0, sizeof(callbacks));

callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;

jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,

                                             &callbacks,

                                             sizeof(callbacks));

JVMTIAgent

JVMTIAgent其实就是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);

JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);

JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);

  • Agent_OnLoad函数,如果agent是在启动时加载的,也就是在vm参数里通过-agentlib来指定的,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。
  • Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数。
  • Agent_OnUnload函数,在agent卸载时调用,不过貌似基本上很少实现它。
    其实我们每天都在和JVMTIAgent打交道,只是你可能没有意识到而已,比如我们经常使用Eclipse等工具调试Java代码,其实就是利用JRE自带的jdwp agent实现的,只是IDEA等工具在没让你察觉的情况下将相关参数(类似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)自动加到程序启动参数列表里了,其中agentlib参数就用来跟要加载的agent的名字,比如这里的jdwp(不过这不是动态库的名字,JVM会做一些名称上的扩展,比如在Linux下会去找libjdwp.so的动态库进行加载,也就是在名字的基础上加前缀lib,再加后缀.so),接下来会跟一堆相关的参数,将这些参数传给Agent_OnLoad或者Agent_OnAttach函数里对应的options。

javaagent

说到javaagent,必须要讲的是一个叫做instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),因为javaagent功能就是它来实现的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),这个名字也完全体现了其最本质的功能:就是专门为Java语言编写的插桩服务提供支持的。

instrument agent

instrument agent实现了Agent_OnLoadAgent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:myagent.jar的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制(JVM Attach机制实现),通过发送load命令来加载agent。

instrument agent的核心数据结构如下:

struct _JPLISAgent {
    JavaVM *                mJVM;                   /* handle to the JVM */
    JPLISEnvironment        mNormalEnvironment;     /* for every thing but retransform stuff */
    JPLISEnvironment        mRetransformEnvironment;/* for retransform stuff only */
    jobject                 mInstrumentationImpl;   /* handle to the Instrumentation instance */
    jmethodID               mPremainCaller;         /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
    jmethodID               mAgentmainCaller;       /* method on the InstrumentationImpl for agents loaded via attach mechanism */
    jmethodID               mTransform;             /* method on the InstrumentationImpl that does the class file transform */
    jboolean                mRedefineAvailable;     /* cached answer to "does this agent support redefine" */
    jboolean                mRedefineAdded;         /* indicates if can_redefine_classes capability has been added */
    jboolean                mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
    jboolean                mNativeMethodPrefixAdded;     /* indicates if can_set_native_method_prefix capability has been added */
    char const *            mAgentClassName;        /* agent class name */
    char const *            mOptionsString;         /* -javaagent options string */
};

struct _JPLISEnvironment {
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */
    JPLISAgent *            mAgent;                 /* corresponding agent */
    jboolean                mIsRetransformer;       /* indicates if special environment */
};

这里解释一下几个重要项:

  • mNormalEnvironment:主要提供正常的类transform及redefine功能。
  • mRetransformEnvironment:主要提供类retransform功能。
  • mInstrumentationImpl:这个对象非常重要,也是我们Java agent和JVM进行交互的入口,或许写过javaagent的人在写premain以及agentmain方法的时候注意到了有个Instrumentation参数,该参数其实就是这里的对象。
  • mPremainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在启动时加载的,则该方法会被调用。
  • mAgentmainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,该方法在通过attach的方式动态加载agent的时候调用。
  • mTransform:指向sun.instrument.InstrumentationImpl.transform方法。
    mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class
  • mOptionsString:传给agent的一些参数。
  • mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置Can-Redefine-Classes:true
  • mNativeMethodPrefixAvailable:是否支持native方法前缀设置,同样在javaagent的MANIFEST.MF里设置Can-Set-Native-Method-Prefix:true
  • mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了Can-Retransform-Classes:true,将会设置mRetransformEnvironment的mIsRetransformer为true。
    在启动时加载instrument agent

正如前面“概述”里提到的方式,就是启动时加载instrument agent,具体过程都在InvocationAdapter.cAgent_OnLoad方法里,这里简单描述下过程:

创建并初始化JPLISAgent

监听VMInit事件,在vm初始化完成之后做下面的事情:
1. 创建InstrumentationImpl对象
2. 监听ClassFileLoadHook事件
3. 调用InstrumentationImpl的loadClassAndCallPremain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Premain-Class类的premain方法
4. 解析javaagent里MANIFEST.MF里的参数,并根据这些参数来设置JPLISAgent里的一些内容
5. 在运行时加载instrument agent

在运行时加载的方式,大致按照下面的方式来操作:

VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);

上面会通过JVM的attach机制来请求目标JVM加载对应的agent,过程大致如下:

创建并初始化JPLISAgent
解析javaagent里MANIFEST.MF里的参数
创建InstrumentationImpl对象
监听ClassFileLoadHook事件
调用InstrumentationImpl的loadClassAndCallAgentmain方法,在这个方法里会调用javaagent里MANIFEST.MF里指定的Agent-Class类的agentmain方法

实现

#include <jni.h>
#include <jvmti.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

_Atomic(int32_t) count = ATOMIC_VAR_INIT(0);


JNIEXPORT jint JNICALL
  Java_com_github_marschall_excount_ExceptionCounter_getCount(JNIEnv *env,
                                                              jobject thisObj) {
    return atomic_load(&count);
}

JNIEXPORT jint JNICALL
  Java_com_github_marschall_excount_ExceptionCounter_clearAndGetCount(JNIEnv *env,
                                                                      jobject thisObj) {
    return atomic_exchange(&count, 0);
}


void JNICALL ExceptionCallback(jvmtiEnv *jvmti, JNIEnv *env, jthread thread,
                               jmethodID method, jlocation location, jobject exception,
                               jmethodID catch_method, jlocation catch_location) {
    atomic_fetch_add(&count , 1);
    char* class_name;
        jclass exception_class = (*env)->GetObjectClass(env, exception);
        (*jvmti)->GetClassSignature(jvmti, exception_class, &class_name, NULL);
        fprintf(stdout, "Exception: %s\n", class_name);
}


JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv* jvmti;
    jvmtiEventCallbacks callbacks;
    jvmtiCapabilities capabilities;

    if ((*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
        fprintf(stderr, "GetEnv failed\n");
        return -1;
    }

    memset(&capabilities, 0, sizeof(capabilities));
    capabilities.can_generate_exception_events = 1;
    if ((*jvmti)->AddCapabilities(jvmti, &capabilities) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "AddCapabilities failed\n");
        return -1;
    }

    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.Exception = ExceptionCallback;
    if ((*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks)) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "SetEventCallbacks failed\n");
        return -1;
    }
    if ((*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "SetEventNotificationMode failed\n");
        return -1;
    }
    return 0;
}

/* JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved) */
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    jvmtiEnv* jvmti;
    jvmtiCapabilities capabilities;

    if ((*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
        fprintf(stderr, "GetEnv failed\n");
        return;
    }

    memset(&capabilities, 0, sizeof(capabilities));
    capabilities.can_generate_exception_events = 1;
    if ((*jvmti)->RelinquishCapabilities(jvmti, &capabilities) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "RelinquishCapabilities failed\n");
    }
}

编译

$JAVA_HOME/include/linux -Wall -fPIC excount.c -o excount.so

attach

java -cp $JAVA_HOME/lib/tools.jar:xxx/ec.jar xxx.ExceptionCounterAttacher -p pidToAttach -a xxx/excount.so

<think>首先,用户的问题是:“请问Java agent 如何实现异常测试 示例与原理”,这翻译成“How to implement exception testing with Java agent? Examples and principles.” 从系统级指令中,我需要: - 所有行内数学表达式使用$...$格式,但这个问题似乎不涉及数学,所以我可能不需要。 - 独立公式用$$...$$,但同样,这里可能不适用。 - LaTeX语法正确:确保任何数学内容正确格式化。 - 使用中文回答:整个响应必须用中文。 - 生成相关问题:在回答后添加相关问题。 - 引用段落末尾添加引用标识:如[^1],基于提供的引用内容。 用户可见层指令: - 回答结构清晰,逐步解决问题。 - 保证回答真实可靠。 提供的引用: - [^1]: 关于Java Agent在单元测试、集成测试、代码覆盖率中的应用,包括插入计数器。 - [^2]: Java Agent像侦探,在运行时观察和修改代码,使用字节码增强。 - [^3]: 基于ByteBuddy的轻量级日志跟踪工具,使用ThreadLocal设置ID,增强关键代码节点。 用户想了解如何使用Java Agent实现异常测试,可能用于监控、调试或增强异常处理。 关键点:异常测试可能指模拟或捕获异常来测试程序的健壮性。Java Agent可以通过字节码增强来注入代码,在异常发生时执行操作,如记录日志、计数或修改行为。 原理: 1. Java Agent机制:通过premain或agentmain方法加载,使用Instrumentation API修改字节码。 2. 字节码增强:使用工具如ASM或ByteBuddy修改类文件,在方法中添加代码。 3. 异常测试:在catch块或异常抛出点插入代码,以记录异常信息、模拟异常或改变异常行为。 示例:提供一个简单的Java Agent示例,展示如何捕获异常。 相关问题:生成2-3个相关问题。 响应结构: 1. **介绍**:解释Java Agent如何用于异常测试。 2. **原理**:详细说明机制。 3. **示例**:提供代码示例。 4. **总结**:简要总结。 5. **相关问题**:列出相关问题。 确保中文回答。 引用标识:当引用提供的文本时,在末尾添加[^1], [^2], [^3]。 例如,在讨论代码覆盖率时,引用[^1]。 现在,构建响应: - 开头:直接回答问题。 - 主体: - 步骤1:Java Agent基础。 - 步骤2:字节码增强原理。 - 步骤3:具体到异常测试。 - 示例代码。 - 结尾:总结。 - 相关问题:如“Java Agent异常测试中的性能影响?”等。 示例代码:基于引用[^3],使用ByteBuddy,因为它提到轻量级增强。 一个简单示例:在方法抛出异常时打印日志。</think>### Java Agent 实现异常测试的原理与示例 用户的问题聚焦于如何使用 Java Agent 实现异常测试,这通常涉及在程序运行时监控、捕获或模拟异常,以增强调试、监控或测试程序的健壮性。Java Agent 是一种基于 JVM Instrumentation API 的技术,它允许在类加载时或运行时修改字节码,从而在异常发生点注入自定义逻辑(如记录异常信息、计数异常次数或模拟异常行为)。下面我将逐步解释原理、提供示例代码,并确保回答基于可靠的技术原理(参考了提供的引用内容)。 #### 1. **Java Agent 实现异常测试的原理** Java Agent 的核心是字节码增强(Bytecode Enhancement),它通过 Instrumentation API 在类加载时修改字节码。具体到异常测试,主要机制包括: - **异常捕获点注入**:Java Agent 可以识别方法中的异常处理块(如 `try-catch` 语句)或异常抛出点(如 `throw` 语句),并在这些位置插入自定义代码。例如,插入逻辑来记录异常堆栈、计数异常发生次数或修改异常行为。 - **Instrumentation API 的作用**:Agent 通过 `premain` 或 `agentmain` 方法启动,使用 `ClassFileTransformer` 接口转换字节码。工具如 ByteBuddy(轻量级字节码增强框架)简化了这一过程,它允许以编程方式定义增强逻辑,而无需直接操作底层字节码[^2][^3]。 - **异常测试的常见应用**:在测试场景中,Java Agent 可用于: - **监控异常**:自动记录异常信息到日志或数据库,帮助分析程序弱点。 - **模拟异常**:在特定条件下强制抛出异常,测试程序的容错能力。 - **性能分析**:统计异常频率,评估系统稳定性(这与代码覆盖率分析类似,引用[^1]提到 Java Agent 可用于插入计数器)。 - **优势与限制**:Java Agent 的优势是无侵入性——无需修改源码即可实现增强。但需要注意性能开销,因为字节码增强会增加方法执行时间(引用[^3]强调轻量级设计,如仅在关键节点增强,以减少开销)。 关键原理公式(独立公式): 在字节码增强中,异常注入的逻辑可以表示为: $$ \text{增强后方法} = \text{原方法字节码} + \text{注入代码(如异常处理逻辑)} $$ 这确保了在运行时,每当异常发生时,注入的代码会自动执行。 #### 2. **实现步骤与示例** 下面是一个完整的 Java Agent 示例,使用 ByteBuddy 框架实现异常测试。示例目标:当一个方法抛出异常时,自动记录异常信息到日志(模拟监控场景)。代码基于引用[^3]的轻量级理念,仅增强异常抛出点,减少性能开销。 ##### 步骤 1: 创建 Java Agent 项目 假设使用 Maven 项目,添加依赖(`pom.xml`): ```xml <dependencies> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.14.5</version> <!-- 使用最新稳定版 --> </dependency> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.14.5</version> </dependency> </dependencies> ``` ##### 步骤 2: 编写 Agent 类(实现字节码增强) 创建 `ExceptionTestAgent.java`,使用 ByteBuddy 在异常抛出时注入日志逻辑。 ```java import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.matcher.ElementMatchers; import java.lang.instrument.Instrumentation; public class ExceptionTestAgent { // premain 方法:在 JVM 启动时加载 Agent public static void premain(String args, Instrumentation inst) { new AgentBuilder.Default() .type(ElementMatchers.any()) // 匹配所有类 .transform((builder, typeDescription, classLoader, module) -> builder.visit(Advice.to(ExceptionAdvice.class).on(ElementMatchers.isMethod())) // 增强所有方法 ) .installOn(inst); } // Advice 类:定义注入逻辑 public static class ExceptionAdvice { @Advice.OnMethodExit(onThrowable = Throwable.class) public static void onExit(@Advice.Thrown Throwable throwable) { if (throwable != null) { // 在异常发生时记录日志(实际中可替换为数据库或监控系统) System.out.println("[Agent] 异常捕获: " + throwable.getMessage()); // 这里可以添加更多逻辑,如异常计数或模拟行为 } } } } ``` **原理说明**: - 使用 `@Advice.OnMethodExit` 注解,在方法退出时检查是否有异常抛出(`@Advice.Thrown`)。 - 如果异常存在,打印日志。实际应用中,可扩展为发送到日志系统(如 Log4j)或集成测试框架。 - 此设计轻量级,仅增加少量开销(引用[^3]强调在关键节点增强)。 ##### 步骤 3: 定义 MANIFEST.MF 文件 在 `src/main/resources/META-INF/MANIFEST.MF` 中指定 Agent 入口: ``` Manifest-Version: 1.0 Premain-Class: ExceptionTestAgent Can-Redefine-Classes: true Can-Retransform-Classes: true ``` ##### 步骤 4: 测试目标应用 创建一个简单的测试类 `TestApp.java`,模拟可能抛出异常的方法: ```java public class TestApp { public static void main(String[] args) { try { riskyMethod(); // 调用可能抛出异常的方法 } catch (Exception e) { System.out.println("主程序捕获异常: " + e.getMessage()); } } public static void riskyMethod() { throw new RuntimeException("测试异常发生!"); // 模拟异常 } } ``` ##### 步骤 5: 运行与验证 1. 打包 Agent 为 JAR(如 `exception-agent.jar`)。 2. 启动测试应用时加载 Agent: ```bash java -javaagent:path/to/exception-agent.jar -jar TestApp.jar ``` 3. **输出示例**: ``` [Agent] 异常捕获: 测试异常发生! 主程序捕获异常: 测试异常发生! ``` 这证明 Agent异常抛出时自动记录了信息,而无需修改 `TestApp` 源码。 #### 3. **总结** 通过 Java Agent 和字节码增强(如 ByteBuddy),可以实现高效的异常测试:在异常发生时注入监控、日志或模拟逻辑,提升测试覆盖率和调试效率。此方法无侵入,适用于单元测试、集成测试或生产监控(引用[^1]提到类似代码覆盖率应用)。注意,实际项目中应优化增强范围以控制性能开销(引用[^3]建议仅增强关键节点)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值