转自:https://www.throwable.club/2019/06/29/java-understand-instrument-first/

前提

很早之前就了解到目前主流的APM开源框架如PinpointSkyWalking等等都是通过java.lang.instrument包提供的字节码增强功能来实现的。趁着对这块的热情还没消退,抽时间分析一下java.lang.instrument包的使用方式,记录下来写成一个系列的文章。本系列博文针对的是JDK11,其他版本的JDK可能不适合。

instrument简介

java.lang.instrument包的结构如下:

java.lang.instrument- ClassDefinition- ClassFileTransformer- IllegalClassFormatException- Instrumentation- UnmodifiableClassException- UnmodifiableModuleException

其中,核心功能由接口java.lang.instrument.Instrumentation提供,这里可以通过Instrumentation类的API注释来理解一下什么是instrument

Instrumentation类提供控制Java语言程序代码的服务。Instrumentation可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定工具的目的。由于插入的字节码是附加的,这些更变不会修改原来程序的状态或者行为。通过这种方式实现的良性工具包括监控代理、分析器、覆盖分析程序和事件日志记录程序等等。

也就是说,java.lang.instrument包的最大功能就是可以在已有的类上附加(修改)字节码来实现增强的逻辑,如果良性使用当然不会影响程序的正常行为,如果恶性使用就可能产生一些负面的影响(其实很多商用Java程序如IntelliJ IDEA的License的破解都可以基于Instrumentation的功能实现,前提是找到程序认证License的入口)。

instrument原理

instrument的底层实现依赖于JVMTI,也就是JVM Tool Interface,它是JVM暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。而instrument agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是专门为java语言编写的插桩服务提供支持的代理。因为涉及到源码分析,笔者暂时没能力展开,可以详细阅读参考资料中你假笨大神的那篇专门分析JVM相关源码实现的文章。

其中,VM启动时加载Agent可以使用命令行参数-javaagent:yourAgent.jar的形式实现。

Instrumentation接口详解

  • void addTransformer(ClassFileTransformer transformer, boolean canRetransform)

注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer实例,相当于它们通过了redefineClasses方法进行重定义。布尔值参数canRetransform决定这里被重定义的类是否能够通过retransformClasses方法进行回滚。

  • void addTransformer(ClassFileTransformer transformer)

相当于addTransformer(transformer, false),也就是通过ClassFileTransformer实例重定义的类不能进行回滚。

  • boolean removeTransformer(ClassFileTransformer transformer)

移除(反注册)ClassFileTransformer实例。

  • boolean isRetransformClassesSupported()

返回当前JVM配置是否支持类重新转换的特性。

  • void retransformClasses(Class<?>... classes) throws UnmodifiableClassException

已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer的列表中进行处理,想深入理解建议阅读API注释。

  • boolean isRedefineClassesSupported()

返回当前JVM配置是否支持重定义类(修改类的字节码)的特性。

  • void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException

重定义类,也就是对已经加载的类进行重定义,ClassDefinition类型的入参包括了对应的类型Class<?>对象和字节码文件对应的字节数组。

其他功能:

  • boolean isModifiableClass(Class<?> theClass):判断对应类是否被修改过。
  • Class[] getAllLoadedClasses():获取所有已经被加载的类。
  • Class[] getInitiatedClasses(ClassLoader loader):获取所有已经被初始化过了的类。
  • long getObjectSize(Object objectToSize):获取某个对象的(字节)大小,注意嵌套对象或者对象中的属性引用需要另外单独计算。
  • void appendToBootstrapClassLoaderSearch(JarFile jarfile):将某个jar加入到Bootstrap Classpath里优先其他jar被加载。
  • void appendToSystemClassLoaderSearch(JarFile jarfile):将某个jar加入到Classpath里供AppClassloard去加载。
  • void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix):设置某些native方法的前缀,主要在找native方法的时候做规则匹配。
  • boolean isNativeMethodPrefixSupported():是否支持设置native方法的前缀。
  • void redefineModule(...):重定义Module
  • boolean isModifiableModule(Module module):判断指定Module是否重定义过。

如何使用Instrumentation

Instrumentation类在API注释中有十分简洁的使用方式描述:

有两种方式可以获取Instrumentation接口的实例:

  1. JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
  2. JVM提供一种在启动之后的某个时刻启动代理的机制,此时Instrumentation实例会传递到代理类代码的agentmain方法。

首先我们知道Instrumentation的实现类是sun.instrument.InstrumentationImpl,在JDK9之后,由于模块权限控制,不可能通过反射构造其实例,一般情况下反射做不到的东西只能通过JVM实现。而且根据上面简洁的API注释我们是无法得知如何使用Instrumentation

其实,premain对应的就是VM启动时的Instrument Agent加载,也就是上文提到的agent on load

