JavaAgent实战
相关资料
仓库地址:gitee.com/ilovexp/jav…
什么是JavaAgent
JavaAgent是Java中的一种特殊类型的代理技术,它允许在Java应用程序编译期或运行时动态地监控和修改字节码。这使得开发者能够在不修改源代码的情况下,向应用程序添加新功能或执行一些特定的任务。
应用场景
核心原理
Instrumentation API
Instrumentation API 是 Java 提供的一组用于在 Java 虚拟机 (JVM) 运行时监控和修改类定义的工具。它允许开发人员在类加载时,动态地检测和修改类的字节码,从而实现一系列功能。主要作用如下:
获取 Instrumentation 实例
开发人员可以通过调用 Agent.premain
或 Agent.agentmain
方法来获取 Instrumentation 实例,从而使用 Instrumentation API。
addTransformer(ClassFileTransformer transformer, boolean canRetransform)
: 添加一个 ClassFileTransformer,用于在类被加载时转换类的字节码。canRetransform
参数表示是否允许对已加载类进行重新转换。retransformClasses(Class... classes)
: 对指定的一组类进行重新转换,这些类必须已经被加载。getAllLoadedClasses()
: 获取当前已经加载的所有类的数组。getInitiatedClasses(ClassLoader loader)
: 获取由指定类加载器加载的所有类的数组。
ClassFileTransformer API
public interface ClassFileTransformer {
byte[] transform(ClassLoader loader,
String className,
Class classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
ClassFileTransformer
接口:这是一个用于类转换的接口,开发人员需要实现它来定义类的转换行为。它只有一个方法 transform
,在类被加载时会被调用,开发人员可以在这个方法中对类的字节码进行修改,并返回修改后的字节码。
ClassLoader loader
: 表示加载当前类的类加载器。该参数指示了加载当前类的类加载器实例,可以用于追溯类的类加载器层次结构。String className
: 表示正在被转换的类的类名。这是一个完全限定的类名,例如 "com.example.MyClass"。Class classBeingRedefined
: 表示正在被重新定义的类。在进行类的重新定义时才会有值,否则为 null
。通常情况下,我们不需要在类加载时关心这个参数。ProtectionDomain protectionDomain
: 表示类的保护域。保护域描述了类所属的安全策略和权限信息。它可以用于检查类的访问权限,或者获取与类相关的安全上下文。byte[] classfileBuffer
: 表示类的字节码。这是一个字节数组,包含了正在加载的类的原始字节码。在 transform
方法中,可以修改这个字节码,并返回修改后的字节码。throws IllegalClassFormatException
: 当 transform
方法无法处理类时,可以抛出这个异常。通常,如果你无法处理正在加载的类,可以直接将原始的 classfileBuffer
返回,不做任何修改。VirtualMachine API
VirtualMachine
是一个类,它位于 com.sun.tools.attach
包中。它提供了一组用于管理和操作正在运行的Java虚拟机的工具方法。主要作用是允许Java应用程序在运行时连接和控制其他正在运行的Java虚拟机,从而实现一些高级的功能。
通过使用 VirtualMachine
类,Java应用程序可以在运行时:
VirtualMachine.attach()
方法连接到正在运行的Java虚拟机。这允许应用程序获得对目标虚拟机的访问权,并执行后续操作,例如获取虚拟机信息、执行诊断命令等。VirtualMachine.loadAgent()
方法来加载Java代理(JavaAgent)。JavaAgent是一种特殊类型的代理,它可以在目标虚拟机中运行,并对类加载进行拦截和修改。VirtualMachine.startManagementAgent()
方法来启动目标虚拟机的管理代理(management agent)。然后,应用程序可以通过JMX(Java Management Extensions)连接到管理代理,执行一些诊断命令、获取JVM运行时信息等。VirtualMachine.detach()
方法断开与目标虚拟机的连接,当不再需要连接时,可以通过这种方式释放资源。实战代码
整个项目目录
application项目是实际运行的项目,可以理解为实际执行的业务jar包
myAgent项目是编写agent代码的项目
middle项目是中间项目,在agentMain模式下,用于连接application和agentMain的项目;
├─application # 实际运行项目
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─cn
│ │ │ │ └─ixp
│ │ │ │ └─agent
│ │ │ └─resources
│ │ └─test
│ │ └─java
├─middle #中间项目,用于agent和application的连接,仅agentMain模式有用
│ ├─src
│ │ └─main
│ │ └─java
│ │ └─cn
│ │ └─ixp
│ │ └─agent
├─myAgent # agent项目
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─cn
│ │ │ │ └─ixp
│ │ │ │ └─agent
│ │ │ │ └─transformer
│ │ │ └─resources
│ │ └─test
│ │ └─java
PreMain方式
premain
方式的 JavaAgent 是在 Java 应用程序启动时,即主函数执行之前,通过指定启动参数 -javaagent
加载的。它的主要效果是在应用程序类加载之前,对类进行拦截并修改类的字节码,从而实现对类的增强、动态注入等操作。
程序执行顺序:premain() => main()
已知,我们有一个Man类,应用程序启动时,循环调用Man类的getName()方法;屏幕上不断打印"Dragon like sheep"
;当启动Agent时,将打印的文字替换为:"This is ordertiny"
;
Application项目:
public class Man {
public Man() {
System.out.println("Create Man");
}
public void getName() {
try {
Thread.sleep((long) (Math.random()*10 * 1000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Dragon like sheep");
}
}
// 启动类
public class ApplicationMain {
public static void main(String[] args) throws InterruptedException {
System.out.println("U are in Application Main");
while (true) {
Man man = new Man();
man.getName();
Thread.sleep(1000);
}
}
}
编写Agent项目
引入Jar包
com.squareup
javapoet
1.13.0
org.javassist
javassist
3.27.0-GA
org.apache.maven.plugins
maven-shade-plugin
3.3.0
package
shade
org.javassist:javassist
cn.ixp.agent.InPreMain
true
true
这样可以在打包的时候在MANIFEST.MF
中显示声明Premain-Class
# MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: cn.ixp.agent.InPreMain
Archiver-Version: Plexus Archiver
Built-By: lenovo
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_181
定义入口方法
package cn.ixp.agent;
import cn.ixp.agent.transformer.MyClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class InPreMain {
/**
* 固定格式,声明premain函数,然后在其中指定我们自己定义的代理类
* @param agentArgs 捕获agent参数
* 例如:java -javaagent:myagent.jar=myarg1,myarg2 -jar myapp.jar
* 那么agentArgs就是myarg1,myarg2
* @param inst 自动注入的Instrumentation实例
*/
public static void premain(String agentArgs, Instrumentation inst) {
// 打印一下参数
System.out.println("premain invoke!"+agentArgs);
// 添加自定义Transformer
inst.addTransformer(new MyClassFileTransformer(), true);
}
}
自定义ClassFileTransformer
用于将原有cn.ixp.agent.Man.class中的getName()方法体修改为 输出"This is orderting"
package cn.ixp.agent.transformer;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
System.out.println("transform invoke! "+className);
// 修改指定类
if (className.contains("cn/ixp/agent/Man")){
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("cn.ixp.agent.Man");
CtMethod convertToAbbr = clazz.getDeclaredMethod("getName");
// 修改指定的方法体
String methodBody = "System.out.println("This is ordertiny");";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
}
}catch (Exception e) {
System.out.println("error");
}
// 表示不修改原有类
return null;
}
}
此时将agent项目打成jar包
启动application项目后,Man的输出将被agent修改;
java -javaagent:myAgent-1.0-SNAPSHOT.jar -jar application.jar
AgentMain方式
实现一个方法的耗时打印
我们实现一个方法的耗时打印,我们在每个方法进入前和后加入时间戳,用来统计方法耗时;
修改Pom文件
指定Agent-Class
所在类路径;
javaAgent
cn.ixp.agent
1.0-SNAPSHOT
4.0.0
myAgent
8
8
UTF-8
com.squareup
javapoet
1.13.0
org.javassist
javassist
3.27.0-GA
org.apache.maven.plugins
maven-shade-plugin
3.3.0
package
shade
org.javassist:javassist
cn.ixp.agent.InPreMain
cn.ixp.agent.InAgentMain
true
true
指定AgentMain入口
package cn.ixp.agent;
import cn.ixp.agent.transformer.InjectMethodCostTimeTransformer;
import cn.ixp.agent.transformer.MyClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class InAgentMain {
public static void agentmain(String agentArgs, Instrumentation inst){
// 添加耗时统计的Transformer
inst.addTransformer(new InjectMethodCostTimeTransformer(), true);
try {
for (Class innerClass : inst.getAllLoadedClasses()) {
if (innerClass.getName().contains("cn.ixp")) {
// 指定某个包下的类
inst.retransformClasses(innerClass);
}
}
} catch (UnmodifiableClassException e) {
System.out.println("出错");
throw new RuntimeException(e);
}
}
}
自定义Transformer
package cn.ixp.agent.transformer;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class InjectMethodCostTimeTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("InjectMethodCostTimeTransformer transform invoke! "+className);
String targetClassName = className.replaceAll("/", ".");
try {
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get(targetClassName);
for (CtMethod declaredMethod : clazz.getDeclaredMethods()) {
declaredMethod.addLocalVariable("_time", CtClass.longType);
// 进入时间
String param1 = "_time=System.currentTimeMillis();";
// 打印语句
String param2 = "System.out.println(""+declaredMethod.getName()+" cost:"+(System.currentTimeMillis()-_time));";
declaredMethod.insertBefore(param1);
declaredMethod.insertAfter(param2);
}
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Exception e) {
System.out.println("has error"+targetClassName);
e.printStackTrace();
}
return null;
}
}
将Agent项目打成jar包
编写中间项目
middle项目只是一个桥接项目,它用于
package cn.ixp.agent;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
// 不会执行
public static void main(String[] args) throws Exception {
System.out.println("Here is MyAgent Main");
List list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if ("cn.ixp.agent.ApplicationMain".equals(vmd.displayName())) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
// 加载agent的jar
virtualMachine.loadAgent("myAgent-1.0-SNAPSHOT.jar");
// 加载完毕后 断开连接
virtualMachine.detach();
}
}
}
}
首先保证Application正在运行,然后执行middle项目,此时即可将 正在运行的java进程中类,通过agent注入了方法耗时统计功能;
注意
我们的agent项目无法单独启动,需要调试时,应该debug应用进程,才能调试agent代码;