Java Instrumentation API开发Java Agent学习记录

1.简介

本文将讨论Java Instrumentation API。Instrumentation API由JVM提供用来修改已加载类的工具,可以提供Java语言编写的插桩功能,动态修改运行时代码的能力。

此外本文还会介绍如何开发Java agent,通过Java agent来动态增强系统功能。

2.JPDA介绍

JPDA(Java platform debugger architecture)定义了一整套完整的调试体系,它将调试体系分为三部分,并规定了三者之间的通信接口[1]。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:
在这里插入图片描述
正式通过这三层结构提供的能力,给我们提供了调试功能。利用JVMTI提供的能力,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

关于JPDA和JVMTI的详细介绍可以进一步阅读参考资料1。

3.什么是Java Agent

简单来说,Java agent是一个我们通过使用JVM提供的Instrumentation API来开发出来的jar包,可以用来修改已加载到JVM中的字节码文件。

一个agent要工作,需要定义两个方法:

  • premain: 在JVM启动时,通过指定-javaagent参数来静态加载agent
  • agentmain: 如果希望在运行时加载一个agent,那么我们就会用到agentmain方法,最后使用Java Attach API动态加载Agent,并执行agentmain方法,实现运行时对字节码文件的修改。

接下来,我们首先将看一下如何使用已有的Java agent。随后,如何从零开始如何动态在创建功能并增加到字节码中。

4. 通过Java agent增强代码

在介绍Java agent增强代码之前首先介绍业务背景,随后对介绍利用agent来增强业务代码。

4.1 业务代码

我们定义一个简单的业务类,打印一段信息。后续我们利用Agent来给代码动态增加功能,实现增强。

public class MyAtm {

    public static void withdrawMoney(int amount) throws InterruptedException {
        //processing going on here
        Thread.sleep(2000L);
        System.out.println(String.format("[Application] Successful Withdrawal of [%s] units!",amount));

    }
}

4.2 InstrumentationAPI简介

为了开发Agent会用到Instrumentation API,几个核心API如下[2]:

public interface Instrumentation {
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);
    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);
    boolean isRetransformClassesSupported();
    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    boolean isRedefineClassesSupported();
    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);
}

关于开发Agent规范,可以进一步阅读参考资料2。

4.3 开发Agent

根据4.2中引用的参考,我们开发一个Agent,对4.1中的代码增加。增强的能力为,在方法前后增加开始和结束时间,从而记录这段代码执行的耗时。

首先我们定义一个ClassFileTransformer实现,该实现的主要功能是通过javassist来修改字节码文件。

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 AtmTransformer implements ClassFileTransformer{

    
    private String targetClassName;

    private ClassLoader targetClassLoader;

    private static final String WITHDRAW_MONEY_METHOD = "withdrawMoney";

    public AtmTransformer(String targetClassName,ClassLoader targetClassLoader) {
        this.targetClassName = targetClassName;
        this.targetClassLoader = targetClassLoader;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] byteCode = classfileBuffer;

        String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/");

        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);
                m.addLocalVariable("startTime",CtClass.longType);
                m.insertBefore("{startTime=System.currentTimeMillis();}");
                StringBuilder endBlock = new StringBuilder();
                m.addLocalVariable("endTime",CtClass.longType);
                m.addLocalVariable("opTime",CtClass.longType);

                endBlock.append("endTime = System.currentTimeMillis();");
                endBlock.append("opTime = (endTime-startTime)/1000;");

                endBlock.append("System.out.println(\"[Application] Withdrawal operation completed in:\" + opTime + \" seconds!\");");
                m.insertAfter(endBlock.toString());
                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Throwable e) {
                System.out.println("Exception" + e);
            }
        }
        return byteCode;
    }
}

随后我们定义premain和agentmain方法,当通过不同的方法加载agent后会执行这些方法。这些方法在启动后,会添加一个转换器,当业务类被加载后,会注册AtmTransformer类,最终完成对原有功能的增加。

package com.baeldung.instrumentation.agent;

import java.lang.instrument.Instrumentation;

public class MyInstrumentationAgentV {

    public static void premain(String agentArgs, Instrumentation inst) {
            System.out.println("[Agent] In premain method");
            String className = "com.baeldung.instrumentation.application.MyAtm";
            transformClass(className, inst);
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("[Agent] In agentmain method");
        String className = "com.baeldung.instrumentation.application.MyAtm";
        transformClass(className, inst);
    }

    private static void transformClass(String className, Instrumentation instrumentation) {
        Class<?> targetCls = null;
        ClassLoader targetClassLoader = null;
        try {
            targetCls = Class.forName(className);
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            return;
        } catch (Exception ex) {
            System.err.println("Class [{}] not found with Class.forName");
        }
        // otherwise iterate all loaded classes and find what we want
        for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
            if(clazz.getName().equals(className)) {
                targetCls = clazz;
                targetClassLoader = targetCls.getClassLoader();
                transform(targetCls, targetClassLoader, instrumentation);
                return;
            }
        }
        throw new RuntimeException("Failed to find class [" + className + "]");
    }

    private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
        AtmTransformer dt = new AtmTransformer(clazz.getName(), classLoader);
        instrumentation.addTransformer(dt, true);
        try {
            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
        }
    }
}

4.4 加载Agent

agent有两种方法加载,静态和动态。静态在系统启动时候通过指定-javaagent:agent.jar来加载。而动态方法通过attach api在运行时来加载agent。

4.4.1 静态加载

定义一个业务执行的主方法,在启动时,通过命令或idea中的ide中的JVM参数来启动agent。

 -javaagent:xx/xx/agent-1.0.0-jar-with-dependencies.jar
 