agentmain对应的是VM运行时的Instrument Agent加载,也就是上文提到的agent on attach

两种加载形式所加载的Instrument Agent都关注同一个JVMTI事件 – ClassFileLoadHook事件,而这个事件是在读取字节码文件之后回调时用。

换言之,premainagentmain方式的回调时机都是类文件字节码读取之后(或者说是类加载之后)

实际上,premainagentmain两种方式最终的目的都是为了回调Instrumentation实例并且激活sun.instrument.InstrumentationImpl#transform()从而回调注册到Instrumentation中的ClassFileTransformer实现字节码修改,本质功能上没有很大区别。两者的非本质功能的区别如下:

  • premain需要通过命令行使用外部代理jar包;而agentmain则可以通过attach机制直接附着到目标VM中加载代理,也就是使用agentmain方式下,操作attach的程序和被代理的程序可以是完全不同的两个程序。
  • premain方式回调到ClassFileTransformer中的类是虚拟机加载的所有类,这个是由于代理加载的顺序比较靠前决定的,在开发者逻辑看来就是:所有类首次加载并且进入程序main()方法之前,premain方法会被激活,然后所有被加载的类都会执行ClassFileTransformer列表中的回调。
  • agentmain方式由于是采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调。
  • premain方式是JDK1.5引入的,而agentmain方式是JDK1.6引入的,也就是JDK1.6之后可以自行选择使用premain或者agentmain

premain使用方式

premain方式依赖独立的javaagent,也就是单独建立一个项目编写好代码之后打成jar包供另一个使用程序通过代理形式引入。简单的步骤如下:

1.编写premain函数

也就是编写一个普通的Java类,包含下面两个方法的其中之一

public static void premain(String agentArgs, Instrumentation inst);  [1]
public static void premain(String agentArgs); [2]

[1]的回调优先级会比[2]高,也就是[1]和[2]同时存在的情况下,只有[1]会被回调。而agentArgspremain函数得到的程序参数,通过– javaagent命令行参数传入。

2.代理服务打包为Jar

Agent一般是一个普通的Java服务,只是需要编写premain函数,并且该Jar包的manifest(也就是MANIFEST.MF文件)属性中需要加入Premain-Class来指定步骤1中编写好premain函数的那个Java类。

3.通过指定Agent运行

java -javaagent:代理Jar包的路径 [=传入premain的参数] yourTarget.jar

简单例子如下:

新建一个premain-agent的项目,新建一个类club.throwable.permain.PermainAgent如下:

public class PermainAgent {private static Instrumentation INST;public static void premain(String agentArgs, Instrumentation inst) {INST = inst;process();}private static void process() {INST.addTransformer(new ClassFileTransformer() {@Overridepublic byte[] transform(ClassLoader loader, String className,Class<?> clazz,ProtectionDomain protectionDomain,byte[] byteCode) throws IllegalClassFormatException {System.out.println(String.format("Process by ClassFileTransformer,target class = %s", className));return byteCode;}});}
}

引入Maven插件maven-jar-plugin

<plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.1.2</version><configuration><archive><manifestEntries><Premain-Class>club.throwable.permain.PermainAgent</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin>
</plugins>

通过mvn package命令打包即可得到premain-agent.jar(笔者发现该插件未支持JDK11,所以降级到JDK8)。

接着可以使用该代理Jar:

// 这个是样品类
public class HelloSample {public void sayHello(String name) {System.out.println(String.format("%s say hello!", name));}
}// main函数,vm参数:-javaagent:I:\J-Projects\instrument-sample\premain-agent\target\premain-agent.jar
public class PermainMain {public static void main(String[] args) throws Exception{}
}
// 输出结果
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders$1
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders$Cache
Process by ClassFileTransformer,target class = sun/nio/cs/ThreadLocalCoders$2
Process by ClassFileTransformer,target class = com/intellij/rt/execution/application/AppMainV2$Agent
Process by ClassFileTransformer,target class = com/intellij/rt/execution/application/AppMainV2
Process by ClassFileTransformer,target class = com/intellij/rt/execution/application/AppMainV2$1
Process by ClassFileTransformer,target class = java/lang/reflect/InvocationTargetException
Process by ClassFileTransformer,target class = java/net/InetAddress$1
Process by ClassFileTransformer,target class = java/lang/ClassValue
// ... 省略大量其他输出

实际上,如果我们要定制功能需要排除掉一些java.lang包和sun包的类,当然这里仅仅作为演示所以无伤大雅。

agentmain使用方式

agentmain的使用方式和permain十分相似,包括编写MANIFEST.MF和生成代理Jar包。但是,它并不需要通过-javaagent命令行形式引入代理Jar,而是在运行时通过attach工具激活指定代理即可。简单的步骤如下:

1.编写premain函数

