当前位置: 首页 > news >正文

Java Agent 学习笔记

原文: http://nullwy.me/2018/10/java...
如果觉得我的文章对你有用,请随意赞赏

Java 从 1.5 开始提供了 java.lang.instrument(doc)包,该包为检测(instrument) Java 程序提供 API,比如用于监控、收集性能信息、诊断问题。通过 java.lang.instrument 实现工具被称为 Java Agent。Java Agent 可以修改类文件的字节码,通常是,在字节码方法插入额外的字节码来完成检测。关于如何使用 java.lang.instrument 包,可以参考 javadoc 的包描述(en, zh)。

开发 Java Agent 的涉及的要点如下图所示 [ref ]

Java Agent

Java Agent 支持两种方式加载,启动时加载,即在 JVM 程序启动时在命令行指定一个选项来启动代理;启动后加载,这种方式使用从 JDK 1.6 开始提供的 Attach API 来动态加载代理。

启动时加载 agent

最简单的例子

现在创建命名为 proj-demo 的 gradle 项目,目录布局如下:

$ tree proj-demo
proj-demo
├── build.gradle
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── demo
    │               └── App.java
    └── test
        └── java

7 directories, 2 files

com.demo.App 类的实现:

public class App {

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            System.out.println(getGreeting());
            Thread.sleep(1000L);
        }
    }

    public static String getGreeting() {
        return "hello world";
    }
}

运行 com.demo.App,每隔 1 秒输出 hello world

$ gradle build
$ java -cp "target/classes/java/main" com.demo.App
hello world
hello world

现在创建名称为 proj-premain 的 gradle 项目,com.demo.MyPremain 类实现 premain 方法:

package com.demo;

public class MyPremain {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println(agentArgs);
    }
}

META-INF/MANIFEST.MF 文件指定 Premain-Class 属性:

