详解Java动态字节码技术

对 Debug 的好奇

初学 Java 时,我对 IDEA 的 Debug 非常好奇,不止是它能查看断点的上下文环境,更神奇的是我可以在断点处使用它的 Evaluate 功能直接执行某些命令,进行一些计算或改变当前变量。

刚开始语法不熟经常写错代码,重新打包部署一次代码耗时很长,我就直接面向 Debug 开发。在要编写的方法开始处打一个断点,在 Evaluate 框内一次次地执行方法函数不停地调整代码,没问题后再将代码复制出来放到 IDEA 里,再进行下一个方法的编写,这样就跟写 PHP 类似的解释性语言一样,写完即执行,非常方便。

但 Java 是静态语言,运行之前是要先进行编译的,难道我写的这些代码是被实时编译又”注入”到我正在 Debug 的服务里了吗?

随着对 Java 的愈加熟悉,我也了解了反射、字节码等技术,直到前些天的周会分享,有位同事分享了 Btrace 的使用和实现,提到了 Java 的 ASM 框架和 JVM TI 接口。 Btrace 修改代码能力的实现与 Debug 的 Evaluate 有很多相似之处,这大大吸引了我。分享就像一个引子,从中学到的东西只是皮毛,要了解它还是要自己研究。于是自己查看资料并写代码学习了下其具体实现。

ASM

实现 Evaluate 要解决的第一个问题就是怎么改变原有代码的行为,它的实现在 Java 里被称为动态字节码技术。

动态生成字节码

我们知道,我们编写的 Java 代码都是要被编译成字节码后才能放到 JVM 里执行的,而字节码一旦被加载到虚拟机中,就可以被解释执行。

字节码文件(.class)就是普通的二进制文件,它是通过 Java 编译器生成的。而只要是文件就可以被改变,如果我们用特定的规则解析了原有的字节码文件,对它进行修改或者干脆重新定义,这不就可以改变代码行为了么。

Java 生态里有很多可以动态生成字节码的技术,像 BCEL、Javassist、ASM、CGLib 等,它们各有自己的优势。有的使用复杂却功能强大、有的简单确也性能些差。

ASM 框架

ASM 是它们中最强大的一个,使用它可以动态修改类、方法,甚至可以重新定义类,连 CGLib 底层都是用 ASM 实现的。

当然,它的使用门槛也很高,使用它需要对 Java 的字节码文件有所了解,熟悉 JVM 的编译指令。虽然我对 JVM 的字节码语法不熟,但有大神开发了可以在 IDEA 里查看字节码的插件:ASM Bytecode Outline,在要查看的类文件里右键选择Show bytecode Outline即可以右侧的工具栏查看我们要生成的字节码。对照着示例,我们就可以很轻松地写出操作字节码的 Java 代码了。

而切到ASMified标签栏,我们甚至可以直接获取到 ASM 的使用代码。

常用方法

在 ASM 的代码实现里,最明显的就是访问者模式,ASM 将对代码的读取和操作都包装成一个访问者,在解析 JVM 加载到的字节码时调用。

ClassReader 是 ASM 代码的入口,通过它解析二进制字节码,实例化时它时,我们需要传入一个 ClassVisitor,在这个 Visitor 里,我们可以实现visitMethod()/visitAnnotation()等方法,用以定义对类结构(如方法、字段、注解)的访问方法。

而 ClassWriter 接口继承了 ClassVisitor 接口,我们在实例化类访问器时,将 ClassWriter “注入” 到里面,以实现对类写入的声明。

Instrument

介绍

字节码是修改完了,可是 JVM 在执行时会使用自己的类加载器加载字节码文件,加载后并不会理会我们做出的修改,要想实现对现有类的修改,我们还需要搭配 Java 的另一个库instrument。

instrument 是 JVM 提供的一个可以修改已加载类文件的类库。1.6以前,instrument 只能在 JVM 刚启动开始加载类时生效,之后,instrument 更是支持了在运行时对类定义的修改。

使用

