一. 前提
目前主流的APM
开源框架如Pinpoint
、SkyWalking
等等都是通过java.lang.instrument
包提供的字节码增强功能来实现的。实际上,我们日常应用的各种工具中,有很多都是基于他们实现的,例如常见的热部署(JRebel, spring-loaded)、各种线上诊断工具(btrace, Greys)、代码覆盖率工具(JaCoCo)等等
利用 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了 一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。
在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地 在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。
另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。
最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。
二. 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的入口)。
简单的来看,如果需要通过Instrumentation操作或监控一个Java程序,相关的工具和流程如下:
下文会依次介绍图中的相关概念,并谈谈原理和具体的应用场景。
2.1 instrument原理
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。
事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。
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的形式实现。
不过上述代码并没有给出transform方法的具体实现,我们举一个具体场景细化一下这个方法的实现:例如,我想要监听某个类,并对这个类的每个方法都做一层AOP,打印出方法调用的耗时。那么使用Instrumentation的解决方式,就是修改这个类的字节码,对每个方法作如下改动:
// 原方法
publicvoidmethod1{
dosomething;
}
↓ ↓ ↓ ↓ ↓
// 修改后的方法
publicvoidmethod1{
longstime = System.currentTimeMillis;
dosomething;
System.out.println( "method1 cost:"+ (System.currentTimeMillis - stime) + " ms");
}
要想实现这种效果,我们需要在transform方法的实现中,对指定的类,做指定的字节码增强。通常来说,做字节码增强都需要使用到框架,比如ASM,CGLIB,Byte Buddy,Javassist。不过如果你喜欢,你可以直接用位运算操作byte[],不需要任何框架,例如JDK反射(method.invoke)的实现,就真的是用位操作拼装了一个类。
言归正传,操作字节码的高手可能更喜欢ASM,因为它提供的方法更底层,功能更强大更直白。对于字节码不熟悉的开发者,更适合javassist,它可以直接以Java代码方式直接修改方法体。我们以javassist为例,看看怎么实现上述的功能,完整代码如下:
public class InstrumentationExample {
// Java agent指定的premain方法,会在main方法之前被调用
public static void premain(String args, Instrumentation inst){
// Instrumentation提供的addTransformer方法,在类加载时会回调ClassFileTransformer接口
inst.addTransformer( newClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if(! "com/test/TestClass".equals(className)) {
// 只修改指定的Class
return classfileBuffer;
}
byte[] transformed = null;
CtClass cl = null;
try{
// CtClass、ClassPool、CtMethod、ExprEditor都是javassist提供的字节码操作的类
ClassPool pool = ClassPool.getDefault;
cl = pool.makeClass( newByteArrayInputStream(classfileBuffer));
CtMethod[] methods = cl.getDeclaredMethods;
for( inti = 0; i < methods.length; i++) {
methods[i].instrument( newExprEditor {
@Override
public void edit(MethodCall m) throws CannotCompileException {
// 把方法体直接替换掉,其中 $proceed($$);是javassist的语法,用来表示原方法体的调用
m.replace( "{ long stime = System.currentTimeMillis;"+ " $_ = $proceed($$);"
+ "System.out.println(""+ m.getClassName + "."+ m.getMethodName
+ " cost:" + (System.currentTimeMillis - stime) + " ms"); }");
}
});
}
// javassist会把输入的Java代码再编译成字节码byte[]
transformed = cl.toBytecode;
} catch(Exception e) {
e.printStackTrace;
} finally {
if(cl != null) {
cl.detach; // ClassPool默认不会回收,需要手动清理
}
}
returntransformed;
}
});
}
}
三. 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接口的实例:
- JVM在指定代理的方式下启动,此时Instrumentation实例会传递到代理类的premain方法。
- 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
事件,而这个事件是在读取字节码文件之后回调时用。换言之,premain和agentmain
方式的回调时机都是类文件字节码读取之后(或者说是类加载之后)。
实际上,premain
和agentmain
两种方式最终的目的都是为了回调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
。- agentmain是依靠attach api, Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序
4.1 premain使用方式
premain
方式依赖独立的javaagent
,也就是单独建立一个项目编写好代码之后打成jar包供另一个使用程序通过代理形式引入。简单的步骤如下:
①编写premain函数,也就是编写一个普通的Java类,包含下面两个方法的其中之一。
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
②通过指定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() {
@Override
public 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-Projectsinstrument-samplepremain-agenttargetpremain-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包的类,当然这里仅仅作为演示所以无伤大雅。
4.2 agentmain使用方式
agentmain
的使用方式和permain
十分相似,包括编写MANIFEST.MF
和生成代理Jar包。但是,它并不需要通过-javaagent
命令行形式引入代理Jar,而是在运行时通过attach工具激活指定代理即可。简单的步骤如下:
①编写premain
函数,也就是编写一个普通的Java类,包含下面两个方法的其中之一。
public static void agentmain(String agentArgs, Instrumentation inst); [1]
public static void agentmain(String agentArgs); [2]
①的回调优先级会比②高,也就是[1]和[2]同时存在的情况下,只有①会被回调。而agentArgs
是agentmain
函数得到的程序参数,通过com.sun.tools.attach.VirtualMachine#loadAgent(var1,var2)
中的var2传入,var1就是代理Jar的绝对路径。
②代理服务打包为Jar。
Agent一般是一个普通的Java服务,只是需要编写agentmain函数,并且该Jar包的manifest(也就是MANIFEST.MF文件)属性中需要加入Agent-Class来指定步骤1中编写好agentmain函数的那个Java类。
③通过attach工具直接加载Agent,执行attach的程序和需要被代理的程序可以是两个完全不同的程序。Attach API可不仅仅是为了实现动态加载agent,Attach API其实是跨JVM进程通讯的工具,能够将某种指令从一个JVM进程发送给另一个JVM进程。
加载agent只是Attach API发送的各种指令中的一种, 诸如jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能,都属于Attach API可以发送的指令。下面简单介绍下
按惯例,以Hotspot虚拟机,Linux系统为例。当external process执行VirtualMachine.attach时,需要通过操作系统提供的进程通信方法,例如信号、socket,进行握手和通信。其具体内部实现流程如下所示:
上面提到了两个文件:
- .attach_pidXXX 后面的XXX代表pid,例如pid为1234则文件名为.attach_pid1234。该文件目的是给目标JVM一个标记,表示触发SIGQUIT信号的是attach请求。这样目标JVM才可以把SIGQUIT信号当做attach连接请求,再来做初始化。其默认全路径为/proc/XXX/cwd/.attach_pidXXX,若创建失败则使用/tmp/attach_pidXXX
- .java_pidXXX 后面的XXX代表pid,例如pid为1234则文件名为.java_pid1234。由于Unix domain socket通讯是基于文件的,该文件就是表示external process与target VM进行socket通信所使用的文件,如果存在说明目标JVM已经做好连接准备。其默认全路径为/proc/XXX/cwd/.java_pidXXX,若创建失败则使用/tmp/java_pidXXX
VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。
// 列出所有VM实例
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// attach目标VM
VirtualMachine.attach(descriptor.id());
// 目标VM加载Agent
VirtualMachine#loadAgent("代理Jar路径","命令参数");
VirtualMachine等相关Class位于JDK的tools.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() {
@Override
public 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.permain.PermainAgent</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
目录下即可。
4.3 本地方法Instrument
在 JDK 1.5 版本的 instumentation 里,并没有对Java本地方法(Native Method)的处理方式,而且在 Java 标准的 JVMTI 之下,并没有办法改变 method signature, 这就使替换本地方法非常地困难。一个比较直接而简单的想法是,在启动时替换本地代码所在的动态链接库—— 但是这样,本质上是一种静态的替换,而不是动态的 Instrumentation。而且,这样可能需要编译较大数量的动态链接库 —— 比如,我们有三个本地函数,假设每一个都需要一个替换,而在不同的应用之下,可能需要不同的组合,那么如果我们把三个函数都编译在同一个动态链接库之中,最多我们需要 8 个不同的动态链接库来满足需要。当然,我们也可以独立地编译之,那样也需要 6 个动态链接库——无论如何,这种繁琐的方式是不可接受的。
在 Java SE 6 中,新的 Native Instrumentation 提出了一个新的 native code 的解析方式,作为原有的 native method 的解析方式的一个补充,来很好地解决了一些问题。这就是在新版本的 java.lang.instrument 包里,我们拥有了对 native 代码的 instrument 方式 —— 设置 prefix。
假设我们有了一个 native 函数,名字叫 nativeMethod,在运行过程中,我们需要将它指向另外一个函数(需要注意的是,在当前标准的 JVMTI 之下,除了 native 函数名,其他的 signature 需要一致)。比如我们的 Java 代码是:
很有趣不是吗?因此如果我们要做类似的工作,一个很好的建议是首先在 Java 中写一个带 prefix 的 native 接口,用 javah 工具生成一个 c 的 header-file,看看它实际解析得到的函数名是什么,这样我们就可以避免一些不必要的麻烦。
另外一个事实是,与我们的想像不同,对于两个或者两个以上的 prefix,虚拟机并不做更多的解析;它不会试图去掉某一个 prefix,再来组装函数接口。它做且仅作两次解析。
总之,新的 native 的 prefix-instrumentation 的方式,改变了以前 Java 中 native 代码无法动态改变的缺点。在当前,利用 JNI 来写 native 代码也是 Java 应用中非常重要的一个环节,因此它的动态化意味着整个 Java 都可以动态改变了 —— 现在我们的代码可以利用加上 prefix 来动态改变 native 函数的指向,正如上面所说的,如果找不到,虚拟机还会去尝试做标准的解析,这让我们拥有了动态地替换 native 代码的方式,我们可以将许多带不同 prefix 的函数编译在一个动态链接库之中,而通过 instrument 包的功能,让 native 函数和 Java 函数一样动态改变、动态替换。 当然,现在的 native 的 instrumentation 还有一些限制条件,比如,不同的 transformer 会有自己的 native prefix,就是说,每一个 transformer 会负责他所替换的所有类而不是特定类的 prefix —— 因此这个粒度可能不够精确。
4.4 BootClassPath / SystemClassPath 的动态增补
我们知道,通过设置系统参数或者通过虚拟机启动参数,我们可以设置一个虚拟机运行时的 boot class 加载路径(-Xbootclasspath)和 system class(-cp)加载路径。当然,我们在运行之后无法替换它。然而,我们也许有时候要需要把某些 jar 加载到 bootclasspath 之中,而我们无法应用上述两个方法;或者我们需要在虚拟机启动之后来加载某些 jar 进入 bootclasspath。在 Java SE 6 之中,我们可以做到这一点了。
实现这几点很简单,首先,我们依然需要确认虚拟机已经支持这个功能,然后在 premain/agantmain 之中加上需要的 classpath。我们可以在我们的 Transformer 里使用 appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch来完成这个任务。
同时我们可以注意到,在 agent 的 mainfest 里加入 Boot-Class-Path 其实一样可以在动态地载入 agent 的同时加入自己的 boot class 路径,当然,在 Java code 中它可以更加动态方便和智能地完成 —— 我们可以很方便地加入判断和选择成分。
在这里我们也需要注意几点:
-
首先,我们加入到 classpath 的 jar 文件中不应当带有任何和系统的 instrumentation 有关的系统同名类,不然,一切都陷入不可预料之中 —— 这不是一个工程师想要得到的结果,不是吗?
-
其次,我们要注意到虚拟机的 ClassLoader 的工作方式,它会记录解析结果。比如,我们曾经要求读入某个类 someclass,但是失败了,ClassLoader 会记得这一点。即使我们在后面动态地加入了某一个 jar,含有这个类,ClassLoader 依然会认为我们无法解析这个类,与上次出错的相同的错误会被报告。
-
再次我们知道在 Java 语言中有一个系统参数“java.class.path”,这个 property 里面记录了我们当前的 classpath,但是,我们使用这两个函数,虽然真正地改变了实际的 classpath,却不会对这个 property 本身产生任何影响。
在公开的 JavaDoc 中我们可以发现一个很有意思的事情,Sun 的设计师们告诉我们,这个功能事实上依赖于 ClassLoader 的 appendtoClassPathForInstrumentation 方法 —— 这是一个非公开的函数,因此我们不建议直接(使用反射等方式)使用它,事实上,instrument 包里的这两个函数已经可以很好的解决我们的问题了。
五. Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:
- premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
- 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新类和老类的父类必须相同。
- 新类和老类实现的接口数也要相同,并且是相同的接口。
- 新类和老类访问符必须一致。
- 新类和老类字段数和字段名要一致。
- 新类和老类新增或删除的方法必须是private static/final修饰的。
- 可以修改方法体。
除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。
六. 小结
本文仅仅简单分析instrument
的原理和基本使用,可以体会到instrument
让Java具有了更强的动态控制、解释能力,从而让Java语言变得更加灵活多变。在JDK1.6之后,使用Instrumentation
,开发者可以构建一个独立于应用程序的代理程序,用来监测和协助运行在JVM上的程序,可以远程重新转换指定JVM实例里面的已经加载的类,这一点实现从开发者角度来看就像是从JVM级别支持了AOP编程。
本文参考和引用:
链接:https://www.jianshu.com/p/5c62b71fd882
Java Instrument 功能使用及原理_chuixue24的博客-CSDN博客
仅作个人学习和记录使用