摘要
JavaAgent就是Java探针,是一个JVM插件,常用于代码热更新,AOP,JVM监控等功能。这个技术对大多数的同学来说可能有点陌生,但是对Java软件开发人员来说肯定都多多少少接触过,只是相对其原理,我们更多的关注在以它为核心的工具使用上。例如常见的热部署(JRebel, spring-loaded)、各种线上诊断工具(btrace, 阿里的Arthas)、代码覆盖率工具(JaCoCo),如果这些你都没使用过的话,那还有一个工具作为开发人员你一定使用过,就是开发工具IDEA的Debug功能,破解的IDEA在idea.vmoptions里也可以看到-javaagent的配置。 另外现在主流的APM(Application Performance Management应用性能管理)工具如OneAPM、OpenTelemetry等也是基于Agent来实现的。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能。
**关键字:**Instrumentation、JVMTI、JavaAgent
在命令行输入 java
可以看到相应的参数,其中有几个和 java agent相关的,要了解怎么使用就要先认识Instrumentation和JVMTI。
-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
加载 Java 编程语言代理, 请参阅 java.lang.instrument
一、认识Instrumentation
rt.jar
中定义了一个包,这个包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。该路径下有两个重要的类:Instrumentation和ClassFileTransformer。
Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等。从 JDK 5 开始,利用JVM提供的 Instrumentation API ,开发者可以构建一个独立于应用程序的代理程序(Agent),使得开发者可以通过Java语言来操作和监控JVM内部的一些状态和监测运行在JVM上的程序,用来对Java程序实现监控分析,甚至能够在对业务代码无侵入的情况下实现对字节码的修改,完成替换和修改某些类的定义,实现一些特殊功能(如AOP、热部署)。在 JDK6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。 Instrumentation的一些主要方法如下:
public interface Instrumentation {
/**
* 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
* Transformer可以直接对类的字节码byte[]进行修改
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
}
其中最常用的方法就是addTransformer(ClassFileTransformer transformer)了,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:
/**
* 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]
* 返回值为需要被修改后的字节码byte[]
*/
public interface ClassFileTransformer {
byte[]
transform( ClassLoader loader,//要转换的类的定义加载程序
String className,//Java虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称
Class<?> classBeingRedefined,//如果这是由重定义或重传触发的,则被重定义或重传的类;如果这是类加载,则为null
ProtectionDomain protectionDomain,//正在定义或重新定义的类的保护域
byte[] classfileBuffer//类格式的输入字节缓冲区——不得修改
) throws IllegalClassFormatException;
}
addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
通过Instrumentation操作或监控一个Java程序的流程如下:
二、认识JVMTI
“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的native编程接口,完成很多跟 JVM 相关的功能。开发者可以通过JVMTI向JVM监控状态、执行指令,其目的是开放出一套JVM接口用于 profile、debug、监控、线程分析、代码覆盖分析等工具。
JVMTI和Instumentation API的作用很相似,都是一套JVM操作和监控的接口,且都需要通过agent来启动:
- Instumentation API需要打包成jar,并通过Java agent加载(-javaagent)
- JVMTI需要打包成动态链接库(随操作系统,如.dll/.so文件),并通过JVMTI agent加载(-agentlib/-agentpath)
既然都是agent,那么加载时机也同样有两种:启动时(Agent_OnLoad)和运行时Attach(Agent_OnAttach)。不过相比于Instumentation API,JVMTI的功能强大的多。它是实现Java调试器,以及其它Java运行态测试与分析工具的基础。JVMTI能做的事情包括:
- 获取所有线程、查看线程状态、线程调用栈、查看线程组、中断线程、查看线程持有和等待的锁、获取线程的CPU时间、甚至将一个运行中的方法强制返回值……
- 获取Class、Method、Field的各种信息,类的详细信息、方法体的字节码和行号、向Bootstrap/System Class Loader添加jar、修改System Property……
- 堆内存的遍历和对象获取、获取局部变量的值、监测成员变量的值……
- 各种事件的callback函数,事件包括:类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、gc开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出……
- 设置与取消断点、监听断点进入事件、单步执行事件……
前面说的Instumentation API也是基于JVMTI来实现的,具体以addTransformer来说,通过Instrumentation注册的ClassFileTransformer,实际上是注册了JVMTI针对类文件加载事件(ClassFileLoadHook)的callback函数。这个callback函数长这个样子:
void JNICALL
ClassFileLoadHook(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protection_domain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data)
参数class_data和new_class_data分别对应了读入的原字节码数组,和提供的修改后的字节码数组的指针。这样,我们在方法的实现中就可以把修改后的类的字节码写回,实现 bytecode instrumentation。
InstumentationImpl的实现中,在这个callback函数里,对ClassFileTransformer的transform方法再进行一次回调。这样的一次封装,就做到了通过Java语言实现字节码拦截修改的能力。
三、认识JavaAgent
Java agent技术的实现是基于Java Instrumentation技术。利用Instrumentation技术,开发者可以构建独立于应用的java agent(代理)程序, 用来监控运行于JVM之上的程序,甚至可以动态修改和替换类的定义。从而在不修改原有应用程序的前提下,动态改变应用程序的行为。
Java agent是一种特殊的Java程序(Jar文件),它是Instrumentation的客户端。与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互。Java agent既可以在VM启动时加载,也可以在VM启动后加载,以jar包的形式部署在JVM中,jar文件的manifest需要指定agent的类名。根据不同的启动时机,agent类需要实现不同的方法(二选一):
1、启动时加载(Java1.5开始提供):
通过vm的启动参数-javaagent:**.jar来启动。premain 方 法 用 于 在 启 动 时 ,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
/**
* 以vm参数的形式载入,在程序main方法执行之前执行
* 其jar包的manifest需要配置属性Premain-Class
* JVM 会优先加载 带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。Instrumentation是一个重要的参数。
*/
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs)
对于VM启动时加载的Java agent,premain 方法仅限于应用程序的启动时,即在程序main方法执行之前被调用,此时大部分Java类都没有被加载(“大部分”是因为,agent类本身和它依赖的类还是无法避免的会先加载的,而这些类使用 premain 方法是无法实现字节码改写的。),这时是一个对类加载埋点做手脚(addTransformer)的好机会。如果此时premain方法执行失败或抛出异常,那么JVM的启动会被终止。agent加载时,Java agent的jar包先会被加入到system class path中,然后agent的类会被system class loader加载,这个system class loader就是所在的Java程序的class loader,这样agent就可以很容易的获取到想要的class。Java agent premain的执行流程如下:
2、启动后加载(Jdk1.6增加):
在vm启动后的任何时间点,通过Attach API,agentmain 可以在 main 函数开始运行之后再运行,调用java进程,将自己编写的agentmain 注入目标完成对程序的监控,修改。被代理的目标程序VM有可能很早之前已经启动,其所有类已经被加载完成,这个时候需要借助Instrumentation#retransformClasses(Class<?>… classes)让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调。
/**
* 以Attach的方式载入,在Java程序启动后执行
* 其jar包的manifest需要配置属性Agent-Class
*/
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain (String agentArgs)
对于VM启动后加载的Java agent,Instrumentation 会通过 agentmain 方法传入程序。agentmain 方法在 main 函数开始运行后才被调用,其最大优势是可以在程序运行期间进行字节码的替换。如果agentmain执行失败或抛出异常,JVM会忽略掉错误,不会影响到正在running的Java程序。Attach API实现动态注入的原理是应用程序通过虚拟机提供的 attach(pid 进程id)方法,可以将代理程序连接(attach)到一个运行中的 Java 进程上,之后便可以通过 loadAgent(AgentJarPath)将 Agent 的 jar 包注入对应的进程,然后对应的进程会调用 agentmain 方法(开源的Java诊断工具BTrace和Alibaba的Arthas也是基于此),如下图所示。
premain的探针使用方式比较局限,而且每次探针更改的时候,都需要重新启动应用,而agentmain的探针程序就可以直接连接到已经启动的 jvm 中。可以实现例如动态替换类,查看加载类信息的一些功能。虽然premain 方法与 agentmain 方法相比有很大的局限性,但是目前主流的基于探针的监控系统都是基于这种方式实现的对应用的无侵入监控,比如java领域的APM(Application Performance Management应用性能管理)工具,这是因为premain方式也有自己的优势:
- premain探针:除了名称以外,可以更改任意内容,名称改了,ClassLoad 就会出问题
- agentmain探针:不能修改Class的文件结构,即不能添加方法,不能添加字段,只能修改方法体的内容,否则就会报UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)类似的异常
四、JavaAgent实现方式
要想使用JavaAgent完成我们的需求,我们需要在transform方法的实现中,对指定的类,做指定的字节码增强。通常来说,做字节码增强都需要使用到一些操作字节码的框架,比如ASM,CGLIB,Byte Buddy,Javassist。当然你非常厉害的话也可以直接用位运算操作byte[],不需要任何框架,例如JDK反射(method.invoke())的实现,用位操作拼装出一个类。
@Override
public byte[] transform(ClassLoader loader,
//要转换的类的定义加载程序,
String className,
//Java虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。
// 例如,“java/util/List”。
Class<?> classBeingRedefined,
//如果这是由重定义或重传触发的,则被重定义或重传的类;如果这是类加载,则为null
ProtectionDomain protectionDomain,
//正在定义或重新定义的类的保护域
byte[] classfileBuffer
//类格式的输入字节缓冲区——不得修改
) throws IllegalClassFormatException {
ClassReader classReader = new ClassReader(classfileBuffer);
PreClassVisitor preClassVisitor = new PreClassVisitor( new ClassWriter(ClassWriter.COMPUTE_MAXS));
classReader.accept(preClassVisitor,0);
return preClassVisitor.toByteArray();
}
相对来说,操作字节码的高手可能更喜欢ASM,因为它提供的方法更底层,功能更强大更直白。对于字节码不熟悉的开发者,更适合javassist,它可以直接以Java代码方式直接修改方法体。下面分别举例使用Javassist结合premain方式完成对方法的耗时计算,使用ASM结合agentmain attach 完成对方法中数据的更换。
(一)premain和Javassist
1、新建maven项目做测试
package org.example;
/**
* 测试服务App,简单的启动hello方法
*/
public class App {
public static void main(String[] args) throws InterruptedException {
hello();
Thread.sleep((long) (Math.random() * 10));//随机暂停0-10ms
}
public static void hello() throws InterruptedException {
System.out.println("The number six is 6");
Thread.sleep((long) (Math.random() * 100));//随机暂停0-100ms
}
}
2、新建一个agent项目,没有main方法,只有premain方法
package org.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
/**
* 探针类
*/
public class PerfMonAgent {
static private Instrumentation inst = null;
/**
* 在App服务main方法调用之前会先调用此方法
* 与main方法运行在同一个JVM中,并被同一个System ClassLoader装载
**/
public static void premain(String agentArgs, Instrumentation _inst) {
System.out.println("org.example.PerfMonAgent.premain() was called.");
inst = _inst;
ClassFileTransformer trans = new PerfMonXformer();
System.out.println("Adding a org.example.PerfMonXformer instance to the JVM.");
inst.addTransformer(trans);
}
}
package org.example;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
/**
* javassist 改造类
*/
public class PerfMonXformer implements ClassFileTransformer {
final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
//java自带的方法不进行处理
if (!className.endsWith("App")) {
return null;
}
className = className.replace("/", ".");
CtClass ctclass = null;
try {
ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
for (CtMethod ctMethod : ctclass.getDeclaredMethods()) {
String methodName = ctMethod.getName();
String newMethodName = methodName + "$old";// 新定义一个方法叫做比如sayHello$old
ctMethod.setName(newMethodName);// 将原来的方法名字修改
// 创建新的方法,复制原来的方法,名字为原来的名字
CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);
// 构建新的方法体
StringBuilder bodyStr = new StringBuilder();
bodyStr.append("{");
bodyStr.append("System.out.println(\"==============Enter Method: " + className +
"." + methodName + " ==============\");");
bodyStr.append(prefix);
bodyStr.append(newMethodName + "($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数
bodyStr.append(postfix);
bodyStr.append("System.out.println(\"==============Exit Method: " + className +
"." + methodName + " Cost:\" +(endTime - startTime) +\"ms " + "===\");");
bodyStr.append("}");
newMethod.setBody(bodyStr.toString());// 替换新方法
ctclass.addMethod(newMethod);// 增加新方法
}
return ctclass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
3、pom文件,除了要引入javassist依赖,最主要的是要配置以下内容
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>org.example.PerfMonAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
这个maven配置在打包时默认会在打包的文件中生成一个MANIFREST.MF文件,在该文件中主要定义了程序运行相关的配置信息,程序运行前会先检测该文件中的配置项:
Manifest-Version: 1.0
Can-Redefine-Classes: true//true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true//true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class: org.example.PerfMonAgent //包含 premain 方法的类(类的全路径名)
MANIFREST.MF也可以手动在 resources 目录下新建目录:META-INF,在该目录下新建文件:MANIFREST.MF,手动编写以上配置,除了这些还有一些关于项目信息的配置。
4、将agent项目打包,然后在idea里添加VM启动参数,启动App的main方法
-javaagent: 后面是agent项目的包路径:
-javaagent:/Users/gaoruiqiang/DataPhant/agentdemo/target/agentdemo.jar
整体为:
java -javaagent:/Users/gaoruiqiang/DataPhant/agentdemo/target/agentdemo.jar -jar app.jar
一个java程序中-javaagent
参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的java agent会按照你定义的顺序执行,例如:
java -javaagent:agent1.jar -javaagent:agent2.jar -jar app.jar
程序执行的顺序将会是:
Agent1.premain -> Agent2.premain -> App.main
App启动后控制台打印出了以下信息,可以看到在方法的前后都有打印信息,最后计算出了方法的耗时,这就是使用premain实现了AOP。
(二)agentmain和ASM
1、新建个maven项目做测试
package org.example;
import java.util.concurrent.TimeUnit;
/**
* 对foo方法做修改
*/
public class MyTestMain {
public static void main(String[] args) throws InterruptedException {
while (true) {
System.out.println(foo());
TimeUnit.SECONDS.sleep(3);
}
}
public static int foo() {
return 100; // 修改后 return 50;
}
}
2、跟premain函数一样,编写一个含有agentmain函数的 Java 类:
package org.example;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.AdviceAdapter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import static org.objectweb.asm.Opcodes.ASM7;
/**
* agentmain方法
*/
public class AgentMain {
//AgentMain运行的JVM链接到MyTestMain运行的JVM后,就可以通过此方法做字节码增强
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
System.out.println("agentmain called");
inst.addTransformer(new MyClassFileTransformer(), true);
Class classes[] = inst.getAllLoadedClasses();
for (int i = 0; i < classes.length; i++) {
if (classes[i].getName().equals("org.example.MyTestMain")) {
System.out.println("Reloading: " + classes[i].getName());
inst.retransformClasses(classes[i]);
break;
}
}
}
public static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
//匹配类名后缀
if (!className.endsWith("MyTestMain")) return bytes;
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new MyClassVisitor(cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
}
//操作类
public static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor classVisitor) {
super(ASM7, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// 只转换 foo 方法
if ("foo".equals(name)) {
return new MyMethodVisitor(mv, access, name, descriptor);
}
return mv;
}
}
//操作方法
public static class MyMethodVisitor extends AdviceAdapter {
protected MyMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
super(ASM7, mv, access, name, desc);
}
@Override
protected void onMethodEnter() {
// 在方法开始插入 return 50;
mv.visitIntInsn(BIPUSH, 50);
mv.visitInsn(IRETURN);
}
}
}
3、pom文件打包配置要配置 ,将AgentMain服务打包为agentmain.jar
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<!--Agent-Class 这个是使用attach方式时配置-->
<Agent-Class>org.example.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
在MANIFEST.MF配置环境参数
Manifest-Version: 1.0
Can-Redefine-Classes: true//true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true//true 表示能重转换此代理所需的类,默认值为 false (可选)
Agent-Class:org.example.AgentMain //指定代理类,使用attach方式会生成
4、需要有个服务将agentmain.jar attach到 MyTestMain 的jvm 进程,执行AgentMain方法
package org.example;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.IOException;
import java.util.List;
public class TestAgentMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException,
AgentLoadException, AgentInitializationException, InterruptedException {
//获取当前系统中所有 运行中的 虚拟机
System.out.println("running JVM start ");
hello();
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agentmain.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().equals("org.example.MyTestMain")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
try {
virtualMachine.loadAgent("/Users/gaoruiqiang/DataPhant/agentdemo/target/agentmain.jar");
} finally {
virtualMachine.detach();
}
}
}
}
}
5、将agentmain项目打包成agentmain.jar后,先启动 MyTestMain ,然后再启动TestAgentMain
可以看到,在TestAgentMain 启动之前打印的是100,启动之后,打印的是50,这就实现用agentmain方式在服务启动后对代码参数做修改。
五、Java探针技术的成熟应用
在现代IT系统中,尤其是云原生、微服务系统,一次外部请求往往需要多个内部服务、多个中间件和多台机器的相互调用才能完成。在这一系列的调用中,任何阶段出现的问题都可能导致外部服务失败或延迟升高,影响用户体验。如果想要精确定位及分析问题,需要使用分布式链路追踪技术。分布式链路追踪(Distributed Tracing,简称Trace)可提供整个服务调用链路的调用关系、延迟、结果等信息,非常适用于云原生、分布式、微服务等涉及多个服务交互的系统。
Java字节码探针技术是一个非常成熟的应用性能监控技术,领先的应用性能管理软件均采用这一技术。这是一个非常成熟的实现方案。在Gartner出具的APM魔力象限报告中,处于领导者象限的三个APM产品DynaTrace、AppDynamics、NewRelic都是采用Java字节码探针技术。相反采用网络镜像技术的IBM、 Riverbed,由于技术局限性都被定义在了特定领域者范畴。 以下是一些成熟的APM工具:
-
OneAPM :是国内首家支持 Java, .NET, PHP,Ruby,Python,Node.js 的应用性能管理云解决方案,通过 SaaS 向用户提供统一的入口、友好的界面、便捷的流程,来管理和监控应用程序的性能,并完成从前端、到网络、直至应用代码的端到端应用性能管理。
-
OpenTelemetry:是目前全球公认的分布式链路追踪标准,兼容OpenTracing、OpenCensus的各类客户端。OpenTelemetry由一组API、SDK和工具组成,用来观察、生成、采集和导出各类可观测性数据(Traces、Logs、Metrics)。OpenTelemetry只提供数据的格式定义、产生、收集、发送,但并不提供分析、可视化、告警等功能。
-
zipkin:Twitter公司开源的一个分布式追踪工具,被Spring Cloud Sleuth集成,使用广泛而稳定
-
skywalking:开源的一款分布式追踪,分析,告警的工具,现在是Apache旗下开源项目
-
cat:大众点评开源的一款分布式链路追踪工具。
-
New Relic
-
Dynatrace
-
Cisco AppDynamics
-
阿里巴巴鹰眼
-
。。。