jar {
    manifest {
        attributes 'Premain-Class': 'com.demo.MyPremain'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

打包生成 proj-premain.jar,这个 jar 包就是 javaagent 代理。现在来试试运行 com.demo.App 时,启动这个 javaagent 代理。根据 javadoc 的描述,可以将以下选项添加到命令行来启动代理:

    -javaagent:jarpath[=options] 

指定 -javaagent:"proj-premain.jar=hello agent",传入的 agentArgshello agent,再次运行 com.demo.App

$ java -javaagent:"proj-premain.jar=hello agent" -cp "target/classes/java/main" com.demo.App
hello agent
hello world
hello world

可以看到,在运行 main 之前,运行了 premain 方法,即先输出 hello agent,每隔 1 秒输出 hello world

修改字节码

在实现 premain 时,除了能获取 agentArgs 参数,还能获取 Instrumentation 实例。Instrumentation 类提供 addTransformer 方法,用于注册提供的转换器 ClassFileTransformer

// 注册提供的转换器
void addTransformer(ClassFileTransformer transformer)

ClassFileTransformer 是抽象接口,唯一需要实现的是 transform 方法。在转换器使用 addTransformer 注册之后,每次定义新类时(调用 ClassLoader.defineClass)都将调用该转换器的 transform 方法。该方法签名如下:

// 此方法的实现可以转换提供的类文件,并返回一个新的替换类文件
byte[] transform(ClassLoader loader,
                 String className,
                 Class<?> classBeingRedefined,
                 ProtectionDomain protectionDomain,
                 byte[] classfileBuffer)
                 throws IllegalClassFormatException

操作字节码可以使用 ASM、Apache BCEL、Javassist、cglib、Byte Buddy 等库。下面示例代码,使用 BCEL 库实现名为 GreetingTransformer 转换器。该转换器实现的逻辑就是,将 com.demo.App.getGreeting() 方法输出的 hello world,替换为输出 premain 方法的传入的参数 agentArgs

public class MyPremain {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new GreetingTransformer(agentArgs));
    }
}
import org.apache.bcel.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class GreetingTransformer implements ClassFileTransformer {
    private String agentArgs;

    public GreetingTransformer(String agentArgs) {
        this.agentArgs = agentArgs;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (!className.equals("com/demo/App")) {
            return classfileBuffer;
        }
        try {
            JavaClass clazz = Repository.lookupClass(className);
            ClassGen cg = new ClassGen(clazz);
            ConstantPoolGen cp = cg.getConstantPool();
            for (Method method : clazz.getMethods()) {
                if (method.getName().equals("getGreeting")) {
                    MethodGen mg = new MethodGen(method, cg.getClassName(), cp);
                    InstructionList il = new InstructionList();
                    il.append(new PUSH(cp, this.agentArgs));
                    il.append(InstructionFactory.createReturn(Type.STRING));
                    mg.setInstructionList(il);
                    mg.setMaxStack();
                    mg.setMaxLocals();
                    cg.replaceMethod(method, mg.getMethod());
                }
            }
            return cg.getJavaClass().getBytes();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

启动后加载 agent

最早 JDK 1.5发布 java.lang.instrument 包时,agent 是必须在 JVM 启动时,通过命令行选项附着(attach)上去。但在 JVM 正常运行时,加载 agent 没有意义,只有出现问题,需要诊断才需要附着 agent。JDK 1.6 实现了 attach-on-demand(按需附着) JDK-[4882798 ],可以使用 Attach API 动态加载 agent [oracle blog, javadoc ]。这个 Attach API 在 tools.jar 中。JVM 启动时默认不加载这个 jar 包,需要在 classpath 中额外指定。使用 Attach API 动态加载 agent 的示例代码如下:

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

public class AgentLoader {

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.err.println("Usage: java -cp .:$JAVA_HOME/lib/tools.jar"
                    + " com.demo.AgentLoader <pid/name> <agent> [options]");
            System.exit(0);
        }

        String jvmPid = args[0];
        String agentJar = args[1];
        String options = args.length > 2 ? args[2] : null;
        for (VirtualMachineDescriptor jvm : VirtualMachine.list()) {
            if (jvm.displayName().contains(args[0])) {
                jvmPid = jvm.id();
                break;
            }
        }

        VirtualMachine jvm = VirtualMachine.attach(jvmPid);
        jvm.loadAgent(agentJar, options);
        jvm.detach();
    }
}

启动时加载 agent,-javaagent 传入的 jar 包需要在 MANIFEST.MF 中包含 Premain-Class 属性,此属性的值是 代理类 的名称,并且这个 代理类 要实现 premain 静态方法。启动后加载 agent 也是类似,通过 Agent-Class 属性指定 代理类代理类 要实现 agentemain 静态方法。agent 被加载后,JVM 将尝试调用 agentmain 方法。

上文提到每次定义新类(调用 ClassLoader.defineClass)时,都将调用该转换器的 transform 方法。对于已经定义加载的类,需要使用重定义类(调用 Instrumentation.redefineClass)或重转换类(调用 Instrumentation.retransformClass)。

// 注册提供的转换器。如果 canRetransform 为 true,那么重转换类时也将调用该转换器
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
// 使用提供的类文件重定义提供的类集。新的类文件字节,通过 ClassDefinition 传入
void redefineClasses(ClassDefinition... definitions)
                     throws ClassNotFoundException, UnmodifiableClassException
// 重转换提供的类集。对于每个添加时 canRetransform 设为 true 的转换器,在这些转换器中调用 transform 方法 
void retransformClasses(Class<?>... classes)
                        throws UnmodifiableClassException

重定义类(redefineClass)从 JDK 1.5 开始支持,而重转换类(retransformClass)是 JDK 1.6 引入。相对来说,重转换类能力更强,当存在多个转换器时,重转换将由 transform 调用链组成,而重定义类无法组成调用链。重定义类能实现的逻辑,重转换类同样能完成,所以保留重定义类方法(Instrumentation.redefineClass)可能只是为了向后兼容 [stackoverflow ]。

实现 agentmain 的示例代码如下,其中 GreetingTransformer 转换器的类定义和上文一样。

public class MyAgentMain {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new GreetingTransformer(agentArgs), true);
        try {
            Class clazz = Class.forName("com.demo.App");
            if (inst.isModifiableClass(clazz)) {
                inst.retransformClasses(clazz);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

MANIFEST.MF 文件配置:

jar {
    manifest {
        attributes 'Agent-Class': 'com.demo.MyAgentMain'
        attributes 'Can-Redefine-Classes' : true
        attributes 'Can-Retransform-Classes' : true
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

需要注意的是,和定义新类不同,重定义类和重转换类,可能会更改方法体、常量池和属性,但不得添加、移除、重命名字段或方法;不得更改方法签名、继承关系 [javadoc ]。这个限制将来可能会通过 “JEP 159: Enhanced Class Redefinition” 移除 [ref ]。

使用 Byte Buddy

Byte Buddy(home, github, javadoc),运行时的代码生成和操作库,2015 年获得 Oracle 官方 Duke's Choice award,提供高级别的创建和修改 Java 类文件的 API,使用这个库时,不需要了解字节码。另外,对 Java Agent 的开发 Byte Buddy 也有很好的支持 ,可以参考 Byte Buddy 作者 Rafael Winterhalter 写的介绍文章 [ref1, ref2 ]。

上文使用 BCEL 实现的 GreetingTransformer,现在改用 Byte Buddy,会变得非常简单。实现 premain 示例代码:

public static void premain(String agentArgs, Instrumentation inst) {
    new AgentBuilder.Default()
            .type(ElementMatchers.named("com.demo.App"))
            .transform(new AgentBuilder.Transformer() {
                @Override
                public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                        TypeDescription typeDescription,
                                                        ClassLoader classLoader,
                                                        JavaModule module) {
                    return builder.method(ElementMatchers.named("getGreeting"))
                            .intercept(FixedValue.value(agentArgs));
                }
            }).installOn(inst);
}

实现 agentmain

public static void agentmain(String agentArgs, Instrumentation inst) {
    new AgentBuilder.Default()
            .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
            .disableClassFormatChanges()
            .type(ElementMatchers.named("com.demo.App"))
            .transform(new AgentBuilder.Transformer() {
                @Override
                public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                        TypeDescription typeDescription,
                                                        ClassLoader classLoader,
                                                        JavaModule module) {
                    return builder.method(ElementMatchers.named("getGreeting"))
                            .intercept(FixedValue.value(agentArgs));
                }
            }).installOn(inst);
}

另外,Byte Buddy 对 Attach API 作了封装,屏蔽了对 tools.jar 的加载,可以直接使用 ByteBuddyAgent 类:

ByteBuddyAgent.attach(new File(agentJar), jvmPid, options);

附注:本文中提到的代码,可以在 github 上访问得到,javaagent-demo。

参考资料

  • 2016-08 Java 5 特性 Instrumentation 实践 https://www.ibm.com/developer...
  • 2007-05 Java SE 6 新特性:Instrumentation 新功能 https://www.ibm.com/developer...
  • 2017-08 The Attach API https://blogs.oracle.com/core...
  • JPLIS: Java programming language agents need instrumentation support (JSR-163) https://bugs.openjdk.java.net...
  • need an attach mechanism https://bugs.openjdk.java.net...
  • Difference between redefine and retransform in javaagent https://stackoverflow.com/q/1...
  • 2016-02 Rafael Winterhalter:通过使用Byte Buddy,便捷地创建Java Agent http://www.infoq.com/cn/artic...
  • 2017-01 Rafael Winterhalter: Fixing Bugs in Running Java Code with Dynamic Attach https://www.sitepoint.com/fix...
  • 2014-08 Making Java more dynamic: runtime code generation for the JVM https://www.slideshare.net/Ra...
  • 2015-09 JVM源码分析之javaagent原理完全解读 http://www.infoq.com/cn/artic...

相关文章:

  • mysql如何直接查出从1开始递增的数
  • GlassFish新纪元
  • 基于树莓派的桌上足球计分器
  • C# 高级编程03----细节内容
  • mongodb之 oplog 日志详解
  • 动态库空间优化
  • 贝叶斯分类器
  • 【完整教程】新版直播频道上线,马上开始创建你的直播吧!
  • 如何用三个月学会python?
  • JDK11的工具的命令参考
  • MySQL缓存及变量
  • MySQL基础之 索引
  • 2.2 目录及文本文件操作命令
  • 等等!这两个 Spring-RabbitMQ 的坑我们已经替你踩了
  • 内存对齐
  • [case10]使用RSQL实现端到端的动态查询
  • 【347天】每日项目总结系列085(2018.01.18)
  • 【React系列】如何构建React应用程序
  • ES6 学习笔记(一)let,const和解构赋值
  • IDEA常用插件整理
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • Laravel核心解读--Facades
  • php的插入排序,通过双层for循环
  • 从伪并行的 Python 多线程说起
  • 思考 CSS 架构
  • 小程序滚动组件,左边导航栏与右边内容联动效果实现
  • ​iOS实时查看App运行日志
  • (ZT)出版业改革:该死的死,该生的生
  • (超简单)构建高可用网络应用:使用Nginx进行负载均衡与健康检查
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (区间dp) (经典例题) 石子合并
  • (一)Linux+Windows下安装ffmpeg
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (转)Linq学习笔记
  • (转)程序员疫苗:代码注入
  • (转)四层和七层负载均衡的区别
  • ./configure,make,make install的作用
  • .net 4.0发布后不能正常显示图片问题
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .Net Core和.Net Standard直观理解
  • .NET Framework与.NET Framework SDK有什么不同?
  • .NET 指南:抽象化实现的基类
  • .net和jar包windows服务部署
  • .Net组件程序设计之线程、并发管理(一)
  • /var/log/cvslog 太大
  • @manytomany 保存后数据被删除_[Windows] 数据恢复软件RStudio v8.14.179675 便携特别版...
  • [ Algorithm ] N次方算法 N Square 动态规划解决
  • [BZOJ1010] [HNOI2008] 玩具装箱toy (斜率优化)
  • [C/C++]_[初级]_[关于编译时出现有符号-无符号不匹配的警告-sizeof使用注意事项]
  • [C++]unordered系列关联式容器
  • [Codeforces] combinatorics (R1600) Part.2
  • [CVPR 2023:3D Gaussian Splatting:实时的神经场渲染]
  • [daily][archlinux][game] 几个linux下还不错的游戏
  • [Dxperience.8.*]报表预览控件PrintControl设置
  • [GDMEC-无人机遥感研究小组]无人机遥感小组-000-数据集制备