写点什么

JavaAgent 查看动态生成类的源码

用户头像
长河
关注
发布于: 刚刚

1. 前言

为什么会接触 JavaAgent 呢?​


这起源于笔者最近在读 Dubbo 的源码,Dubbo 有一个很有意思的功能——SPI,它可以根据运行时的 URI 参数,自适应的调用特定的实现类。大致的原理其实也能猜到,无非就是生成一个代理类,反射解析 URI 参数里的值,然后再调用对应的实现类。虽然大概可以猜到实现原理,但毕竟只是猜想,抱着科学严谨的精神,还是想看看 Dubbo 的实现源码,此时就有了一个想法,能不能把 Dubbo 生成的代理对象的 Class 类 Dump 下来,然后反编译看看它的源码呢?​


理论上是完全可行的,阿里有一个很好用的开源工具 Arthas,它的jad命令就支持对 JVM 已经加载的类进行反编译查看源码,笔者把 Arthas 项目源码 down 下来了,查看以后发现,需要用到 JavaAgent 技术。

2. JavaAgent 规范

在 JDK1.5 以后,我们可以使用 JavaAgent 技术,以「零侵入」的方式对 Java 程序做增强。例如阿里云的 Arms 应用监控服务,就可以通过 JavaAgent 的方式接入一个探针,它会把应用的运行数据上报到阿里云,开发者可以在后台查看到应用的运行数据。这种方式,不需要我们对应用做任何改动,就可以轻松实现应用监控。​


JavaAgent 是一种规范,它分为两类:主程序运行前 Agent、主程序运行后 Agent。它可以在 JVM 加载 Class 文件前,对字节码做修改,甚至允许修改已经加载过的 Class,这样我们就可以对应用做增强、以及实现代码热部署。


主程序运行前 Agent 的步骤:1、编写 Agent 类,该类必须有静态方法premain()


public class MyAgentClass {
// JVM优先执行该方法 public static void premain(String agentArgs, Instrumentation inst) { System.err.println("main before..."); }
public static void premain(String agentArgs) { System.err.println("main before..."); }}
复制代码


2、在resources/META-INF目录下编写 MANIFEST.MF 文件,指定Premain-Class,然后将程序打成 Jar 包。


Manifest-Version: 1.0Can-Redefine-Classes: trueCan-Retransform-Classes: truePremain-Class: top.javap.agent.MyAgentClass// 注意,这里必须空一行
复制代码


使用 Maven 构建程序时,也可使用如下配置。


<plugin>  <groupId>org.apache.maven.plugins</groupId>  <artifactId>maven-jar-plugin</artifactId>  <configuration>    <archive>      <manifest>        <addClasspath>true</addClasspath>      </manifest>      <manifestEntries>        <Premain-Class>top.javap.agent.MyAgentClass</Premain-Class>        <Can-Retransform-Classes>true</Can-Retransform-Classes>        <Can-Redefine-Classes>true</Can-Redefine-Classes>      </manifestEntries>    </archive>  </configuration></plugin>
复制代码


3、启动目标程序时,指定 JVM 参数,如下:


java -javaagent:agent-1.0-SNAPSHOT.jar JavaApp
复制代码


主程序运行后 Agent 的步骤:这种是针对已经运行的 JVM 进程,我们可以通过 attach 机制,启动一个新的 JVM 进程发送指令给它执行。1、编写 Agent 类,该类必须有静态方法agentmain()


public class MyAgentClass {
public static void agentmain(String agentArgs, Instrumentation inst) { System.err.println("main after..."); }}
复制代码


2、在resources/META-INF目录下编写 MANIFEST.MF 文件,指定Premain-Class,然后将程序打成 Jar 包。


Manifest-Version: 1.0Can-Redefine-Classes: trueCan-Retransform-Classes: trueAgent-Class: top.javap.agent.MyAgentClass// 注意,这里必须空一行
复制代码


3、编写 attach 程序,启动并 attach 到目标 JVM 进程。


public static void main(String[] args) throws Exception {    VirtualMachine vm = VirtualMachine.attach("8080");    vm.loadAgent("/dev/agent.jar");}
复制代码

3. 相关组件

3.1 Instrumentation

编写的 AgentClass 类必须有premain()方法,其中一个比较重要的参数就是 Instrumentation。它是 JavaAgent 技术用到的主要 API,接口定义如下:


