字节码基于Byte Buddy语法创建的第一个 HelloWorld

Posted 九师兄

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节码基于Byte Buddy语法创建的第一个 HelloWorld相关的知识,希望对你有一定的参考价值。

1.概述

转载:https://github.com/fuzhengwei/itstack-demo-bytecode

相对于 小傅哥 之前编写的字节码编程; ASM 、 Javassist 系列, Byte Buddy 玩法上更加高级,你可以完全不需要了解一个类和方法块是如何通过 指令码 LDC、LOAD、STORE、IRETURN... 生成出来的。

就像它的官网介绍;

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外, Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外, Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。

比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。

2015年10月,Byte Buddy被 Oracle 授予了 Duke’s Choice大奖。该奖项对Byte Buddy的“ Java技术方面的巨大创新 ”表示赞赏。我们为获得此奖项感到非常荣幸,并感谢所有帮助Byte Buddy取得成功的用户以及其他所有人。我们真的很感激!

除了这些简单的介绍外,还可以通过官网: https://bytebuddy.net ,去了解更多关于 Byte Buddy的内容。

好! 那么接下来,我们开始从 HelloWorld 开始。深入了解一个技能前,先多多运行,这样总归能让找到学习的快乐。

2.环境

首先我们引入如下的maven参数


    <properties>
        <asm.version>9.0</asm.version>
        <slf4j-api.version>1.7.28</slf4j-api.version>
        <byte-buddy.version>1.12.11</byte-buddy.version>
        <fastjson.version>1.2.76</fastjson.version>
        <lombok.version>1.18.16</lombok.version>
    </properties>


    <dependencies>

        <!-- 日志 相关 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>$slf4j-api.version</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>$slf4j-api.version</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>$fastjson.version</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>$lombok.version</version>
        </dependency>

        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>$byte-buddy.version</version>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>$byte-buddy.version</version>
        </dependency>

        <dependency>
            <groupId>com.bytebuddy</groupId>
            <artifactId>byte-buddy-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>



        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

3.案例目标

每一个程序员,都运行过 N 多个 HelloWorld ,就像很熟悉的 Java

public class Hi 

    public static void main(String[] args) 
        System.out.println("Byte-buddy Hi HelloWorld By 小傅哥(bugstack.cn)");
    

 

那么我们接下来就通过使用动态字节码生成的方式,来创建出可以输出 HelloWorld 的程序。

新知识点的学习不要慌,最主要是找到一个可以入手的点,通过这样的一个点去慢慢解开整个程序的面

4.技术实现

4.1 官网经典例子

在我们看官网文档中,从它的介绍了就已经提供了一个非常简单的例子,用于输出 HelloWorld ,我们在这展示并讲解下。

案例代码:

 /**
     * 测试点: 测试官网案例
     * <p>
     * 运行结果输出
     * <p>
     * Hello World!
     *
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    @Test
    public void main() throws InstantiationException, IllegalAccessException 
        String helloWorld = new ByteBuddy()
                .subclass(Object.class)
                .method(named("toString"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .toString();
        System.out.println(helloWorld); // Hello World!
    

他的运行结果就是一行, Hello World! ,整个代码块核心功能就是通过method(named("toString")) ,找到 toString 方法,再通过拦截 intercept ,设定此方法的返回值。 FixedValue.value("Hello World!") 。到这里其实一个基本的方法就通过 Byte-buddy ,改造完成。

接下来的这一段主要是用于加载生成后的 Class 和执行,以及调用方法 toString() 。也就是最终我们输出了想要的结果。那么,如果你不能看到这样一段方法块,把我们的代码改造后的样子,心里还是有点虚。那么,我们通过字节码输出到文件,看下具体被改造后的样子,如下;编译后的Class文件, ByteBuddyHelloWorld.class

public class HelloWorld 
	public String toString() 
		return "Hello World!";
	 
	public HelloWorld() 
	

在官网来看,这是一个非常简单并且能体现 Byte buddy 的例子。但是与我们平时想创建出来的 main
方法相比,还是有些差异。那么接下来,我们尝试使用字节码编程技术创建出这样一个方法。

4.2 字节码创建类和方法

接下来的例子会通过一点点的增加代码梳理,不断的把一个方法完整的创建出来。

4.2.1 定义输出字节码方法

为了可以更加清晰的看到每一步对字节码编程后,所创建出来的方法样子(clazz),我们需要输出字节码
生成 clazz 。在Byte buddy中默认提供了一个 dynamicType.saveIn() 方法,我们暂时先不使用,
而是通过字节码进行保存。

 public static void writeBytes(String filepath, byte[] bytes) 
        System.out.println("准备写出文件:" + filepath);
        File file = new File(filepath);
        File dirFile = file.getParentFile();
        mkdirs(dirFile);

        try (OutputStream out = new FileOutputStream(filepath);
             BufferedOutputStream buff = new BufferedOutputStream(out)) 
            buff.write(bytes);
            buff.flush();
         catch (IOException e) 
            e.printStackTrace();
        

        if (Const.DEBUG) System.out.println("file://" + filepath);
    

4.2.2 创建类信息

 /**
     * 测试点:测试生成一个最简单的类
     * 运行结果如下
     * <p>
     * package com.bytebuddy;
     * <p>
     * public class HelloWorld 
     * public HelloWorld() 
     * 
     * 
     * 可以看到生成了一个简单的类
     *
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    @Test
    public void makeClassToFile() throws InstantiationException, IllegalAccessException 

        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("com.bytebuddy.HelloWorld")
                .make();
        byte[] bytes = dynamicType.getBytes();
        String filePathNotTest = FileUtils.getFilePathNotTest("");
        String clazz = filePathNotTest + "HelloWorld.class";
        // 输出类字节码
        FileUtils.writeBytes(clazz, bytes);
    

4.2.3 创建main方法

 /**
     * 测试点:测试生成一个带main方法的类
     * <p>
     * 此时生成的文件内容如下
     * <p>
     * <p>
     * package com.bytebuddy;
     * <p>
     * public class HelloWorld 
     * public static void main(String[] args) 
     * String var10000 = "Hello World!";
     * 
     * <p>
     * public HelloWorld() 
     * 
     * 
     */
    @Test
    public void makeMainClassToFile() throws InstantiationException, IllegalAccessException 


        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("com.bytebuddy.HelloWorld")
                .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
                .withParameter(String[].class, "args")
                .intercept(FixedValue.value("Hello World!"))
                .make();

        byte[] bytes = dynamicType.getBytes();
        String filePathNotTest = FileUtils.getFilePathNotTest("");
        String clazz = filePathNotTest + "HelloWorld.class";
        // 输出类字节码
        FileUtils.writeBytes(clazz, bytes);
    

