[Java] Byte Buddy 和 InvocationHandler 的结合

10 阅读5分钟

背景

[Java] 从 class 文件看动态代理 一文中,我们分析了 JDK 所生成的代理类的结构。那么在使用 Byte Buddy 时,是否也可以使用 InvocationHandler 呢?如果可以的话,所生成的代理类又会是什么样子呢?让我们一起来探索吧。

要点

通过观察代理类的 class 文件,会发现

  • 代理类中有一个 java.lang.reflect.InvocationHandler\text{java.lang.reflect.InvocationHandler} 类型的静态字段(我们简称为 ihstaticih_{static} 吧)
  • 这个静态字段 ihstaticih_{static} 和我们指定的 java.lang.reflect.InvocationHandler\text{java.lang.reflect.InvocationHandler} 实例(我们把后者简称为 ihvarih_{var} 吧)是同一个对象
  • java.lang.Runnable\text{java.lang.Runnable} 中的 run() 方法为例,如果我们执行代理类中的 run() 方法,那么它会调用 ihstaticih_{static}invoke(Object, Method, Object[]) 方法,即 ihvarih_{var}invoke(Object, Method, Object[]) 方法

项目结构

我们用一个小项目来进行探索。在这个项目顶层执行 tree . 命令,可以看到如下结果 ⬇️

.
├── pom.xml
└── src
    └── main
        └── java
            └── org
                └── example
                    └── MyProxy.java

6 directories, 2 files

MyProxy.java

其中 MyProxy.java 的内容如下 ⬇️

package org.example;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.InvocationHandlerAdapter;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.reflect.InvocationHandler;

public class MyProxy {
    public static void main(String[] args) throws ReflectiveOperationException {
        // Create a naive InvocationHandler
        InvocationHandler handler = (proxy, method, params) -> {
            System.out.println("Hello world");
            return null;
        };

        // Generate the proxy class
        Class<? extends Runnable> proxyClass = new ByteBuddy()
                .subclass(Runnable.class)
                .method(ElementMatchers.any())
                .intercept(InvocationHandlerAdapter.of(handler))
                .make()
                .load(MyProxy.class.getClassLoader())
                .getLoaded();

        // Create a new instance of the proxy
        Runnable runnable = proxyClass.getDeclaredConstructor().newInstance();
        runnable.run();
    }
}

说明:MyProxy.java 中的代码并不是我原创的。我在 Google 搜了 byte buddy invocationhandler example 就能看到比较有用的代码示例 ⬇️,我是在这些代码的基础上写的 MyProxy.java

image.png

pom.xml

pom.xml 的内容如下 ⬇️

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>byte-byddy-study</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.18.7</version>
        </dependency>
    </dependencies>

</project>

运行结果

运行 MyProxy 中的 main 方法,可以看到如下的结果 ⬇️

Hello world

分析

MyProxy 类的 main 方法

运行 main 方法时,发生了什么呢?一共发生了三件事 ⬇️

image.png

MyProxyV2: 将代理类保存至 class 文件中

Byte Buddy 生成了代理类,但是我们暂时看不到这个代理类长什么样子。我们在 MyProxy.java 的基础上可以写出如下的代码 ⬇️ (请将如下代码保存为 MyProxyV2.java,并将其保存到何 MyProxy.java 平级的位置)

package org.example;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.InvocationHandlerAdapter;
import net.bytebuddy.matcher.ElementMatchers;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;

public class MyProxyV2 {
    public static void main(String[] args) throws IOException {
        // Create a naive InvocationHandler
        InvocationHandler handler = (proxy, method, params) -> {
            System.out.println("Hello world");
            return null;
        };

        // Generate and save the proxy class
        new ByteBuddy()
                .subclass(Runnable.class)
                .name("org.example.SimpleProxy")
                .method(ElementMatchers.any())
                .intercept(InvocationHandlerAdapter.of(handler))
                .make()
                .saveIn(new File("."));
    }
}

现在执行 tree . 命令,会看到如下的结果

.
├── pom.xml
└── src
    └── main
        └── java
            └── org
                └── example
                    ├── MyProxy.java
                    └── MyProxyV2.java