也就是编写一个普通的Java类,包含下面两个方法的其中之一

public static void agentmain(String agentArgs, Instrumentation inst);  [1]
public static void agentmain(String agentArgs); [2]

[1]的回调优先级会比[2]高,也就是[1]和[2]同时存在的情况下,只有[1]会被回调。而agentArgsagentmain函数得到的程序参数,通过com.sun.tools.attach.VirtualMachine#loadAgent(var1,var2)中的var2传入,var1就是代理Jar的绝对路径。

2.代理服务打包为Jar

Agent一般是一个普通的Java服务,只是需要编写agentmain函数,并且该Jar包的manifest(也就是MANIFEST.MF文件)属性中需要加入Agent-Class来指定步骤1中编写好agentmain函数的那个Java类。

3.通过attach工具直接加载Agent

执行attach的程序和需要被代理的程序可以是两个完全不同的程序

// 列出所有VM实例
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// attach目标VM
VirtualMachine.attach(descriptor.id());
// 目标VM加载Agent
VirtualMachine#loadAgent("代理Jar路径","命令参数");

 举个简单的例子:

编写agentmain函数的类如下:

public class AgentmainAgent {private static Instrumentation INST;public static void agentmain(String agentArgs, Instrumentation inst) {INST = inst;process();}private static void process() {INST.addTransformer(new ClassFileTransformer() {@Overridepublic byte[] transform(ClassLoader loader, String className,Class<?> clazz,ProtectionDomain protectionDomain,byte[] byteCode) throws IllegalClassFormatException {System.out.println(String.format("Agentmain process by ClassFileTransformer,target class = %s", className));return byteCode;}}, true);try {INST.retransformClasses(Class.forName("club.throwable.instrument.AgentTargetSample"));} catch (Exception e) {e.printStackTrace();}}
}

更改Maven插件maven-jar-plugin的配置,然后通过mvn pacakge打包:

<plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.1.2</version><configuration><archive><manifestEntries><!-- 主要改这个配置项 --><Agent-Class>club.throwable.agentmain.AgentmainAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin>
</plugins>

负责attach工作的程序AgentmainAttachMain

public class AgentmainAttachMain {public static void main(String[] args) throws Exception {List<VirtualMachineDescriptor> list = VirtualMachine.list();for (VirtualMachineDescriptor descriptor : list) {if (descriptor.displayName().endsWith("AgentTargetSample")) {VirtualMachine virtualMachine = VirtualMachine.attach(descriptor.id());virtualMachine.loadAgent("I:\\J-Projects\\instrument-sample\\premain-agent\\target\\premain-agent.jar", "arg1");virtualMachine.detach();}}}
}

被代理的目标程序AgentTargetSample

public class AgentTargetSample {public void sayHello(String name) {System.out.println(String.format("%s say hello!", name));}public static void main(String[] args) throws Exception {AgentTargetSample sample = new AgentTargetSample();for (; ; ) {Thread.sleep(1000);sample.sayHello(Thread.currentThread().getName());}}
}

接着先启动AgentTargetSample,然后再启动AgentmainAttachMain:

main say hello!
main say hello!
main say hello!
main say hello!
main say hello!
main say hello!
main say hello!
Agentmain process by ClassFileTransformer,target class = club/throwable/instrument/AgentTargetSample
main say hello!
main say hello!
main say hello!

PS:如果没有找到VirtualMachineDescriptor或者VirtualMachine,只需要把${JAVA_HONE}/lib/tools.jar拷贝到${JAVA_HONE}/jre/lib目录下即可。

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:

  • premainagentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
  • 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
    • 新类和老类的父类必须相同。
    • 新类和老类实现的接口数也要相同,并且是相同的接口。
    • 新类和老类访问符必须一致。
    • 新类和老类字段数和字段名要一致。
    • 新类和老类新增或删除的方法必须是private static/final修饰的。
    • 可以修改方法体。

除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。

小结

本文仅仅简单分析instrument的原理和基本使用,可以体会到instrument让Java具有了更强的动态控制、解释能力,从而让Java语言变得更加灵活多变。在JDK1.6之后,使用Instrumentation,开发者可以构建一个独立于应用程序的代理程序,用来监测和协助运行在JVM上的程序,可以远程重新转换指定JVM实例里面的已经加载的类,这一点实现从开发者角度来看就像是从JVM级别支持了AOP编程。下一篇文章将会结合实际的场景和字节码改造进行更深入的探究。

参考资料:

  • JVM源码分析之javaagent原理完全解读 - By你假笨
  • JDK11相关源码
  • 动手写一个javaagent

