Instrumentation
Instrumentation
是 JDK 1.5 的一个新个性,通过 java.lang.instrument
包能够实现一个独立于应用程序的 Agent 程序,可能替换和批改类的定义。有了这样的性能,开发者能够实现灵便的运行时虚拟机监控和 Java 类加强,实际上提供了一种虚拟机级别的 AOP 实现形式。
Java Agent Demo
上面介绍通过 java.lang.instrument
编写 Agent 的个别办法。
实现 Agent 启动办法
Java Agent 反对指标 JVM 启动时加载,也反对在指标 JVM 运行时加载,这两种不同的加载模式会应用不同的入口函数,如果须要在指标 JVM 启动的同时加载 Agent,那么能够抉择实现上面的办法:
[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);
JVM 将首先寻找[1],如果没有发现[1],再寻找[2]。
如果心愿在指标 JVM 运行时加载 Agent,则须要实现上面的办法:
[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);
这两组办法的第一个参数 agentArgs
是伴随 –javaagent
一起传入的程序参数,如果这个字符串代表了多个参数,就须要本人解析这些参数。instrumentation
是 Instrumentation
类型的对象,是 JVM 主动传入的,咱们能够拿这个参数进行类加强等操作。
public class Agent {public static void premain(String agentArgs, Instrumentation instrumentation) {System.out.println("Agent premain start ...");
System.out.println("Agent args:" + agentArgs);
instrumentation.addTransformer(new AppInitTramsformer());
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {System.out.println("Agent agentmain start ...");
System.out.println("Agent args:" + agentArgs);
instrumentation.addTransformer(new AppInitTramsformer());
}
}
实现 Transformer
在 Instrumentation
接口中,通过 addTransformer
办法来减少一个类转换器,类转换器由类 ClassFileTransformer
接口实现。ClassFileTransformer
接口中惟一的办法 transform
用于实现类转换,当类被加载的时候,就会调用 transform
办法,进行类转换。在运行时,咱们能够通过 Instrumentation
的redefineClasses
办法进行类重定义。实现时留神不要减少、删除或者重命名字段和办法,扭转办法的签名或者类的继承关系。
批改字节码能够借助 ASM
、javassist
、bytebuddy
等工具。下例中应用了javassist
。
public class AppInitTramsformer implements ClassFileTransformer {
private static final String INJECTED_CLASS = "com.github.godshang.agent.Agent";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {String realClassName = className.replace("/", ".");
if (realClassName.equals(INJECTED_CLASS)) {System.out.println("拦挡到的类名:" + realClassName);
CtClass ctClass;
try {ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod method : declaredMethods) {System.out.println(method.getName() + "办法被拦挡");
method.addLocalVariable("time", CtClass.longType);
method.insertBefore("System.out.println(\"--- 开始执行 ---\");");
method.insertBefore("time = System.currentTimeMillis();");
method.insertAfter("System.out.println(\"--- 完结执行 ---\");");
method.insertAfter("System.out.println(\" 运行耗时: \"+ (System.currentTimeMillis() - time));");
}
return ctClass.toBytecode();} catch (Throwable e) {System.out.println(e.getMessage());
e.printStackTrace();}
}
return new byte[0];
}
}
MANIFEST.MF 文件
编写的 Agent 如何被内部应用程序通晓呢?依附的是 MANIFEST.MF
文件。文件的具体门路是:src/main/resources/META-INF/MANIFEST.MF
。
Manifest-Version: 1.0
Premain-Class: com.github.godshang.agent.Agent
Agent-Class: com.github.godshang.agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
MANIFEST.MF
文件 Premain-Class
对应 premain
入口即启动时加载 Agent,Agent-Class
对应 agentmain
入口即运行时加载 Agent。Can-Redefine-Classes
示意是否能重定义此代理所需的类,默认值是 false。Can-Retransform-Classes
示意是否能重转换此代理所需的类,默认值是 false。
除了间接创立 MANIFEST.MF
文件,也能够在 Maven 中配置编译打包插件。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!-- 主动增加 META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.github.godshang.agent.Agent</Premain-Class>
<Agent-Class>com.github.godshang.agent.Agent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
加载 Agent
如果是启动时进行动态加载,须要在启动参数中减少 -javaagent
参数:
public class AppMain {
/**
* -javaagent:E:\github\java-agent\agent\target\agent-1.0-SNAPSHOT.jar=hello
* @param args
*/
public static void main(String[] args) {AppInit appInit = new AppInit();
appInit.init();}
}
如果是运行时进行动静加载,可参考如下代码:
public class AttachMain {public static void main(String[] args) {for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("AttachMain")) {
try {VirtualMachine vm = VirtualMachine.attach(vmd.id());
vm.loadAgent("E:\\github\\java-agent\\agent\\target\\agent-1.0-SNAPSHOT.jar=hello");
vm.detach();} catch (Exception e) {e.printStackTrace();
}
}
}
AppInit appInit = new AppInit();
appInit.init();}
}
Java Agent 原理
Instrumentation
的底层实现 依赖于JVMTI
,也就是JVM Tool Interface
。
JVMTI
JVMTI(JVM Tool Interface)
是 Java 虚构计对外提供的 Native 编程接口,是 JVM 裸露进去给用户扩大应用的接口汇合。通过 JVMTI
内部过程能够获取到运行时 JVM 的诸多信息,比方线程、GC 等。JVMTI
是基于事件驱动的,JVM 每执行肯定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户能够自行扩大实现本人的逻辑。
JVMTI Agent
实现了 JVMTI
的客户端程序称之为 agent,它其实就是利用 JVMTI
裸露进去的接口实现用户自行的逻辑。在 JVMTI Agent
中次要有三个办法:
Agent_OnLoad
办法,如果 agent 在启动时加载,就执行这个办法Agent_OnAttach
办法,如果 agent 不是在启动的时候加载的,是咱们先 attach 到指标线程上,而后对对应的指标过程发送 load 命令来加载 agent,在加载过程中调用 Agent_OnAttach 函数Agent_OnUnload
办法,在 agent 做卸载掉时候调用
如何应用 C ++ 开发 Agent,能够参考这篇文章。
Instrument
JVMTI
是一套 Native 接口,在 Java SE 5 之前,要实现一个 Agent 只能通过编写 Native 代码来实现。从 Java SE 5 开始,能够应用 Java 的 Instrumentation
接口来编写 Agent。无论是通过 Native 的形式还是通过 Java Instrumentation 接口的形式来编写 Agent,它们的工作都是借助 JVMTI 来进行实现。
JPLISAgent
全名是 Java Programming Language Instrumentation Services Agent
,它是一种非凡的JVMTI Agent
,作用是初始化所有通过Instrumentation
接口编写的 Agent,并且也承当着通过 JVMTI
实现 Instrumentation
中裸露 API 的责任。
Reference
- https://docs.oracle.com/javas…
- https://cloud.tencent.com/dev…
- https://xz.aliyun.com/t/10186
- https://juejin.cn/post/708602…
- https://jueee.github.io/2020/…
- https://www.overops.com/blog/…