public class Launcher {
    public static void main(String[] args) throws Exception {
       MyAtmApplication.run();
    }
}

public class MyAtmApplication {
    public static void run() throws Exception {
        System.out.println("[Application] Starting ATM application");
        MyAtm.withdrawMoney(10);
        TimeUnit.SECONDS.sleep(2);
        MyAtm.withdrawMoney(10);
    }
}

当启动系统后,premain方法会优先执行,随后执行我们的main方法。
接着会看到增加代码的输出执行时间记录,[Application] Withdrawal operation completed in:2 seconds!

[Agent] In premain method
[Application] Starting ATM application
[Application] Successful Withdrawal of [10] units!
[Application] Withdrawal operation completed in:2 seconds!
[Application] Successful Withdrawal of [10] units!
[Application] Withdrawal operation completed in:2 seconds!

4.4.2 动态加载

在运行时,我们找到目标JVM,然后将agent绑定到对应的JVM上,实现对目标系统的增加。这里用到一个AgentLoader类,完成目标JVM的发现和Agent的加载。

import com.sun.tools.attach.VirtualMachine;

import java.io.File;
import java.util.Optional;

public class AgentLoader {
    public static void run() {
        String agentFilePath = "xxx/xxx/agent-1.0.0-jar-with-dependencies.jar";
        String applicationName = "Launcher";

        Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
                .stream()
                .filter(jvm -> {
                    System.out.println(String.format("jvm:%s", jvm.displayName()));
                    return jvm.displayName().endsWith(applicationName);
                })
                .findFirst().get().id());

        if(!jvmProcessOpt.isPresent()) {
            System.out.println("Target Application not found");
            return;
        }
        File agentFile = new File(agentFilePath);
        try {
            String jvmPid = jvmProcessOpt.get();
            System.out.println("Attaching to target JVM with PID: " + jvmPid);
            VirtualMachine jvm = VirtualMachine.attach(jvmPid);
            jvm.loadAgent(agentFile.getAbsolutePath());
            jvm.detach();
            System.out.println("Attached to target JVM and loaded Java agent successfully");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

随后运行启动方法,同样会输出增强信息:

[Application] Withdrawal operation completed in:2 seconds!

package com.baeldung.instrumentation.application;

public class Launcher {
    public static void main(String[] args) throws Exception {
            AgentLoader.run();
            MyAtmApplication.run();
    }
}

5. 遇到的一些问题

  • 找不到javassist依赖,NoClassDefFound:这个问题是运行时,找不到工具类依赖,加载后,可以将agent jar工程打为一个fat jar,相关依赖都打好,或者指定好依赖类路径也可以。
  • Agent JAR loaded but agent failed to initialize:这个问题是我在通过AgentLoader找目标JVM时,找到了一个错误的JVM导致初始化失败。最开始用的匹配方法是jvm.displayName().contains(applicationName);导致错误。后来改为endsWith(applicationName); 遇到agent初始化错误时,可以简化问题进行排查,先看agent是否正常被加载。这个加日志观察即可。如果都没正常加载肯定是找错JVM了。接着继续缩小问题范围,逐个解决。

6. 总结

通过Agent方法配合字节码增加工具[5]实现运行时对系统代码修改,或者获取JVM信息,实现系统增加,从而可以实现不同的系统能力。如无侵入的在线系统诊断,监控上报。 通过自己完成文章中的demo及处理相关问题,加深对Agent的初步认知。

参考资料

[1]jpda介绍,https://developer.ibm.com/zh/articles/j-lo-jpda1/
[2]java agent学习,https://www.cnblogs.com/rickiyang/p/11368932.html
[3]打包可执行,jarhttps://www.baeldung.com/executable-jar-with-maven
[4]学习demo参考,https://www.baeldung.com/java-instrumentation
[5]字节码增强,https://blog.csdn.net/meituantech/article/details/100570756

Java Agent是Arthas使用的技术,是Skywalking使用的技术,是一份十分重要的技术。 课程的稀缺性在此之前,市面上并没有针对Java Agent进行系统介绍的课程。 通过搜索引擎查找,会发现与Java Agent相关的内容大多是个人知识总结分享的内容。这些内容有如下特点:内容质量不一详略程度不一学习难度千差万别总体上来说,学习者很难有一个整体认知、系统学习的过程。 课程的设计目标 在构思课程内容时,本课程带有以下目标:课程学习梯度:从简单到复杂,让学习者有一个循序渐进的理解过程。构造完整、统一的知识体系:不是零散的知识点堆砌,而是有一个统一的贯穿始终的知识框架。具有可操作性的代码示例,不只是讲概念,更注意于实践。课程内容安排 本课程通过四章内容对Java Agent相关知识进行讲解:第一章,介绍Agent Jar的三个组成部分:Manifest、Agent Class和ClassFileTransformer。第二章,介绍Agent Jar的两种启动方式:从命令行启动和使用Attach机制启动。第三章,介绍如何利用Instrumentation API来实现Agent Jar的功能。第四章,Java Agent的应用与技巧。 通过本课程的学习,让同学们更好地建立起一个完整的知识体系:  讲师介绍我叫刘森,南京师范大学研究生毕业,2015年获得信息系统项目管理师(高级),2014年获得系统集成项目管理工程师(中级)。 目前,我的课程都是围绕着“Java字节码”技术展开: 《Java Agent基础篇》是在一个运行JVM当中提供修改字节码的机会《Java ASM系列》(免费课程)是一个操作字节码的类库《Java 8 ClassFile》专注于字节码的理论知识,入选为“51CTO数字化人才证书项目认证课程” 因此,我对字节码技术有较为深入的研究和理解,大家想学习字节码的技术可以找我:字节码技术找刘森,轻松学习又省心~~~ 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值