【转】深入理解Instrument相关推荐

  1. Prometheus监控的最佳实践——关于监控的3项关键指标

    本文来自Weaveworks的工程师Anita Burhrle在Rancher Labs与Weaveworks联合举办的Online Meetup上的技术分享.在此次分享中,嘉宾们讨论了如何使用Ran ...

  2. 字节码插桩之Java Agent

    字节码插桩之Java Agent 本篇文章将详细讲解有关Java Agent的知识,揭开它神秘的面纱,帮助开发人员了解它的黑魔法,帮助我们完成更多业务需求 What is Java Agent Jav ...

  3. java.lang.Instrument 动态修改替换类代码

    java.lang.Instrument 动态修改替换类代码 | java.lang.Instrument包是在JDK5引入的,程序员通过修改方法的字节码实现动态修改类代码. 这通常是在类的main方 ...

  4. Python自然语言处理学习笔记(7):1.5 自动理解自然语言

    Updated log 1st:2011/8/5 1.5 Automatic Natural Language Understanding 自然语言的自动理解 We have been explori ...

  5. java.lang.Instrument 代理Agent使用

    原文出处: 陶邦仁 java.lang.Instrument包是在JDK5引入的,程序员通过修改方法的字节码实现动态修改类代码.这通常是在类的main方法调用之前进行预处理的操作,通过java指定该类 ...

  6. iPhone开发进阶(1) --- 深入理解iPhone OS/SDK与Objective-C 2.0

    iPhone开发进阶(1) --- 深入理解iPhone OS/SDK与Objective-C 2.0 工欲善其事,必先利其器.在开发iPhone应用程序的时候,深入理解iPhone OS/SDK与O ...

  7. 深入理解GCD之dispatch_queue

    原文链接深入理解GCD之dispatch_queue 前言 上一篇我们介绍了GCD的结构体,这一篇我们着重看一下GCD中队列的构成.队列是我们在使用GCD中经常接触的技术点. 关键点 主队列和主线程 ...

  8. Linux内核调试原理和工具介绍--理解静态插装/动态插装、tracepoint、ftrace、kprobe、SystemTap、Perf、eBPF

    可以将linux跟踪系统分成Tracer(跟踪数据来自哪里),数据收集分析(如"ftrace")和跟踪前端(更方便的用户态工具). 1. 数据源(Tracers) printk 是 ...

  9. spring(7)---深入理解Spring核心技术——Spring中的各模块详解

    深入理解Spring核心技术--Spring中的各模块详解 Spring框架的两个基本概念IOC容器和AOP,相信大家现在对Spring中的这两个部分的基本概念有了一定的认识,好了,那么今天我们就来正 ...

  10. Linux内核深入理解系统调用(3):open 系统调用实现以及资源限制(setrlimit/getrlimit/prlimit)

    Linux内核深入理解系统调用(3) open 系统调用实现以及资源限制(setrlimit/getrlimit/prlimit) rtoax 2021年3月 对原文进行了5.10.13的代码分析. ...

最新文章

  1. MinMaxScaler.fit 归一化数据的方法
  2. 不喜欢SAP GUI?那试试用Eclipse进行ABAP开发吧
  3. 文轩在线:如何让IT部门成为企业的价值中心
  4. 什么是 WMI?From MSDN
  5. DNS篇之二DNS记录类型
  6. Unity3D图像后处理特效——Crease
  7. 【无人机系统】四轴飞行器及其UAV飞控系统 - 桂林电子科技大学信息科技学院 电子工程系(四 五 )
  8. 外贸软件常见图片类问题丨汇信
  9. 小米HTML查看器记住密码,小米路由器3管理密码_默认密码是多少?-192路由网
  10. 自动化运维工具——Ansible
  11. iOS15.4 Beta4 新测试版推送,新增反跟踪功能
  12. 金地农村土地承包经营权证打证系统
  13. matlab能否独立做程序,如何将MATLAB程序编译成独立可执行的程序
  14. mingw-w64-install.exe
  15. bitset的使用示例
  16. javascript事件轮询(event loop)详解
  17. 怎么在IOS上阅读txt小说,小说阅读器推荐
  18. 【Linux】lftp客户端使用详解
  19. 拼多多主站频道推广接口/限时秒杀/充值中心/百亿补贴/领券中心
  20. 基于ISA/TMG的双线接入

热门文章

  1. linux pt 客户端,下载工具系列——rTorrent (轻量级优秀BT/PT客户端)
  2. HDLBits练习——Exams/ece241 2013 q7
  3. 为防泄密 新加坡政府将断掉公务员的网络连接
  4. 使用和风天气接口获取天气信息
  5. secureCRT注册码
  6. 第十二章 非编码RNA与复杂疾病
  7. 小课堂week17 编程范式巡礼第二季 并发那些事
  8. 。新浪搜狐 博客无间道 按摩乳原创
  9. Tafel 曲线绘制
  10. 计算机司法鉴定的程序,计算机司法鉴定的流程说明