6 directories, 3 files

执行 MyProxyV2 中的 main 方法,它不会在控制台输出文字,但是我们会看到当前目录下多了一个 org 目录。我执行 tree org 命令后,看到的结果如下

org
└── example
    └── SimpleProxy.class

2 directories, 1 file

我们可以用 javap 命令自行分析 class 文件的内容 ⬇️

javap -v -p org.example.SimpleProxy

但是这样的工作太枯燥了,而且手动反编译很容易出错,所以我们还是直接借助 Intellij IDEA (Community Edition) 来查看这个 class 文件反编译的结果吧 ⬇️

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class SimpleProxy implements Runnable {
    // $FF: synthetic field
    public static volatile InvocationHandler invocationHandler$26p2530;
    // $FF: synthetic field
    private static final Method cachedValue$qnsSwswr$4cscpe1 = Object.class.getMethod("toString");
    // $FF: synthetic field
    private static final Method cachedValue$qnsSwswr$m7m8693 = Runnable.class.getMethod("run");
    // $FF: synthetic field
    private static final Method cachedValue$qnsSwswr$5j4bem0 = Object.class.getMethod("equals", Object.class);
    // $FF: synthetic field
    private static final Method cachedValue$qnsSwswr$7m9oaq0 = Object.class.getDeclaredMethod("clone");
    // $FF: synthetic field
    private static final Method cachedValue$qnsSwswr$9pqdof1 = Object.class.getMethod("hashCode");

    public boolean equals(Object var1) {
        return (Boolean)invocationHandler$26p2530.invoke(this, cachedValue$qnsSwswr$5j4bem0, new Object[]{var1});
    }

    public String toString() {
        return (String)invocationHandler$26p2530.invoke(this, cachedValue$qnsSwswr$4cscpe1, (Object[])null);
    }

    public int hashCode() {
        return (Integer)invocationHandler$26p2530.invoke(this, cachedValue$qnsSwswr$9pqdof1, (Object[])null);
    }

    protected Object clone() throws CloneNotSupportedException {
        return invocationHandler$26p2530.invoke(this, cachedValue$qnsSwswr$7m9oaq0, (Object[])null);
    }

    public void run() {
        invocationHandler$26p2530.invoke(this, cachedValue$qnsSwswr$m7m8693, (Object[])null);
    }

    public SimpleProxy() {
        super();
    }
}

为了方便理解,我画了对应的类图 ⬇️

image.png

org.example.SimpleProxy\text{org.example.SimpleProxy} 中定义了 6 个字段和 5 个方法(如果不考虑构造函数的话) 大致浏览一下反编译的结果,会发现这 5 个方法的逻辑是类似的,所以我们仔细看其中一个就够了。我们以 run() 方法为例,细看一下它背后发生了什么。

run() 方法的内容只有一点点 ⬇️

public void run() {
    invocationHandler$26p2530.invoke(this, cachedValue$qnsSwswr$m7m8693, (Object[])null);
}

其中 invocationHandler$26p2530cachedValue$qnsSwswr$m7m8693 具体是什么,我在下图中用红色框标记出来了 ⬇️

image.png

由此可见

  • invocationHandler$26p2530java.lang.reflect.InvocationHandler\text{java.lang.reflect.InvocationHandler} 的一个实例
  • cachedValue$qnsSwswr$m7m8693java.lang.reflect.Method\text{java.lang.reflect.Method} 的一个实例
    • 它代表了 java.lang.Runnable\text{java.lang.Runnable} 中的 run() 方法

那么 invocationHandler$26p2530 具体是什么呢?既然它是 java.lang.reflect.InvocationHandler\text{java.lang.reflect.InvocationHandler} 的实例,那么它会不会就是 MyProxyV2main 方法里的 handler 呢?我们写点代码验证一下 ⬇️ (请将如下代码保存为 MyProxyV3.java,并将其保存到何 MyProxy.java 平级的位置)

MyProxyV3: 验证 InvocationHandler 来自哪里

