2019独角兽企业重金招聘Python工程师标准>>>
1 Instrumentation简介
使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
Instrumentation是从Java 5后存在的重要特性,Java 6又对其多了一系列优化。详见 http://www.ibm.com/developerworks/cn/java/j-lo-jse6/
在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。
在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。
Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。
在实际中,这两种方式现在都被广泛使用甚至同时存在于一个项目中。下面我们就来具体看看这两种方式的具体做法。
2 Instrumentation的基本用法
2.1 Premain方式
Premain即为Java 5中的在运行前设置代理类的方式。
我们可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent
参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
简单步骤
步骤一:创建Premain类,编写premain函数
编写一个 Java 类,包含如下两个方法当中的任何一个
代码块
Plain Text
public static void premain(String agentArgs, Instrumentation inst); //优先级更高,若两者都存在,只会执行该方法
public static void premain(String agentArgs);
-
agentArgs 是 premain 函数得到的程序参数,随同 “
– javaagent
”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。 -
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等
在这个 premain 函数中,开发者可以进行对类的各种操作。
步骤二:添加manifest
在src/main/resources/META-INF中创建MANIFEST.MF文件,属性当中加入” Premain-Class”来指定步骤 1 当中编写的那个带有 premain 的 Java 类。
代码块
Plain Text
Manifest-Version: 1.0
Premain-Class: Premain
步骤三:打jar包
如果是用maven管理的工程,打包前可以加入maven-jar-plugin插件,以便用-jar来执行。并且可以控制相应的manifest文件和main函数
代码块
Plain Text
org.apache.maven.plugins
maven-jar-plugin
3.0.2
src/main/resources/META-INF/MANIFEST.MF
true
UserClassMain
步骤四:启动
代码块
Plain Text
java -javaagent:java-agent-demo-1.0-SNAPSHOT.jar -jar java-agent-demo-1.0-SNAPSHOT.jar
如果要传入参数agentArgs,则如下:
代码块
Plain Text
java -javaagent:java-agent-demo-1.0-SNAPSHOT.jar=hello,world -jar java-agent-demo-1.0-SNAPSHOT.jar
注意,路径后直接跟=号,后面参数不能有空格。
代码示例
代码块
Plain Text
/*用户的类*/
public class UserClass {
public int getNum() {
return 1;
}
}
/*用户的main函数所在类*/
public class UserClassMain {
public static void main(String[] args) {
System.out.println("Execute User Class Main Method! result="+new UserClass().getNum());
}
}
/*Premain类*/
public class Premain {
public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
System.out.println("agentArgs=" + agentArgs);
}
}
执行结果
2.2 Agentmain方式
Premain只能在main函数执行前进行处理,Agentmain则可以在程序启动后再运行。这就使得我们给程序增加监控的时候可以不用停止应用。
Agentmain和Premain的方式较相似,区别有三处:
1、Agentmain类
需要用Agentmain类替代之前的Premain类,Agentmain类也必须还有一个agentmain的函数:
代码块
Plain Text
public static void agentmain(String agentArgs, Instrumentation inst); //优先级更高,若两者都存在,只会执行该方法
public static void agentmain(String agentArgs);
2、添加manifest
类似的,添加manifest文件,属性当中加入” Agent-Class””来指定步骤 1 当中编写的那个带有agentmain的Java类。
代码块
Plain Text
Manifest-Version: 1.0
Agent-Class: Agentmain
3、启动
要启动agentmain,就需要引入一个新的包com.sun.tools.attach。Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。
Attach API 很简单,只有 2 个主要的类:
-
VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ;
-
VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
以下的例子表示在启动当前jvm中插入一个agent。
代码块
Plain Text
public class AgentLoader {
private static final String jarFilePath = "/Users/jiangxu/work/git/java-agent/target/java-agent-demo-1.0-SNAPSHOT.jar";
public static void loadAgent() {
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
int p = nameOfRunningVM.indexOf('@');
String pid = nameOfRunningVM.substring(0, p);
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(jarFilePath, "");
vm.detach();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
AgentLoader.loadAgent();
}
}
3 Instrumentation进阶方法
Premain和Agentmain提供了两种方式以供我们提前或者在JVM运行时进行一些操作,但是如何对JVM中的类进行监控甚至修改,首先就需要用到两个类:ClassFileTransformer和ClassDefinition。
3.1 ClassFileTransformer
ClassFileTransformer是一个接口,该接口有一个方法:
代码块
Plain Text
byte[] transform( ClassLoader loader,
String className,
Class> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException;
3.2 ClassDefinition