Java 에이전트는 문자 그대로 Java 에이전트로 번역되며, Java 프로브 기술이라고도 합니다.
Java Agent 이 기술은 JDK1.5에 도입되었으며 런타임 시 Java 바이트코드를 동적으로 수정할 수 있습니다. Java의 클래스는 JVM에 의해 실행되는 바이트코드를 형성하기 위해 컴파일됩니다. JVM은 이러한 바이트코드를 실행하기 전에 이러한 바이트코드의 정보를 얻고 프로세스를 완료하기 위해 바이트코드 변환기를 통해 이러한 바이트코드를 수정합니다.
Java 에이전트는 독립적으로 실행할 수 없는 jar 패키지이며 대상 프로그램에 연결된 JVM 프로세스를 통해 작동합니다. 시작할 때 ClassFileTransformer
바이트코드 변환기를 추가하려면 대상 프로그램의 시작 매개변수에 -javaagent 매개변수만 추가하면 됩니다. 이는 기본 메소드 앞에 인터셉터를 추가하는 것과 같습니다. ClassFileTransformer
字节码转换器,相当于在main方法前加了一个拦截器。
Java Agent 主要有以下功能:
Java Agent
能够在加载 Java 字节码之前拦截并对字节码进行修改;
Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;
Java Agent 的应用场景:
IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
热部署功能,例如 JRebel、XRebel、spring-loaded;
各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
各种性能分析工具,例如 Visual VM、JConsole 等;
全链路性能检测工具,例如 Skywalking、Pinpoint等;
在了解Java Agent
的实现原理之前,需要对Java类加载机制有一个较为清晰的认知。一种是在man方法执行之前,通过premain来执行,另一种是程序运行中修改,需通过JVM中的Attach实现,Attach的实现原理是基于JVMTI。
主要是在类加载之前,进行拦截,对字节码修改
下面我们分别介绍一下这些关键术语:
JVMTI 就是JVM Tool Interface
,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展
JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现
JVMTIAgent是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
Agent_OnLoad
函数,如果agent是在启动时加载的,通过JVM参数设置
Agent_OnAttach
函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
Agent_OnUnload
函数,在agent卸载时调用
javaagent 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent
(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的
instrument 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent
,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent
JVM Attach 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行
我们就以打印方法的执行时间为例,通过Java Agent
来实现。
首先我们需要构建一个精简的Maven
Java Agent
는 Java 바이트코드를 로드할 수 있습니다.
Java 에이전트는 Jvm 실행 중에 로드된 바이트코드를 수정할 수 있습니다.
Java Agent
이해 원리를 구현하기 전에 Java 클래스 로딩 메커니즘을 명확하게 이해해야 합니다. 하나는 man 메소드가 실행되기 전에 premain을 통해 실행하는 것이고, 다른 하나는 실행 중에 프로그램을 수정하는 방법인데, 이는 JVM에서 Attach를 통해 구현해야 한다. Attach의 구현 원리는 JVMTI를 기반으로 한다. 🎜🎜 주로 클래스 로딩 전에 바이트코드를 가로채서 수정합니다. 🎜🎜🎜 주요 용어를 각각 소개하겠습니다: 🎜🎜JVM 도구입니다. 사용자 확장을 위해 JVM에서 제공하는 인터페이스 모음인 인터페이스
는 JVM이 특정 논리를 실행할 때마다 이러한 콜백 인터페이스를 통해 사용자가 확장할 수 있습니다. 🎜Agent_OnLoad
함수, 에이전트가 시작 시 로드됨에 있는 경우 다음을 통해 설정 JVM 매개변수 🎜Agent_OnAttach
함수, 에이전트가 시작 시 로드되지 않았지만 먼저 대상 프로세스에 연결한 다음 응답하는 경우 대상 프로세스가 로드 명령을 전송하여 로드합니다. , Agent_OnAttach 함수는 로딩 프로세스 중에 호출됩니다🎜Agent_OnUnload
함수는 에이전트가 언로드될 때 >JPLISAgent
(Java 프로그래밍 언어 계측 서비스 에이전트)라는 개체도 있습니다. 악기 에이전트
를 로드할 수도 있습니다. 런타임 시 동적 로드는 로드 명령을 전송하여 에이전트를 로드합니다🎜를 통해 인쇄 메소드의 실행 시간을 예로 들어 보겠습니다. Java Agent 를 달성합니다. 🎜🎜먼저 간소화된 <code>Maven
프로젝트를 빌드해야 합니다. 여기서는 두 개의 Maven 하위 프로젝트(플러그인 에이전트 구현용 및 테스트 대상 프로그램 구현용)를 빌드해야 합니다. 🎜🎜🎜🎜🎜🎜두 프로젝트가 공통적으로 의존하는 패키지를 상위 애플리케이션에서 가져옵니다🎜🎜<dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.28.0-GA</version> </dependency> </dependencies>🎜🎜먼저 테스트할 대상 프로그램을 빌드합니다🎜🎜
// 启动类 public class APPMain { public static void main(String[] args) { System.out.println("APP 启动!!!"); AppInit.init(); } } // 模拟的应用初始化的类 public class AppInit { public static void init() { try { System.out.println("APP初始化中..."); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
然后我们启动程序,测试是否能正常执行,程序正常执行之后,我们开始构建探针程序
探针程序中我们需要编写,改变原有class的Transformer
,通过自定义的Transformer类完成输出方法执行时间的功能,
首先构检Agent程序的入口
public class RunTimeAgent { public static void premain(String arg, Instrumentation instrumentation) { System.out.println("探针启动!!!"); System.out.println("探针传入参数:" + arg); instrumentation.addTransformer(new RunTimeTransformer()); } }
这里每个类加载的时候都会走这个方法,我们可以通过className进行指定类的拦截,然后借助javassist这个工具,进行对Class的处理,这里的思想和反射类似,但是要比反射功能更加强大,可以动态修改字节码。
javassist是一个开源的分析、编辑和创建Java字节码的类库。
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 RunTimeTransformer implements ClassFileTransformer { private static final String INJECTED_CLASS = "com.zhj.test.init.AppInit"; @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 { // 使用javassist,获取字节码类 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) { //这里要用Throwable,不要用Exception System.out.println(e.getMessage()); e.printStackTrace(); } } return classfileBuffer; } }
我们需要在Maven中配置,编译打包的插件,这样我们就可以很轻松的借助Maven生成Agent的jar包
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 --> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> <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.zhj.agent.RunTimeAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
否则我们需要在resources下创建META-INF/MANIFEST.MF文件,文件内容如下,我们可以看出这个与Maven中的配置是一致的,然后通过配置编译器,借助编译器打包成jar包,需指定该文件
Manifest-Version: 1.0 Premain-Class: com.zhj.agent.RunTimeAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
告示文件MANIFEST.MF参数说明:
Manifest-Version
文件版本
Premain-Class
包含 premain 方法的类(类的全路径名)main方法运行前代理
Agent-Class
包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构
Boot-Class-Path
设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选)
Can-Redefine-Classes true
表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes true
表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix true
表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
最后通过Maven生成Agent的jar包,然后修改测试目标程序的启动器,添加JVM参数即可
参数示例:-javaagent:F:\code\myCode\agent-test\runtime-agent\target\runtime-agent-1.0-SNAPSHOT.jar=hello
最终效果:
위 내용은 Java 에이전트 사용 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!