public interface Instrumentation {    /**   * 添加Class文件转换器,底层采用数组存储   * JVM加载Class文件前,需要依次经过转换   * @param transformer   * @param canRetransform 是否允许转换   */    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    void addTransformer(ClassFileTransformer transformer);
// 删除Class文件转换器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported();
// 重新转换Class void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported();
// 重新定义Class,热更新 void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); @SuppressWarnings("rawtypes") Class[] getInitiatedClasses(ClassLoader loader); // 获取对象大小 long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);}
复制代码


重要的方法笔者已经写上注释了,本文会用到的方法主要是addTransformer()。它可以用来添加 Class 转换器,JVM 在加载 Class 前,会先经过这些转换器进行加工。

3.2 ClassFileTransformer

Class 文件转换器,JVM 加载某个 Class 前,会先经过它转换,我们可以在这里去修改字节码以达到功能增强的目的。它只有一个方法transform()


public interface ClassFileTransformer{        /**   * 转换Class   * @param loader 类加载器   * @param className 类名   * @param classBeingRedefined 原始Class   * @param ProtectionDomain    * @param classfileBuffer Class文件字节数组   */  byte[] transform(  ClassLoader loader,                String className,                Class<?>  classBeingRedefined,                ProtectionDomain protectionDomain,                byte[]  classfileBuffer)        throws IllegalClassFormatException;}
复制代码


本文主要用到的就是classfileBuffer,有了 Class 的字节数组,只要把它导出到磁盘,通过 IDEA 反编译就能看到源码了。​

4. 实战

【需求】支持将任意 Java 对象的 Class 文件导出到磁盘,通过反编译查看源码,包括动态生成的类。


【实现】1、编写 InstrumentationHolder,持有 Instrumentation 实例,后续操作全靠它。


public class InstrumentationHolder {  private static Instrumentation INSTANCE;
public static void init(Instrumentation ins) { INSTANCE = ins; }
public static Instrumentation get() { if (INSTANCE == null) { throw new RuntimeException("检查 -javaagent 配置"); } return INSTANCE; }}
复制代码


2、编写 MyAgentClass,保存 Instrumentation 实例。


public class MyAgentClass {
public static void premain(String agentArgs, Instrumentation inst) { System.err.println("main before..."); InstrumentationHolder.init(inst); }}
复制代码


3、编写 ClassDumpTransformer,获取 Class 文件字节数组,导出到磁盘。


public class ClassDumpTransformer implements ClassFileTransformer {  private final File file;  private final Set<Class<?>> classes = new HashSet<>();
public ClassDumpTransformer(String path, Class<?>... classes) { this.file = new File(path); this.classes.addAll(Arrays.asList(classes)); }
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (classes.contains(classBeingRedefined)) { FileUtil.writeBytes(classfileBuffer, file); } return null; }}
复制代码


4、编写 ClassUtil 工具类,支持导出 Class 文件。


public class ClassUtil {
public static void classDump(Class<?> c, String path) { ClassDumpTransformer transformer = new ClassDumpTransformer(path, c); Instrumentation inst = InstrumentationHolder.get(); inst.addTransformer(transformer, true); try { inst.retransformClasses(c); } catch (UnmodifiableClassException e) { e.printStackTrace(); } finally { inst.removeTransformer(transformer); } }}
复制代码


5、编写 MANIFEST.MF 文件,构建 Jar 包。


Manifest-Version: 1.0Can-Redefine-Classes: trueCan-Retransform-Classes: truePremain-Class: top.javap.agent.MyAgentClass
复制代码


6、编写测试类,利用 JDK 动态代理生成代理类,然后将代理类的 Class 文件导出。


public class AgentDemo {  public static void main(String[] args) throws Exception {    Object instance = Proxy.newProxyInstance(A.class.getClassLoader(), new Class[]{A.class}, new InvocationHandler() {      @Override      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        return null;      }    });
ClassUtil.classDump(instance.getClass(), "/target/X.class"); }
public static interface A { void a(); }}
复制代码


7、设置-javaagent参数并启动程序。


java -javaagent:agent.jar AgentDemo
复制代码


此时,target 目录下就会生成X.class文件,通过 IDEA 打开即可看到 JDK 生成的代理类源码。​

5. 总结

JavaAgent 十分强大,通过它可以在 JVM 加载 Class 文件前修改字节码,甚至修改 JVM 已经加载的 Class。基于此,我们可以「零侵入」的对应用程序做增强,服务实现热部署等等。​


本文通过一个小示例,编写 ClassFileTransformer 实现类导出对象的 Class 文件,反编译查看其源码。这对于 ASM 操作字节码、JDK 动态代理等动态生成类的场景下,而我们又想看对象的具体实现时,提供了帮助。​

发布于: 刚刚阅读数: 2
用户头像

长河

关注

还未添加个人签名 2021.10.12 加入

还未添加个人简介

评论

发布
暂无评论
JavaAgent查看动态生成类的源码