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.0Premain-Class: com.github.godshang.agent.AgentAgent-Class: com.github.godshang.agent.AgentCan-Redefine-Classes: trueCan-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/...