<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-jar-pluginartifactId>
<version>2.6version>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>com.warrenyoung.instrumentions.agent.AgentMainAgent-Class>
<Can-Retransform-Classes>trueCan-Retransform-Classes>
manifestEntries>
archive>
configuration>
plugin>
plugins>
build>
// AgentMain.class
public class AgentMain {
public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new TransformerTest(), true);
inst.retransformClasses(InstrumentTestClass.class);
System.out.println("Agent Main Done");
}
}
// TransformerTest.class
public class TransformerTest implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!className.equals("com/warrenyoung/instrumentions/instrumentiondest/InstrumentTestClass"))
{
System.out.println(className);
return classfileBuffer;
}
try {
System.out.println("x:" + className);
ClassPool classPool = new ClassPool();
classPool.appendClassPath(new LoaderClassPath(loader));
final CtClass ctClass;
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(classfileBuffer)) {
ctClass = classPool.makeClass(byteArrayInputStream);
}
ctClass.getDeclaredMethod("getStr").setBody("return \"b\";");
ctClass.getDeclaredMethod("getNum").setBody("return 2;");
System.out.println("transformed");
return ctClass.toBytecode();
} catch (Exception e)
{
e.printStackTrace();
}
return classfileBuffer;
}
}
// 用于执行注入动作的app2
public class Main {
private static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
public static void main(String[] args)
{
// 这里其实不需要循环,单次注入,被注入JVM终生受影响,除非其他行为又触发了class被重新加载
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
run1();
}
}, 2000, 20000000, TimeUnit.MILLISECONDS);
}
public static void run1() {
List<VirtualMachineDescriptor> listAfter = null;
try {
int count = 0;
listAfter = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : listAfter) {
if (vmd.displayName().equals("com.warrenyoung.instrumentions.instrumentiondest.Main"))
{
VirtualMachine vm = VirtualMachine.attach(vmd);
vm.loadAgent("/Users/warrenyoung/develops/instrumentiontest/agenttest/target/agenttest-1.0-SNAPSHOT.jar");
vm.detach();
}
System.out.println(vmd.displayName() + ", " + vmd.id());
}
} catch (Exception e) {
}
}
}
IDE启动的时候会使用 -javaagent参数,如
/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/bin/java “-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=55984:/Applications/IntelliJ IDEA.app/Contents/bin” …com.warrenyoung.instrumentions.instrumentiondest.Main
JVMTI是JVM Tool Interface,是JVM开放的一些供开发者扩展的native编程接口,基于事件驱动,开发者一般设置一些callback接口,对应事件(如:虚拟机初始化、开始运行、结束,类的加载,方法出入,线程始末等等)被触发时,callback会被调用。
JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式),也可以在 Java 5 之后使用运行时加载(活动加载模式)。
-agentlib:agent-lib-name=options
-agentpath:path-to-agent=options
Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期,在这个时间点上:
JVMTI Agent可以实现三个方法
JVMTI的一种典型应用是Java调试功能,这主要包括了设置断点、调试(step)等,在 JVMTI 里面,设置断点的 API 本身很简单:
jvmtiError SetBreakpoint(jvmtiEnv* env, jmethodID method, jlocation location)
我们常用的Java Agent是基于一个叫做Instrument的JVMTI Agent实现的,Instrument Agent实现了OnLoad, OnAttach方法。
Instrumentation的redefineClasses和retransformClasses功能相似,redefineClasses是Java5引入的,retransformClasses是Java6引入的。
在Instrumentation.addTransformer,添加ClassFileTransformer,在以下三种情形下ClassFileTransformer.transform会被执行
所以java agent的两种工作模式下,
premain
中显式调用retransformClasses/redefineClasses
,因为premain
方法是在所有用户类被加载之前执行的可能
需要在agentmain
中显式调用retransformClasses/redefineClasses
,因为attach的时候用户类可能
已经被加载过了在第一种方式下,由于java agent premain
方法是在所有用户类被加载之前执行的,transform
前后的类的结构可以完全不同;第二种方式下,由于java agent agentmain
方法执行的时候,部分类已经被加载过了,如果需要重新加载已加载的类,为了保证transform
之后的类仍然可用,要求新的类格式与老的类格式兼容,因为transform
只是更新了类里内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新带来的开销。限制如下:
类重新定义,这是Instrumentation提供的基础功能之一,主要用在已经被加载过的类上,想对其进行修改,通过InstrumentationImpl的下面的redefineClasses方法去操作了:
public void redefineClasses(ClassDefinition[] definitions) throws ClassNotFoundException {
if (!isRedefineClassesSupported()) {
throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
}
if (definitions == null) {
throw new NullPointerException("null passed as 'definitions' in redefineClasses");
}
for (int i = 0; i < definitions.length; ++i) {
if (definitions[i] == null) {
throw new NullPointerException("element of 'definitions' is null in redefineClasses");
}
}
if (definitions.length == 0) {
return; // short-circuit if there are no changes requested
}
redefineClasses0(mNativeAgent, definitions);
}
在JVM里对应的实现是创建一个VM_RedefineClasses
的VM_Operation
,注意执行它的时候会stop the world的:
jvmtiError
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
VMThread::execute(&op);
return (op.check_error());
} /* end RedefineClasses */
这个过程我尽量用语言来描述清楚,不详细贴代码了,因为代码量实在有点大:
上面是基本的过程,总的来说就是只更新了类里内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新带来的开销。
Java 5就提供了Class Redifine的能力,而Java 6才支持Class Restransform,可以认为Restransform是Redifine的一种升级版本,更加方便使用,两者能实现的功能是一致的,只是调用方式有些区别。
Instrumentation.retransform过程中会对每个class逐个调用使用Instrumentation.addTransformer添加的ClassFileTransformer的transform方法,多个ClassFileTransformer之间的变更具备传递性。
public void retransformClasses(Class>[] classes) {
if (!isRetransformClassesSupported()) {
throw new UnsupportedOperationException(
"retransformClasses is not supported in this environment");
}
retransformClasses0(mNativeAgent, classes);
}