如何开发Java Agent
简介
Java Instrumentation API
此API由java.lang.instrument包提供,其核心是Instrumentation接口,它提供了探测(instrument)Java代码的基本服务,可用于实现性能监控、Profiler、事件记录器等功能。获取Instrumentation实例的方法有两种:
- 如果JVM启动时静态加载了Agent,那么Instrumentation被传递给Agent类的 premain方法
- 如果JVM在运行时动态加载了Agent,那么Instrumentation被传递给Agent类的 agentmain方法
常用方法
Instrumentation接口最常用的方法包括:
方法 | 说明 |
addTransformer | 为instrumentation引擎提供一个转换器 |
getAllLoadedClasses | 获取当前已经加载的所有类的列表 |
retransformClasses | 通过添加字节码来修改已加载的类 |
removeTransformer | 移除转换器 |
redefineClasses | 直接替换类 |
Java Agent
Java Agent本质上就是一个Jar包,它会调用 Instrumentation API,来修改已经加载到JVM中的字节码。Java Agent具有两种加载方式。
静态加载
这种方式下,在应用程序的任何代码被执行之前,就加载Agent以修改字节码。静态加载需要使用JVM的-javaagent参数:
1 2 3 |
java -javaagent:agent.jar -jar application.jar # 可以同时加载多个Agents java -javaagent:agentA.jar -javaagent:agentB.jar application.jar |
动态加载
这种方式下,Agent可以在运行时动态按需的加载。动态加载需要调用Java Attach API,下面是个例子:
1 2 3 4 5 6 |
// 根据PID查找目标JVM,并连接到JVM VirtualMachine jvm = VirtualMachine.attach(jvmPid); // 加载Agent jvm.loadAgent(agentFile.getAbsolutePath()); // 取消到JVM的连接 jvm.detach(); |
基础
创建入口点
Agent类就是一个普通的Java类,不需要特殊签名或者实现接口。根据加载方式的不同,你需要添加下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static void premain( String agentArgs, Instrumentation inst) { LOGGER.info("[Agent] In premain method"); String className = "com.baeldung.instrumentation.application.MyAtm"; // 添加转换器 transformClass(className,inst); } public static void agentmain( String agentArgs, Instrumentation inst) { LOGGER.info("[Agent] In agentmain method"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass(className,inst); } |
编写转换器
转换器实现ClassFileTransformer接口的transform方法,进行字节码编辑。我们通常会利用javassist来操控字节码。
下面的例子,修改HttpURLConnection类,以便对JVM访问的每个URL进行审计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform( final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer ) throws IllegalClassFormatException { // 仅仅操作HttpURLConnection类 if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) { try { // 从ClassPool获得CtClass对象 final ClassPool classPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("sun.net.www.protocol.http.HttpURLConnection"); // 修改构造函数,在其结尾添加日志记录逻辑 for (final CtConstructor constructor: clazz.getConstructors()) { constructor.insertAfter("System.out.println(this.getURL());"); } // 返回字节码,并且detachCtClass对象 byte[] byteCode = clazz.toBytecode(); clazz.detach(); return byteCode; } catch (final NotFoundException | CannotCompileException | IOException ex) { ex.printStackTrace(); } } // 如果返回null则字节码不会被修改 return null; } } |
创建Agent清单
你需要在Jar的MANIFEST.MF文件中适当的属性:
1 2 3 4 |
Agent-Class: cc.gmem.agent.MyInstrumentationAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: cc.gmem.agent.MyInstrumentationAgent |
示例
本章提供一个例子,该例子没有直接使用Instrumentation API,而是调用Byte Buddy提供的更简便的API。 在该例子中,我们拦截IntelliJ IDE的DesktopLayout.getInfo方法,实现Tool Window的宽度、高度固定化。
Agent入口点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package cc.gmem.intellij; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.implementation.MethodDelegation; import java.lang.instrument.Instrumentation; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; public class IntellijUIAgent { public static void premain( String arguments, Instrumentation instrumentation ) throws Exception { new AgentBuilder.Default() // 需要拦截的类 .type( named( "com.intellij.openapi.wm.impl.DesktopLayout" ) ) .transform( ( builder, type, classLoader, module ) -> { // 需要拦截的方法 return builder.method( named( "getInfo" ).and( takesArguments( String.class, boolean.class ) ) ) // to可以指定一个对象,也可以指定一个类。后者的话,拦截器方法必须是static .intercept( MethodDelegation.withDefaultConfiguration().to( createaInterceptor( classLoader, arguments ) ) ); } ) // 将无法拦截的错误信息打印到控制台 .with( AgentBuilder.Listener.StreamWriting.toSystemOut().withErrorsOnly() ) // 使用REBASE方式,否则可能出现None of [XXX] allows for delegation from YYY .with( AgentBuilder.TypeStrategy.Default.REBASE ) .installOn( instrumentation ); System.out.println( "Intellij UI Agent by Alex Wong" ); } // 注意不要通过new操作符直接创建DesktopLayoutInterceptor,原因是 // Intellij使用独立的ClassLoader com.intellij.util.lang.UrlClassLoader来加载它自己的类库 // 你new的时候使用的是系统类加载器 private static Object createaInterceptor( ClassLoader classLoader, String arguments ) { try { String[] weights = arguments.split( "," ); float leftWeight = Float.parseFloat( weights[0] ); float bottomWeight = Float.parseFloat( weights[1] ); Class<?> cls = classLoader.loadClass( "cc.gmem.intellij.DesktopLayoutInterceptor" ); return cls.getConstructor( float.class, float.class ).newInstance( leftWeight, bottomWeight ); } catch ( Exception e ) { throw new RuntimeException( e ); } } } |
拦截器类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package cc.gmem.intellij; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.impl.WindowInfoImpl; import net.bytebuddy.implementation.bind.annotation.Origin; import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.implementation.bind.annotation.SuperCall; import java.lang.reflect.Method; import java.util.concurrent.Callable; public class DesktopLayoutInterceptor { private final float leftWeight; private final float bottomWeight; public DesktopLayoutInterceptor( float leftWeight, float bottomWeight ) { this.leftWeight = leftWeight; this.bottomWeight = bottomWeight; } @RuntimeType public Object getInfo( @Origin Method method, @SuperCall Callable<?> callable ) { WindowInfoImpl info = null; try { info = (WindowInfoImpl) callable.call(); } catch ( Exception e ) { e.printStackTrace(); } if ( info.getAnchor() == ToolWindowAnchor.LEFT ) { info.setWeight( leftWeight ); } else if ( info.getAnchor() == ToolWindowAnchor.BOTTOM ) { info.setWeight( bottomWeight ); } return info; } } |
使用Agent
1 2 |
# 注意向Agent传参的方式 -javaagent:target/ui-agent-1.0-SNAPSHOT.jar=0.28,0.4 |
Leave a Reply