java探针之修改类字节码文件

本文介绍了java探针如何利用javaAgent和ASM动态修改类文件,实现监控方法耗时和调用路径。通过Instrumentation接口,可以在JVM加载class时介入,使用ASM框架对字节码进行修改,实现类的动态增强。同时探讨了Instrumentation的实现原理,涉及到JVMTI和ClassFileTransformer接口,以及ASM、Javassist、Byte Buddy等字节码生成框架。

java探针利用了javaAgent + ASM字节码注入工具实现了动态修改类文件的功能。像skywalking和arthas都使用到了这个技术。
具体原理为:

jdk1.5以后引入了javaAgent技术,javaAgent是运行方法之前的拦截器。我们利用javaAgent和ASM字节码技术,在JVM加载class二进制文件的时候,利用ASM动态的修改加载的class文件,在监控的方法前后添加计时器功能,用于计算监控方法耗时,同时将方法耗时及内部调用情况放入处理器,处理器利用栈先进后出的特点对方法调用先后顺序做处理,当一个请求处理结束后,将耗时方法轨迹和入参map输出到文件中,然后根据map中相应参数或耗时方法轨迹中的关键代码区分出我们要抓取的耗时业务。最后将相应耗时轨迹文件取下来,转化为xml格式并进行解析,通过浏览器将代码分层结构展示出来,方便耗时分析。

上篇我们介绍了JavaAgent的基本使用,下面介绍如何去动态的修改类的字节码文件,这个才是agent实现更强大功能的核心所在!

Instrumentation接口

Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以独立于应用程序之外的代理程序,可以用来监控和扩展JVM上运行的应用程序,相当于是JVM层面的AOP。

功能:
监控和扩展JVM上的运行程序,它可以替换和修改java类的字节码以便采集数据,用于监控,性能统计,覆盖率分析,事件记录等。可以用在程序启动时,也可以用于程序运行时动态attach。

比如说一个Java程序在JVM上运行,这时如果需要监控JVM的状态,除了使用JDK自带的jps等命令之外,就可以通过instrument来更直观的获取JVM的运行情况;
或者一个Java方法在JVM中执行,如果我想获取这个方法的执行时间又不想改代码,常用的做法是通过Spring的AOP来实现,而AOP通过面向切面编程,而instrument是在JVM层面上直接改动java方法来实现。

public interface Instrumentation{
    //添加ClassFileTransformer
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //添加ClassFileTransformer
    void addTransformer(ClassFileTransformer transformer);

    //移除ClassFileTransformer
    boolean removeTransformer(ClassFileTransformer transformer);

    //是否可以被重新定义
    boolean isRetransformClassesSupported();

    //重新定义Class文件
    void redefineClasses(ClassDefinition... definitions)
        throws ClassNotFoundException, UnmodifiableClassException;

    //是否可以修改Class文件
    boolean isModifiableClass(Class<?> theClass);

    //获取所有加载的Class
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    //获取指定类加载器已经初始化的类
    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    //获取某个对象的大小
    long getObjectSize(Object objectToSize);

    //添加指定jar包到启动类加载器检索路径
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    //添加指定jar包到系统类加载检索路径
    void appendToSystemClassLoaderSearch(JarFile jarfile);

    //本地方法是否支持前缀
    boolean isNativeMethodPrefixSupported();

    //设置本地方法前缀,一般用于按前缀做匹配操作
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

要是定义了操作java类的class文件方法,这里又涉及到了ClassFileTransformer接口,这个接口的作用是改变Class文件的字节码,返回新的字节码数组,源码如下:

public interface ClassFileTransformer{

    byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
}

ClassFileTransformer接口只有一个方法,就是改变指定类的Class文件,该接口没有默认实现,很显然如果需要改变Class文件的内容,需要改成什么样需要使用者自己来实现。
如:

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MyTransformer implements ClassFileTransformer {

    final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
    final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";

    // 被处理的方法列表
    final static Map<String, List<String>> methodMap = new HashMap<>();

    public MyTransformer() {
        add("com.jun.sail.myservice.service.HelloService.say");
        add("com.jun.sail.myservice.service.HelloService.say2");
    }

    private void add(String methodString) {
        String className = methodString.substring(0, methodString.lastIndexOf("."));
        String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
        List<String> list = methodMap.computeIfAbsent(className, k -> new ArrayList<>());
        list.add(methodName);
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace("/", ".");
        if (methodMap.containsKey(className)) { // 判断加载的class的包路径是不是需要监控的类
            CtClass ctclass = null;
            try {
                // 使用全称,用于取得字节码类<使用javassist>
                ctclass = ClassPool.getDefault().get(className);
                for (String methodName : methodMap.get(className)) {
                    String outputStr = "\nSystem.out.println(\"this method [" + methodName
                            + "] cost:\" +(endTime - startTime) +\"ms.\");";
                    // 得到这方法实例
                    CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);

                    // 根据原来的方法 创建新的方法,名字为原来的methodName
                    CtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);

                    // 把旧方法名字改掉,否则会冲突
                    String oldMethodName = methodName + "$old";
                    ctmethod.setName(oldMethodName);

                    // 构建新的方法体
                    StringBuilder bodyStr = new StringBuilder();
                    bodyStr.append("{");
                    bodyStr.append(prefix);
                    bodyStr.append(oldMethodName).append("($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数
                    bodyStr.append(postfix);
                    bodyStr.append(outputStr);
                    bodyStr.append("}");
                    newMethod.setBody(bodyStr.toString());

                    newMethod.setName(methodName);
                    ctclass.addMethod(newMethod);
                }
                return ctclass.toBytecode();
            } catch (Exception e) {
                System.out.println("AAAAA" + e.getMessage());
                e.printStackTrace();
            }
        }
        return null;
    }
}

