[Java] Byte Buddy 和 InvocationHandler 的结合

背景

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

要点

项目结构

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

text 复制代码
.
├── pom.xml
└── src
    └── main
        └── java
            └── org
                └── example
                    └── MyProxy.java

6 directories, 2 files

MyProxy.java

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

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

pom.xml

pom.xml 的内容如下 ⬇️

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 方法,可以看到如下的结果 ⬇️

text 复制代码
Hello world

分析

MyProxy 类的 main 方法

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

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

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

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 . 命令,会看到如下的结果

text 复制代码
.
├── pom.xml
└── src
    └── main
        └── java
            └── org
                └── example
                    ├── MyProxy.java
                    └── MyProxyV2.java

6 directories, 3 files

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

text 复制代码
org
└── example
    └── SimpleProxy.class

2 directories, 1 file

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

bash 复制代码
javap -v -p org.example.SimpleProxy

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

java 复制代码
//
// 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();
    }
}

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

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

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

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

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

由此可见

  • invocationHandler$26p2530 是 <math xmlns="http://www.w3.org/1998/Math/MathML"> java.lang.reflect.InvocationHandler \text{java.lang.reflect.InvocationHandler} </math>java.lang.reflect.InvocationHandler 的一个实例
  • cachedValue$qnsSwswr$m7m8693 是 <math xmlns="http://www.w3.org/1998/Math/MathML"> java.lang.reflect.Method \text{java.lang.reflect.Method} </math>java.lang.reflect.Method 的一个实例
    • 它代表了 <math xmlns="http://www.w3.org/1998/Math/MathML"> java.lang.Runnable \text{java.lang.Runnable} </math>java.lang.Runnable 中的 run() 方法

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

MyProxyV3: 验证 InvocationHandler 来自哪里

java 复制代码
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 命令,会看到如下的结果

text 复制代码
src
└── main
    └── java
        └── org
            └── example
                ├── MyProxy.java
                ├── MyProxyV2.java
                └── MyProxyV3.java

5 directories, 3 files

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

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

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

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> org.example.SimpleProxy \text{org.example.SimpleProxy} </math>org.example.SimpleProxy 里的 <math xmlns="http://www.w3.org/1998/Math/MathML"> java.lang.reflect.InvocationHandler \text{java.lang.reflect.InvocationHandler} </math>java.lang.reflect.InvocationHandler 类型的字段
  • MyProxyV3main 方法里的 handler

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

其他

画 "MyProxy 中的 InvocationHandler 实例如何发挥作用?" 这张图所用到的代码

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

puml 复制代码
@startmindmap
'https://plantuml.com/mindmap-diagram

title <i>MyProxy</i> 中的 <i>InvocationHandler</i> 实例如何发挥作用?
caption \n\n
' caption 中的内容是为了防止掘金平台生成的水印遮盖图中的文字

* <i>Byte Buddy</i> 会生成一个代理类
*:代理类中有一个 <b><i>java.lang.reflect.InvocationHandler</i></b> 类型的静态字段
我们将其简称为 <i>ih<sub>static</sub></i>;
* 我们将 <i>MyProxy</i> 类 <i>main</i> 方法里的 <b><i>java.lang.reflect.InvocationHandler</i></b> 实例简称为 <i>ih<sub>var</sub></i>
* <i>ih<sub>static</sub></i> 和 <i>ih<sub>var</sub></i> 引用的是 <b>同一个对象</b>
*:以 <i>java.lang.Runnable</i> 中的 <i>run()</i> 方法为例
如果我们执行代理类中的 <i>run()</i> 方法
那么它会调用 <i>ih<sub>static</sub></i> 的 <i>invoke(Object, Method, Object[])</i> 方法
即 <i>ih<sub>var</sub></i> 的 <i>invoke(Object, Method, Object[])</i> 方法
后者的逻辑我们可以自由掌控
(在本文的例子里只是输出了 <i>"Hello world"</i>);


@endmindmap

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

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

puml 复制代码
@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 的插件来画那张图,用到的代码如下 ⬇️

puml 复制代码
@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
相关推荐
xieliyu.2 小时前
Java、多态
java·开发语言
feng尘2 小时前
Java线程池的执行流程与常见配置
java
yaoxin5211232 小时前
364. Java IO API - 复制文件和目录
java·开发语言
独断万古他化2 小时前
【Java 实战项目】多用户网页版聊天室:项目总览与用户 & 好友管理模块实现
java·spring boot·后端·websocket·mybatis
白露与泡影2 小时前
金三银四高频 Java 面试题及答案整理 (建议收藏)
java·开发语言
小杍随笔2 小时前
【Rust 半小时速成(2024 Edition 更新版)】
开发语言·后端·rust
tsyjjOvO2 小时前
SpringBoot 整合 MyBatis
java·spring boot·mybatis
中国胖子风清扬3 小时前
实战:基于 Camunda 8 的复杂审批流程实战指南
java·spring boot·后端·spring·spring cloud·ai·maven
烧饼Fighting3 小时前
java+vue推rtsp流实现视频播放(由javacv+ffmpg转为vlcj)
java·开发语言·音视频