Java Agent

Java 字节码 一文中有提到,使用 Java Agent 操控字节码,本文将讨论 Java Agent ,这是普通 Java 开发人员的真正的黑魔法。Java Agent 能够通过执行字节码的直接修改,在运行时 “侵入” JVM 上运行的 Java 应用程序的执行。Java Agent 很强大,但是也很危险:它们几乎可以完成所有操作,但是如果出现问题,也很容易导致 JVM 崩溃。
我们将通过解析 Java Agent 如何工作,如何运行它们以及展示 Java Agent 作为明显优势的一些简单示例来揭开 Java Agent 的神秘面纱。

1. Java Agent 基础知识

从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。代理类必须实现一个 public static void premain(String agentArgs, Instrumentation inst) 成为代理入口点的 main 方法(类似于常规 Java 应用程序的方法)。

Java 虚拟机(JVM)初始化后,premain(String agentArgs, Instrumentation inst) 将按照在 JVM 启动时指定代理的顺序调用每个代理的每个此类方法。完成此初始化步骤后, main 将调用真正的 Java 应用程序方法。简单来讲,就是 premain 方法,在 main 方法之前执行。

但是,如果类没有实现 public static void premain(String agentArgs, Instrumentation inst) 方法,JVM 将尝试查找并调用另一个重载版本 public static void premain(String agentArgs) 。注意,每个 premain 方法都必须返回,以便启动阶段继续进行。

咋一看很简单,但 Java Agent 实现应该提供另外一件事作为其包装的一部分:manifest。通常在 META-INF 文件夹中,名为 MANIFEST.MF,包含于包分发相关的各种元数据。
点击阅读:Java Agent 官方文档

2. Java Agent 代理和检测

Java Agent 的检测功能是无限的。最值得注意的包括但不限于:

  • 能够在运行时重新定义类。
    重新定义可以改变方法体,常量池和属性。重定义不得添加,删除,重命名字段或方法,不得更改方法的签名或更改继承。
  • 能够在运行时重新转换类。
    重新转换可以改变方法体,常量和属性。新转换不得添加,删除,重命名字段或方法,不得更改方法的签名或更改继承。
  • 能够允许使用应用于名称的前缀进行重试来修改本机方法解析的失败处理。

注意,在应用了转换或重新定义之后,不会检查,验证和安装重新转换或重新定义的类字节码。如果生成的字节码错误或不正确,则会抛出异常,这可能会导致 JVM 完全崩溃。

3. 编写一个简单的 Java Agent

我们将通过实现自己的类转换器来编写一个简单的 Java Agent。话虽如此,使用 Java Agent 的唯一缺点是,需要直接的字节码操作技能(如果大家对 Java 字节码 不是很了解,可以参考我的这篇文章: Java 字节码 )。而且,遗憾的是,Java 标准库不提供任何 API来使这些字节码操作成为可能。
为了填补这一空白,Java 社区提供了一些很成熟的库,比如 Javassist (Javassist 入门)。
现在,我们着手编写一个示例,我们假设想捕获 Java 应用程序中打开的每个 HTTP 连接的 URL。有很多方法可以通过直接修改 Java 源代码来实现,但让我们假设源代码由于许可证策略或其他原因而不可用。
为了方便操作 Java 字节码,首先引入 Javassist 的 maven 包:

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

打开 HTTP 连接的类的典型示例如下所示:

public class SimpleClass {
    public static void main( String[] args ) throws IOException {
        System.out.println("===========执行main方法=============");
        fetch("http://www.baidu.com");
        fetch("http://www.163.com");
    }

    private static void fetch(final String address) throws IOException {

        final URL url = new URL(address);
        final URLConnection connection = url.openConnection();

        try (final BufferedReader in = new BufferedReader(
                new InputStreamReader( connection.getInputStream())
        )){
            String inputLine = null;
            final StringBuffer sb = new StringBuffer();
            while ( (inputLine = in.readLine()) != null){
                sb.append(inputLine);
            }

            System.out.println("Content size:" + sb.length());
        }

    }

}

Java Agent 非常适合解决此类问题。我们只需要定义变换器,它将 sun.net.www.protocol.http.HttpURLConnection 通过注入代码来稍微修改构造函数。让我们来看看其实现:

