1、ClassFileTransformer
java
/**
* 参数:
* loader - 正在加载类的类加载器
* className - 正在加载的类的名称
* classBeingRedefined -重定义的类
* protectionDomain-要定义或重定义的类的保护域
* classfileBuffer-类文件格式的输入字节缓冲区(不得修改)
*/
1 public interface ClassFileTransformer
2 {
3
4 byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
5 ProtectionDomain protectionDomain, byte[] classfileBuffer)
6 throws IllegalClassFormatException;
7 }
2、Instrumentation
java
1 public interface Instrumentation {
2
3 //添加ClassFileTransformer
4 void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
5
6 //添加ClassFileTransformer
7 void addTransformer(ClassFileTransformer transformer);
8
9 //移除ClassFileTransformer
10 boolean removeTransformer(ClassFileTransformer transformer);
11
12 //是否可以被重新定义
13 boolean isRetransformClassesSupported();
14
15 //重新定义Class文件
16 void redefineClasses(ClassDefinition... definitions)
17 throws ClassNotFoundException, UnmodifiableClassException;
18
19 //是否可以修改Class文件
20 boolean isModifiableClass(Class<?> theClass);
21
22 //获取所有加载的Class
23 @SuppressWarnings("rawtypes")
24 Class[] getAllLoadedClasses();
25
26 //获取指定类加载器已经初始化的类
27 @SuppressWarnings("rawtypes")
28 Class[] getInitiatedClasses(ClassLoader loader);
29
30 //获取某个对象的大小
31 long getObjectSize(Object objectToSize);
32
33 //添加指定jar包到启动类加载器检索路径
34 void appendToBootstrapClassLoaderSearch(JarFile jarfile);
35
36 //添加指定jar包到系统类加载检索路径
37 void appendToSystemClassLoaderSearch(JarFile jarfile);
38
39 //本地方法是否支持前缀
40 boolean isNativeMethodPrefixSupported();
41
42 //设置本地方法前缀,一般用于按前缀做匹配操作
43 void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
44 }
3、 java.lang.instrument简介
-
Java5之后,增加了一个包java.lang.instrument,这个包的东西很少,两个接口,ClassFileTransformer和Instrumentation,一个类ClassDefinition,还有两个Exception:IllegalClassFormatException和UnmodifiableClassException;
-
Instrument的最大作用,就是类定义的动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
-
一个代理就是部署一个Jar文件。Jar文件的manifest文件(每个jar包根目录下有个META-INF文件夹,此文件夹下的MANIFEST.MF文件就是这里的manifest文件)需要指定这个代理的类,指定之后,这个类将被加载以启动代理。因为支持命令行接口,所以一个代理可以通过在命令行中指定一个选项来移动。具体的实现也可能支持在虚拟机启动之后的某个时间开启代理。例如,一个具体的实现可能提供一个机制,使一个工具附加到一个运行的应用中,然后,初始化这个工具的代理,加载到正在运行的应用中。关于类的加载如何被初始化的细节,依赖于具体的实现。
-
在用命令行接口的实现中,一个代理可以通过增加下面的Option到命令行中启动:
java
-javaagent:jarpath[=options]
jarpath是代理jar文件的路径。option是代理的选项。上边的命令可以在同一个命令行中使用多次,因此,可以创建多个代理。多个代理可以使用同一jarpath。一个代理Jar文件必须符合Jar文件规范。
- 一个代理jar文件的manifest必须包含Premain-Class属性。这个属性的值是代理类的名字。代理类必须实现一个public static的premain 方法,与应用程序中的main方法作为程序入口的原理类似。JVM初始化之后,每一个premain方法将被以代理被指定时的顺序调用,然后应用程序真正的main方法将被调用。每一个premain方法必须以启动的顺序返回结果。(premain=pre-main)
java
JVM首先尝试调用代理类中下面的方法:
public static void premain(String agentArgs, Instrumentation inst);
如果代理类没有实现这个方法,jvm将尝试调用
public static void premain(String agentArgs);
- 一个代理具体的实现可能提供一个在JVM启动之后启动的机制。关于代理被初始化的细节是具体实现特定的,但是通常应用程序已经启动,应用程序的main方法已经被调用。在代理的实现支持在JVM启动之后启动的情况下,需要遵循下面的规则:
代理的Jar文件中的manifest文件必须包含Agent-Class属性。这个属性的值是代理类的名字。
代理类必须实现一个public static agentmain方法
系统类加载器( ClassLoader.getSystemClassLoader)必须支持增加一个代理Jar文件到系统类路径。
代理的Jar文件被追加到系统类路径中。这个类加载器是通常用来加载包含应用程序main方法的类的类加载器。代理类被加载之后,JVM尝试调用agentmain方法。
java
JVM首先尝试调用代理类中的下面这个方法:
public static void agentmain(String agentArgs, Instrumentation inst);
如果代理类中没有实现这个方法,然后JVM尝试调用:
public static void agentmain(String agentArgs);
这个代理类可能也有一个premain方法,当代理在命令行选项中启动时使用;当这个代理类在JVM启动之后启动时,premain方法不会被调用。
- manifest属性在一个代理的jar文件中被定义:
yaml
Premain-Class
当一个代理在JVM启动时被指定时,这个属性指定代理使用的类。也就是,这个类包含premain方法。当一个代理在JVM启动时被指定时,这个属性是必须的。如果没有配置这个属性,JVM将会abort。注意:这是一个类的名字,不是一个文件名活路径。
Agent-Class
如果一个代理的实现支持在JVM启动之后的某个时间启动,这个属性指定一个代理类。也就是说,这个类要包含agentmain方法。这个属性是必须的,如果不配置的话,代理将不会被启动。注意:这是一个类的名字,不是一个文件名字,也不是文件路径。
Boot-Class-Path
这个代理的实现中用到的第三方jar包都放到这里边。
Can-Redefine-Classes
Boolean(true或false)。Agent是否可以重定义类。可选的,默认是false。
Can-Retransform-Classes
Boolean(true或false)。Agent是否可以将类变回原形。可选的,默认是false。
Can-Set-Native-Method-Prefix
Boolean(true或false)。Agent是否可以设置本地方法的前缀。可选的,默认是false。
- 一个代理Jar文件中的manifest文件可能同时包含Premain-Class和Agent-Class属性。当代理在命令行中用-javaagent选项被启动,使用Premain-Class属性指定的代理类的名字,然后Agent-Class属性的配置被忽略。相似地,如果代理在JVM启动之后的某个时间启动,使用Agent-Class属性指定的类的名字,Premain-Class属性被忽略。
4、 示例
(1) 启动时加载
Agent类
java
public class MyAgent {
public static void premain(String args,Instrumentation inst)
{
System.out.println("premain方法会在main方法之前执行......");
inst.addTransformer(new MyTransformClass());
}
}
ClassFileTransformer类
java
public class MyTransformClass implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException{
// 编译后的字符流数组
byte[] newClassFileBuffer = new byte[classfileBuffer.length];
// 重定义指定类
String transClassName = "com.App";
if (className.equals(transClassName)){
System.out.println("监控到目标类");
// 对byte[]重新编译可以使用第三方工具如javassist
newClassFileBuffer = classfileBuffer;
}
return newClassFileBuffer;
}
}
MANIFEST.MF文件
yaml
Manifest-Version: 1.0
Premain-Class:com.test.MyAgent
Boot-Class-Path: javassist.jar
打jar包
yaml
jar cvfm MyAgent.jar MANIFEST.MF javassist.jar com
将MANIFEST.MF文件,javassist.jar,com文件夹下的类打成jar包
编写测试类
java
public class App {
public static void main(String[] args) {
System.out.println("HelloWorld!!");
}
}
执行测试
yaml
java -javaagent:MyAgent.jar App
(2)运行时加载
Agent类
java
public class MyAgent {
public static void agentmain(String args, Instrumentation inst) {
Class[] classes = inst.getAllLoadedClasses();
for(Class cls :classes){
System.out.println(cls.getName());
}
}
}
MANIFEST.MF文件
yaml
Manifest-Version: 1.0
Agent-Class::com.test.MyAgent
Boot-Class-Path: javassist.jar
打jar包
yaml
jar cvfm MyAgent.jar MANIFEST.MF javassist.jar com
将MANIFEST.MF文件,javassist.jar,com文件夹下的类打成jar包
编写测试类,用jps命令可以方便的查看进程的ID,名字是应用程序main方法所在的类的名字。
java
public class TargetVM {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(1000);
}
}
}
执行测试
java
public class Test {
public static void main(String[] args) throws AttachNotSupportedException,
IOException, AgentLoadException, AgentInitializationException {
VirtualMachine vm = VirtualMachine.attach(args[0]);
vm.loadAgent("MyAgent.jar");
}
}