然后在permain或agentmain方法中inst.addTransformer(new MyTransformer());,其他步骤同之前,不再赘述。

Instrumentation接口相当于一个代理,当执行premain方法时,通过Instrumentation提供的API可以动态的添加管理JVM加载的Class文件,Instrumentation管理着ClassFileTransformer。
ClassFileTransformer接口可以动态的改变Class文件的字节码,在加载字节码的时候可以将字节码进行动态修改,具体实现需要自定义实现类来实现ClassFileTransformer接口

Java字节码生成框架大致有ASM、Javassist和byte buddy三种

  • ASM框架介绍及使用
    ASM是一种Java字节码操控框架,能够以二进制形式修改已有的类或是生成类,ASM可以直接生成二进制class文件也可以在类被加载入JVM之前动态改变类,只不过ASM在创建class字节码时说底层JVM的汇编指令,需要使用者对class组织结构和JVM汇编指令有一定的了解。由于Java 类存储在.class文件中,这些类文件中包含有:类名称、方法、属性及字节码,ASM从类文件中读入信息后改变类行为、分析类信息或者直接创建新的类。

    著名的使用到ASM的案例便是lambda表达式、CGLIB动态代理类

    ASM框架核心类包含
    ClassReader:该类用来解析编译过的class字节码文件
    ClassWriter:该类用来重新构建编译后的类,比如修改类名、属性、方法或者根据要求创建新的字节码文件
    ClassAdapter:实现了ClassVisitor接口,将对它的方法调用委托给另一个ClassVisitor对象

  • Javassist及使用
    Javassit相比于ASM要简单点,Javassit提供了更高级的API,当时执行效率上比ASM要差,因为ASM上直接操作的字节码。功能和JDK自带的反射功能类似,但是比反射要强大。

    Javassist核心类包括ClassPool:
    一个基于HashMap实现的CtClass对象容器,key上类名,value上这个类的CtClass对象
    CtClass:表示一个类,可以从ClassPool中获取
    CtMethods:表示一个类的方法
    CtFields:表示类中的属性

  • Byte Buddy及使用
    byte buddy是一个提供了API用于生成任意Java类工具包,可以生成和修改字节码。

3. Instrumentation的实现原理

说起Instrumentation的原理,就不得不先提起JVMTI:
JVMTI官网文档
JVMTI
JVMTI 是JVM Tool Interface 的缩写,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会调用一些事件的回调接口,这些接口可以给用户自行扩展来实现自己的逻辑。JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 和coverage analysis等工具的统一基础,在主流 Java 虚拟机中都有实现。

JVMTIAgent
JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式)
主要有三个函数:

  • Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的Agent_OnLoad函数
  • Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后通过load命令来加载agent,由ClassFileLoadHook event提供回调,调用Agent_OnAttach方法
  • Agent_OnUnload方法:在agent卸载时调用

回到主题,Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以在运行时动态加载

  • 启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式
  • 运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载,这种方式明显更加灵活,对监控目标启动也无限制,arthas的attach就是基于此
private void attachAgent(Configure configure) throws Exception {
        VirtualMachineDescriptor virtualMachineDescriptor = null;
        for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
            String pid = descriptor.id();
            if (pid.equals(Integer.toString(configure.getJavaPid()))) {
                virtualMachineDescriptor = descriptor;
            }
        }
        VirtualMachine virtualMachine = null;
        try {
            if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
                virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
            } else {
                virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
            }

            Properties targetSystemProperties = virtualMachine.getSystemProperties();
            String targetJavaVersion = targetSystemProperties.getProperty("java.specification.version");
            String currentJavaVersion = System.getProperty("java.specification.version");
            if (targetJavaVersion != null && currentJavaVersion != null) {
                if (!targetJavaVersion.equals(currentJavaVersion)) {
                    AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",
                                    currentJavaVersion, targetJavaVersion);
                    AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.",
                                    targetSystemProperties.getProperty("java.home"));
                }
            }

            virtualMachine.loadAgent(configure.getArthasAgent(),
                            configure.getArthasCore() + ";" + configure.toString());
        } finally {
            if (null != virtualMachine) {
                virtualMachine.detach();
            }
        }
    }

通过 VirtualMachine , 可以attach到当前指定的jvm pid上,然后 virtualMachine.loadAgent()将编写好的agent用于监控目标。

总结:

  1. Instrumentation相当于一个JVM级别的AOP

  2. Instrumentation在JVM启动的时候监听事件,如类加载事件,JVM触发来指定的事件通过回调通知,并创建一个 Instrumentation接口的实例,然后找到MANIFEST.MF中配置的实现了premain方法的Class,然后将Instrumentation实例传入premain方法中

  3. premain方法会在main方法之前执行,可以添加ClassFileTransfer来实现对Class文件字节码的动态修改(并不会修改Class文件中的字节码,而是修改已经被JVM加载的字节码)

  4. 修改字节码的技术可以使用开源的 ASM、javassist、byteBuddy等

https://blog.csdn.net/u010862794/article/details/87773434

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值