要使用 instrument 的类修改功能,我们需要实现它的ClassFileTransformer接口定义一个类文件转换器。它唯一的一个transform()方法会在类文件被加载时调用,在 transform 方法里,我们可以对传入的二进制字节码进行改写或替换,生成新的字节码数组后返回,JVM 会使用 transform 方法返回的字节码数据进行类的加载。

JVM TI

定义完了字节码的修改和重定义方法,但我们怎么才能让 JVM 能够调用我们提供的类转换器呢?这里又要介绍到 JVM TI 了。

介绍

JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一个非常强大的对 JVM 操作的工具接口,通过这个接口,我们可以实现对 JVM 多种组件的操作,从JVMTM Tool Interface这里我们认识到 JVM TI 的强大,它包括了对虚拟机堆内存、类、线程等各个方面的管理接口。

JVM TI 通过事件机制,通过接口注册各种事件勾子,在 JVM 事件触发时同时触发预定义的勾子,以实现对各个 JVM 事件的感知和反应。

Agent

Agent 是 JVM TI 实现的一种方式。我们在编译 C 项目里链接静态库,将静态库的功能注入到项目里,从而才可以在项目里引用库里的函数。我们可以将 agent 类比为 C 里的静态库,我们也可以用 C 或 C++ 来实现,将其编译为 dll 或 so 文件,在启动 JVM 时启动。

这时再来思考 Debug 的实现,我们在启动被 Debug 的 JVM 时,必须添加参数-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 选项就指定了我们要加载的 Java Agent,jdwp 是 agent 的名字,在 linux 系统中,我们可以在 jre 目录下找到 jdwp.so 库文件。

Java 的调试体系 jdpa 组成,从高到低分别为jdi->jdwp->jvmti,我们通过 JDI 接口发送调试指令,而 jdwp 就相当于一个通道,帮我们翻译 JDI 指令到 JVM TI,最底层的 JVM TI 最终实现对 JVM 的操作。

使用

JVM TI 的 agent 使用很简单,在启动 agent 时添加 -agent 参数指定我们要加载的 agent jar包即可。

而要实现代码的修改,我们需要实现一个 instrument agent,它可以通过在一个类里添加premain()或agentmain()方法来实现。而要实现 1.6 以上的动态 instrument 功能,实现 agentmain 方法即可。

在 agentmain 方法里,我们调用Instrumentation.retransformClasses()方法实现对目标类的重定义。

另外往一个正在运行的 JVM 里动态添加 agent,还需要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的VirtualMachine类提供了 attach 一个本地 JVM 的功能,它需要我们传入一个本地 JVM 的 pid, tools.jar 可以在 jre 目录下找到。

agent生成

另外,我们还需要注意 agent 的打包,它需要指定一个 Agent-Class 参数指定我们的包括 agentmain 方法的类,可以算是指定入口类吧。

此外,还需要配置MANIFEST.MF文件的一些参数,允许我们重新定义类。如果你的 agent 实现还需要引用一些其他类库时,还需要将这些类库都打包到此 jar 包中,下面是我的 pom 文件配置。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <archive>
                    <manifestEntries>
                        <Agent-Class>asm.TestAgent</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        <Manifest-Version>1.0</Manifest-Version>
                        <Permissions>all-permissions</Permissions>
                    </manifestEntries>
                </archive>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
        </plugin>
    </plugins>
</build>

另外在打包时需要使用mvn assembly:assembl命令生成 jar-with-dependencies 作为 agent。

代码实现

我在测试时写了一个用以上技术实现了一个简单的字节码动态修改的 Demo。

被修改的类

TransformTarget 是要被修改的目标类,正常执行时,它会三秒输出一次 “hello”。

public class TransformTarget {
    public static void main(String[] args) {
        while (true) {
            try {
                Thread.sleep(3000L);
            } catch (Exception e) {
                break;
            }
            printSomething();
        }
    }

    public static void printSomething() {
        System.out.println("hello");
    }

}

Agent