public class SimpleClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
            ClassLoader loader,
            String className,
            Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.endsWith("sun/net/www/protocol/http/HttpURLConnection")){
            ClassPool classPool = ClassPool.getDefault();
            CtClass clazz = null;
            try {
                clazz = classPool.get("sun.net.www.protocol.http.HttpURLConnection");

                CtConstructor[] cs = clazz.getConstructors();
                for(CtConstructor constructor: cs){
                    constructor.insertAfter("System.out.println(this.getURL());");
                }

                byte[] byteCode = clazz.toBytecode();
                clazz.detach();

                return byteCode;
            } catch (NotFoundException | CannotCompileException | IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

定义 premain 方法,将 SimpleClassTransformer 类的实例添加到检测上下文中:

public class SimpleAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("=========开始执行premain============");
        SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer(transformer);

    }
}

要完成 Java Agent,还需要提供正确的 MANIFEST.MF,以便 JVM 能够选择正确的类。在 META-INF 目录下找到你的 MANIFEST.MF 文件:

Manifest-Version: 1.0
Premain-Class: com.demo.SimpleAgent

请注意,冒号后面一定要有空格,最后一行要为空。

4. 运行 Java Agent

从命令运行时,可以使用 -javaagent

-javaagent:<path-to-jar>[=options]

类似如下:

java -javaagent:agent.jar

注意,这是条伪命令,因为 agent.jar 有引用 javassist.jar 包中的内容,想要执行成功还需要调用 javassist.jar,命令如下:

java -javaagent:agent.jar -jar javassist.jar

运行结果:
在这里插入图片描述
下面我们在 IDE 中运行 Java Agent。

使用 idea:

在这里插入图片描述
然后运行 SimpleClass 中的 main 方法。结果如下:
在这里插入图片描述

使用 Eclipse:

在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述
写到这里,突然想到以前公司花了150W买过一个链路监控的产品,就是基于Java Agent 做的,功能很强大。但是需要注意,任何错误或不准确的字节码生成都可能导致JVM崩溃,一把双刃剑,看你怎么用了。

### Java Agent 使用方法 Java Agent 提供了一种机制,在不改变原有代码的情况下,允许开发者在 JVM 加载类之前或之后修改其行为。这可以通过 `-javaagent` 命令行参数或者 Attach API 来实现。 #### 通过命令行参数使用 Java Agent 当希望在整个应用程序生命周期内生效时,可以采用这种方式。具体来说,就是在启动 Java 应用程序的时候,利用 `-javaagent:<path-to-agent-jar>` 参数指定一个包含 `Instrumentation` 接口实现的 JAR 文件路径[^3]。此方式支持传递额外配置给代理程序,如: ```bash java -javaagent:/path/to/your-javaagent.jar=configParam=value -jar your-application.jar ``` 对于多个代理的情况,这些代理按照它们被声明的顺序依次执行完毕后再进入主程序逻辑。 #### 动态附加到正在运行的应用程序 (Attach API) 另一种场景下,可能需要对已经处于运行状态下的 JVM 实施监控或是调试功能而无需重启服务。此时可借助于 JDK 自带工具包里的 com.sun.tools.attach.VirtualMachine 类所提供的接口完成动态连接并加载新的 instrumentation agent 到目标进程中去[^4]。 以下是简单的例子展示如何创建自定义的 Java Agent 并将其应用于现有项目中: ```java // MyAgent.java import java.lang.instrument.Instrumentation; public class MyAgent { public static void premain(String args, Instrumentation inst) { System.out.println("MyAgent is running with argument: " + args); // Register a transformer to modify bytecode at load time. inst.addTransformer(new MyClassFileTransformer()); } } ``` 为了使上述代码片段能够作为有效的 Java Agent 工作,还需要确保打包成 jar 文件的同时附加上 MANIFEST.MF 清单文件,其中应包含如下属性以便让 JVM 认识这是一个合法的 agent 程序[^2]: ``` Manifest-Version: 1.0 Premain-Class: MyAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Can-Set-Native-Method-Prefix: true ``` 最后编译好后的 Jar 可以像下面这样用于实际环境中: ```bash java -javaagent:path/to/myagent.jar=argValue -jar application.jar ``` 这种技术特别适用于性能分析、故障排查以及AOP(面向切面编程)等领域内的开发工作。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值