与上面相比新增的代码片段;

  1. defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC) ,定义方法;
    名称、返回类型、属性public static withParameter(String[].class, "args") ,定义参数;参数类型、参数名称
    intercept(FixedValue.value("Hello World!")) ,拦截设置返回值,但此时还能满足我们的要求。

  2. 这里有一个知识点, Modifier.PUBLIC + Modifier.STATIC ,这是一个是二进制相加,每一个类型
    都在二进制中占有一位。例如 1 2 4 8 … 对应的二进制占位 1111 。所以可以执行相加运算,并又
    能保留原有单元的属性。

4.2.4 委托函数使用

为了能让我们使用字节码编程创建的方法去输出一段 Hello World ,那么这里需要使用到 委托 。


    /**
     * 测试点:测试生成一个类,该类调用其他类的方法
     *
     * 生成结果如下
     *
     * <code>
     * package com.bytebuddy;
     *
     * import com.bytebuddy.entity.Hi;
     *
     * public class HelloWorld 
     * public static void main(String[] args) 
     * Hi.main(args);
     * 
     *
     * public HelloWorld() 
     * 
     * 
     * </code>
     *
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    @Test
    public void makeCallMethodClassToFile() throws InstantiationException, IllegalAccessException 

        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("com.bytebuddy.HelloWorld")
                .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC)
                .withParameter(String[].class, "args")
                .intercept(MethodDelegation.to(Hi.class))
                .make();

        byte[] bytes = dynamicType.getBytes();
        String filePathNotTest = FileUtils.getFilePathNotTest("");
        String clazz = filePathNotTest + "HelloWorld.class";
        // 输出类字节码
        FileUtils.writeBytes(clazz, bytes);
    

整体来看变化并不大,只有 intercept(MethodDelegation.to(Hi.class)) ,使用了一段委托
函数,真正去执行输出的是另外的函数方法。

MethodDelegation,需要是 public 类被委托的方法与需要与原方法有着一样的入参、出参、方法名,否则不能映射上

5.总结

在本章节 Byte buddy 中,需要掌握几个关键信息;创建方法、定义属性、拦截委托、输出字节码,以及最终的运行。这样的一个简单过程,可以很快的了解到如何使用 Byte buddy 。

本系列文章后续会继续更新,把常用的 Byte buddy 方法通过实际的案例去模拟建设,在这个过程中加强学习使用。一些基础知识也可以通过官方文档进行学习;https://bytebuddy.net

在学习整理的过程中发现,关于字节码编程方面的资料并不是很全,主要源于大家平时的开发中基本是用不到的,谁也不可能总去修改字节码。但对于补全这样的成体系完善技术栈资料,却可以帮助很多需要的人。因此我也会持续输出类似这样空白的技术文章。

以上是关于字节码基于Byte Buddy语法创建的第一个 HelloWorld的主要内容,如果未能解决你的问题,请参考以下文章

字节码基于javassist的第一个案例helloworld

字节码Byte-buddy 使用委托实现抽象类方法并注入自定义 注解信息

JAVA动态字节码实现方式对比之Byte Buddy

10-java安全基础——javassist字节码编程

10-java安全基础——javassist字节码编程

10-java安全基础——javassist字节码编程