Agent 是执行修改类的主体,它使用 ASM 修改 TransformTarget 类的方法,并使用 instrument 包将修改提交给 JVM。

入口类,也是代理的 Agent-Class。

public class TestAgent {
    public static void agentmain(String args, Instrumentation inst) {
        inst.addTransformer(new TestTransformer(), true);
        try {
            inst.retransformClasses(TransformTarget.class);
            System.out.println("Agent Load Done.");
        } catch (Exception e) {
            System.out.println("agent load failed!");
        }
    }
}

执行字节码修改和转换的类。

public class TestTransformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("Transforming " + className);
        ClassReader reader = new ClassReader(classfileBuffer);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter);
        reader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        return classWriter.toByteArray();
    }

    class TestClassVisitor extends ClassVisitor implements Opcodes {
        TestClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
            if (name.equals("printSomething")) {
                mv.visitCode();
                Label l0 = new Label();
                mv.visitLabel(l0);
                mv.visitLineNumber(19, l0);
                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("bytecode replaced!");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                Label l1 = new Label();
                mv.visitLabel(l1);
                mv.visitLineNumber(20, l1);
                mv.visitInsn(Opcodes.RETURN);
                mv.visitMaxs(2, 0);
                mv.visitEnd();
                TransformTarget.printSomething();
            }
            return mv;
        }
    }
}

Attacher

使用 tools.jar 里方法将 agent 动态加载到目标 JVM 的类。

public class Attacher {
    public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {

        VirtualMachine vm = VirtualMachine.attach("34242"); // 目标 JVM pid
        vm.loadAgent("/path/to/agent.jar");
    }
}

这样,先启动 TransformTarget 类,获取到 pid 后将其传入 Attacher 里,并指定 agent jar,将 agent attach 到 TransformTarget 中,原来输出的 “hello” 就变成我们想要修改的 “bytecode replaced!” 了。

小结

掌握了字节码的动态修改技术后,再回头看 Btrace 的原理就更清晰了,稍微摸索一下我们也可以实现一个简版的。另外很多大牛实现的各种 Java 性能分析工具的技术栈也不外如此,了解了这些,未来我们也可以写出适合自己的工具,至少能对别人的工具进行修改~

不得不说 Java 的生态真的非常繁荣,当真是博大精深,查阅一个模块的资料时能总引出一大堆新的概念,永远有学不完的新东西。