package org.example;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.InvocationHandlerAdapter;
import net.bytebuddy.matcher.ElementMatchers;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;

public class MyProxyV3 {
    public static void main(String[] args) throws IOException, IllegalAccessException {
        // Create a naive InvocationHandler
        InvocationHandler handler = (proxy, method, params) -> {
            System.out.println("Hello world");
            return null;
        };

        final String proxyClassName = "org.example.SimpleProxy";
        // Generate and the proxy class
        Class<? extends Runnable> proxyClass = new ByteBuddy()
                .subclass(Runnable.class)
                .name(proxyClassName)
                .method(ElementMatchers.any())
                .intercept(InvocationHandlerAdapter.of(handler))
                .make()
                .load(MyProxy.class.getClassLoader())
                .getLoaded();

        Field[] fields = proxyClass.getFields();
        for (Field field : fields) {
            if (field.getType() == InvocationHandler.class) {
                String message =
                        String.format(
                                "%s 里的 InvocationHandler 类型的字段和当前 main 方法里的 handler 是同一个对象吗? ⬇️",
                                proxyClassName
                        );
                System.out.println(message);
                System.out.println(field.get(null) == handler);
            }
        }
    }
}

现在执行 tree src 命令,会看到如下的结果

src
└── main
    └── java
        └── org
            └── example
                ├── MyProxy.java
                ├── MyProxyV2.java
                └── MyProxyV3.java

5 directories, 3 files

运行 MyProxyV3 中的 main 方法,会看到如下的结果 ⬇️

org.example.SimpleProxy 里的 InvocationHandler 类型的字段和当前 main 方法里的 handler 是同一个对象吗? ⬇️
true

由此可见,我们的猜测是正确的,也就是说,下面两个对象引用的是同一个东西

  • org.example.SimpleProxy\text{org.example.SimpleProxy} 里的 java.lang.reflect.InvocationHandler\text{java.lang.reflect.InvocationHandler} 类型的字段
  • MyProxyV3main 方法里的 handler

此时再看看 MyProxyV2 ⬇️ 就能 main 方法里的 handler 是如何起作用了

image.png

其他

画 "MyProxymain 方法做了什么?" 这张图所用到的代码

我借助了 PlantUML 的插件来画那张图,用到的代码如下 ⬇️

@startwbs
'https://plantuml.com/wbs-diagram

title <i>MyProxy</i> 的 <i>main</i> 方法做了什么?

* <i>MyProxy</i> 的 <i>main</i> 方法做了什么?
** 1. 创建 <i>InvocationHandler</i> 的实例: <i>handler</i>
** 2. 借助 <i>Byte Buddy</i> 生成 <i>Runnable</i> 的一个动态代理类
*** 一些准备工作 (本文不关心其中的细节)
*** 将这个动态代理类的 <i>Class</i> 对象保存在 <b><i>proxyClass</i></b> 里
**:3. 借助 <b><i>proxyClass</i></b> 生成代理类的一个实例 <i>runnable</i>
调用 <i>runnable</i> 的 <i>run()</i> 方法;
@endwbs

画 "org.example.SimpleProxy 的类图" 所用到的代码

我借助了 PlantUML 的插件来画那张图,用到的代码如下 ⬇️

@startuml
'https://plantuml.com/class-diagram

title <i>org.example.SimpleProxy</i> 的类图

interface java.lang.Runnable
class org.example.SimpleProxy

java.lang.Runnable <|.. org.example.SimpleProxy

class org.example.SimpleProxy {
     + {static} volatile InvocationHandler invocationHandler$26p2530
     - {static} final Method cachedValue$qnsSwswr$4cscpe1
     - {static} final Method cachedValue$qnsSwswr$m7m8693
     - {static} final Method cachedValue$qnsSwswr$5j4bem0
     - {static} final Method cachedValue$qnsSwswr$7m9oaq0
     - {static} final Method cachedValue$qnsSwswr$9pqdof1
     + boolean equals(Object var1)
     + String toString()
     + int hashCode()
     # Object clone() throws CloneNotSupportedException
     + void run()
     + SimpleProxy()
}

@enduml