以上就是详解Java动态字节码技术的详细内容,更多关于Java动态字节码技术的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Java字节码编程之非常好用的javassist

    一.Javassist入门 (一)Javassist是什么 Javassist是可以动态编辑Java字节码的类库.它可以在Java程序运行时定义一个新的类,并加载到JVM中:还可以在JVM加载时修改一个类文件.Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的. (二)Javassist核心API 在Javassist中每个需要编辑的class都对应一个CtCLass实例,CtClass的含义是编译时的类(compile time class),这些类会存储在Class Poo

  • java 中如何获取字节码文件的相关内容

    java 中如何获取字节码文件的相关内容 反射机制是指在运行状态中,对任意一个类(class文件),都能知道这个类的所有属性和方法:对任意一个对象,都能调用这个对象的方法和属性.这种动态的获取信息和动态的调用对象的方法的功能称为--Java语言的反射机制. 简单点说,动态的获取类中的信息,这就是Java的反射机制. 在Java的反射机制中,我们可以通过配置文件信息,然后通过类名来获取类中包含的详细信息,如构造函数.成员变量和成员函数等.在接下来,作者将分别演示如何通过类名来获取类中包含的信息.

  • java 获取字节码文件的几种方法总结

    java 获取字节码文件的几种方法总结 在本文中,以Person类为例,将分别演示获取该类字节码文件的三种方式, 其具体思想及代码如下所示: public class Person { private int age; private String name; public Person() { System.out.println("person run"); } public Person(String name, int age) { this.age = age; this.n

  • java字节码框架ASM的深入学习

    一.什么是ASM ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能.ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为.Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称.方法.属性以及 Java 字节码(指令).ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类. 使用ASM框架需要导入asm的jar包,下载链接:

  • java字节码框架ASM操作字节码的方法浅析

    之前我们已经对ASM进行的详细的介绍,需要的朋友们可以点击这里:java字节码框架ASM的深入学习 JVM的类型签名对照表 Type Signature Java Type Z boolean B byte C char S short I int J long F float D double L fully-qualified-class ;fully-qualified-class [ type type[] ( arg-types ) ret-type method type 比如,ja

  • java获取版本号及字节码编译版本方法示例

    前言 之所以会有这篇文章,是因为公司的开发环境比较老,寻找一些jar包的时候总是会纠结对应的编译版本,感觉很麻烦,所以写了一个工具类用于读取class或jar文件的编译版本,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 示例代码 package com.jinggujin.util; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.

  • Java字节码中jvm实例用法

    要想使Java运行,我们可以设计一个面向Java语言特性的虚拟机,并通过编译器将Java程序转换为它可以识别的指令序列,也称为Java字节码.由于Java字节码指令的操作码被固定为一个字节,所以它的名字就这样命名了.本篇要带来的是Java字节码中jvm的使用,主要分为两个操作视角,一起来看看具体内容吧. 1.虚拟机视角 在执行Java代码时,首先需要将编译后的class文件装入Java虚拟机.装入的Java类将存储在方法区(MethodArea)中.虚拟机会在实际运行时执行方法区内的代码.Jav

  • 浅谈javap命令拆解字节码文件

    目的拆解分析反编译字节码 解析成人能够理解的结构 ,然后再对字节码文件进一步分析 源代码 public class test { private static int classV =2; public static void main(String[] args) { classV =200; int localV =4; localV =400; } } 二进制 idea bin_ed插件查看. 看不懂 那就使用人能看的懂的汇编语言查看类文件结构和代码指令. javap 指令和选项 0:无选

  • Javassist如何操作Java 字节码

    一.开篇 说起 AOP 小伙伴们肯定很熟悉,无论是 JDK 动态代理或者是 CGLIB 等,其底层都是通过操作 Java 字节码来实现代理.常用的一些操作字节码的技术有 ASM.AspectJ.Javassist 等. ASM 其设计和实现是尽可能小而且快,更专注于性能.它在指令的层面来操作,所以使用它需要对 JVM 的指令有所了解,门槛较高,CGLIB 就使用了 ASM 技术. AspectJ 扩展了 Java 语言,定义了一系列 AOP 语法,在 JVM 中运行需要使用特定的编译器生成遵守

  • 详解Java动态字节码技术

    对 Debug 的好奇 初学 Java 时,我对 IDEA 的 Debug 非常好奇,不止是它能查看断点的上下文环境,更神奇的是我可以在断点处使用它的 Evaluate 功能直接执行某些命令,进行一些计算或改变当前变量. 刚开始语法不熟经常写错代码,重新打包部署一次代码耗时很长,我就直接面向 Debug 开发.在要编写的方法开始处打一个断点,在 Evaluate 框内一次次地执行方法函数不停地调整代码,没问题后再将代码复制出来放到 IDEA 里,再进行下一个方法的编写,这样就跟写 PHP 类似的

  • 详解Java动态代理的实现及应用

    详解Java动态代理的实现及应用 Java动态代理其实写日常业务代码是不常用的,但在框架层一起RPC框架的客户端是非常常见及重要的.spring的核心思想aop的底层原理实现就使用到了java的动态代理技术. 使用代理可以实现对象的远程调用以及aop的实现. java的动态代理的实现,主要依赖InvoctionHandler(接口)和Proxy(类)这两个. 下面是一个例子 实现的代理的一般需要有个接口 package com.yasin.ProxyLearn; public interface

  • 详解JAVA动态代理

    文档更新说明 2018年09月24日 v1.0 初稿 代理在生活中很常见,比如说婚介网站,其实就是找对象的代理:还有社保代理.人事代理:还有找黄牛抢票,其实也是一种代理:而这些代理,在JAVA中也是有对应实现的. 1.为什么要动态代理 动态代理的作用其实就是在不修改原代码的前提下,对已有的方法进行增强. 关键点: 不修改原来已有的代码(满足设计模式的要求) 对已有方法进行增强 2.举个栗子 我们用一个很简单的例子来说明:Hello类,有一个introduction方法. 现在我们的需求就是不修改

  • 详解java动态代理模式

    本文针对java动态代理进行知识点整理,具体内容如下 一. JAVA的动态代理(比较官方说法) 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处 理消息.过滤消息.把消息转发给委托类,以及事后处理消息等. 代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的 对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提 供特定的服务. 按照代理的创建时期,代理类可以分为两种. 静态代理:由程序员创建或特定工

  • 详解Java动态加载数据库驱动

    问题背景 在同一套系统中,要支持连接访问各种流行的数据库,以及同一数据库的不同版本,例如,oracle9i.oracle10g.oracle11g.oracle12c.sqlserver2000.sqlserver2005.sqlserver2008.sqlserver2012等,其中就会碰到一些问题,就是不同的数据库,数据库驱动肯定不同,对于这个问题到好解决,只需要将相应的驱动加入即可:然而对于同种数据库,不同版本时,而且不同版本的数据库驱动不仅不兼容,同时存在还会出现冲突,例如,能满足sql

  • 详解java动态代理的2种实现方式

    java的动态代理在接java的api上有说明,这里就不写了.我理解的代理: 对特定接口中特定方法的功能进行扩展,这就是代理.代理是通过代理实例关联的调用处理程序对象调用方法. 下面通过一个例子看一下: 接口: public interface Num { void show(); int getNum(); int getProduct(int x); } 实现类: public class MyNum implements Num { @Override public int getNum(

  • 详解Java动态代理的实现机制

    一.概述 代理是一种设计模式,其目的是为其他对象提供一个代理以控制对某个对象的访问,代理类负责为委托类预处理消息,过滤消息并转发消息以及进行消息被委托类执行后的后续处理.为了保持行为的一致性,代理类和委托类通常会实现相同的接口. 按照代理的创建时期,代理类可分为两种: 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,也就是说在程序运行前代理类的.class文件就已经存在. 动态代理:在程序运行时运用反射机制动态创建生成. 下面在将动态代理的实现机制之前先简单介绍一下静态代理. 二

  • Java ArrayList集合详解(Java动态数组)

    目录 一.ArrayList集合的概述和基本使用 1.概述 2.基本使用 二.ArrayList集合的详细介绍 1.定义一个ArrayList集合 2.ArrayList集合常用的方法 3.将"类"存入ArrayList集合 4.遍历ArrayList集合 5.将基本数据类型存入ArrayList集合 6.ArrayList作为方法的参数 7.ArrayList作为方法的返回值 一.ArrayList集合的概述和基本使用 1.概述 ArrayList是集合的一种实现,Collectio

  • 详解Java Proxy动态代理机制

    一.Jvm加载对象 在说Java动态代理之前,还是要说一下Jvm加载对象的过程,这个依旧是理解动态代理的基础性原理: Java类即源代码程序.java类型文件,经过编译器编译之后就被转换成字节代码.class类型文件,类加载器负责读取字节代码,并转换成java.lang.Class对象,描述类在元数据空间的数据结构,类被实例化时,堆中存储实例化的对象信息,并且通过对象类型数据的指针找到类. 过程描述:源码->.java文件->.class文件->Class对象->实例对象 所以通过

  • 详解Java中的字节码增强技术

    目录 1.字节码增强技术 2.常见技术 3.ASM 3.1 测试 Main 3.2 测试 CustomerClassVisitor 3.3 测试 Test 1.字节码增强技术 字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术. 参考地址 2.常见技术 技术分类 类型 静态增强 AspectJ 动态增强 ASM.Javassist.Cglib.Java Proxy 3.ASM <dependency> <groupId>org.ow2.asm</gro

随机推荐