[Java] 如何通过 cglib 的 FastClass 调用一个类中的“任意”方法?

背景

使用反射,我们可以调用一个类 <math xmlns="http://www.w3.org/1998/Math/MathML"> C \text{C} </math>C 中的任意方法。但是使用反射调用一个方法时,有一定的开销,是否有其他办法既能调用(几乎)任意的方法,又不至于让调用开销明显增大呢?cglib 中提供了 <math xmlns="http://www.w3.org/1998/Math/MathML"> net.sf.cglib.reflect.FastClass \text{net.sf.cglib.reflect.FastClass} </math>net.sf.cglib.reflect.FastClass,使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> net.sf.cglib.reflect.FastClass \text{net.sf.cglib.reflect.FastClass} </math>net.sf.cglib.reflect.FastClass 我们可以调用指定类 <math xmlns="http://www.w3.org/1998/Math/MathML"> C \text{C} </math>C 中的任意非 private 方法。

要点

为各个非 private 方法分配编号

<math xmlns="http://www.w3.org/1998/Math/MathML"> net.sf.cglib.reflect.FastClass \text{net.sf.cglib.reflect.FastClass} </math>net.sf.cglib.reflect.FastClass 会给指定类 <math xmlns="http://www.w3.org/1998/Math/MathML"> C \text{C} </math>C 中定义的各个非 private 方法分配编号。 <math xmlns="http://www.w3.org/1998/Math/MathML"> net.sf.cglib.reflect.FastClass \text{net.sf.cglib.reflect.FastClass} </math>net.sf.cglib.reflect.FastClass 里有 getIndex(String, Class[]) 方法,它会给各个非 private 方法分配 int 类型的编号。

就本文中的例子而言,addV1(int, int)/addV2(int, int) 这两个方法分配的编号分别是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 ⬇️

使用编号

<math xmlns="http://www.w3.org/1998/Math/MathML"> net.sf.cglib.reflect.FastClass \text{net.sf.cglib.reflect.FastClass} </math>net.sf.cglib.reflect.FastClass 里有 invoke(int var1, Object var2, Object[] var3) 方法,它会用到这个编号

  • var1: 方法编号
  • var2: 会被 cast 成 <math xmlns="http://www.w3.org/1998/Math/MathML"> C \text{C} </math>C 类型
  • var3: 参数组成的数组

就本文的例子而言, <math xmlns="http://www.w3.org/1998/Math/MathML"> net.sf.cglib.reflect.FastClass \text{net.sf.cglib.reflect.FastClass} </math>net.sf.cglib.reflect.FastClass 的子类中 invoke(int var1, Object var2, Object[] var3) 方法逻辑如下 ⬇️

正文

说明:我写代码时,得到了 TRAE 的协助。

下载相关 jar

本文的例子会用到 cglib,为了使代码短小,我会用采用 java --class-path ... 的方式来运行 main 方法。既然不用 maven 管理依赖,我们就自行下载相关 jar 包。可以通过以下两个链接分别下载 3.3.0 版本的 cglib7.1 版本的 asm (cglib 依赖了 asm)

下载后,将这两个 jar 包移动到合适的目录下,马上就会用到它们。

代码

请将以下代码保存为 Main.java

java 复制代码
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.reflect.FastClass;

public class Main {
    static {
        // 将 cglib 生成的类保存到当前目录下
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".");
    }

    public static void main(String[] args) throws InvocationTargetException {
        // 创建 FastClass 实例,用于快速方法调用
        FastClass fastClass = FastClass.create(Adder.class);
        // 创建 Adder 实例
        Adder adder = new Adder();

        // 获取 Adder 类的所有声明方法
        Method[] methods = Adder.class.getDeclaredMethods();
        // 定义方法参数类型(两个 int 类型)
        Class<?>[] parameterTypes = new Class[]{int.class, int.class};
        // 定义测试参数值
        int param1 = 1;
        int param2 = 1;
        
        // 遍历所有方法
        for (Method method : methods) {
            // 跳过 private 方法
            if (Modifier.isPrivate(method.getModifiers())) {
                continue;
            }
            
            // 打印分隔线,提高输出可读性
            System.out.println("=".repeat(20));
            
            // 获取方法名和 FastClass 中的索引
            String methodName = method.getName();
            int methodIndex = fastClass.getIndex(methodName, parameterTypes);
            System.out.printf("Method index for %s: %d%n", methodName, methodIndex);

            // 调用方法并获取结果
            Object result = fastClass.invoke(methodIndex, adder, new Object[]{param1, param2});
            System.out.printf("%d + %d = %s%n", param1, param2, result);
        }
    }
}

class Adder {

    public int addV1(int a, int b) {
        System.out.printf("addV1(int, int) is called with %d, %d%n", a, b);
        return a + b;
    }

    protected int addV2(int a, int b) {
        System.out.printf("addV2(int, int) is called with %d, %s%n", a, b);
        return a + b;
    }

    int addV3(int a, int b) {
        System.out.printf("addV3(int, int) is called with %d, %d%n", a, b);
        return a + b;
    }

    private int addV4(int a, int b) {
        System.out.printf("addV4(int, int) is called with %d, %d%n", a, b);
        return a + b;
    }
}

现在执行 tree . 应该会看到以下内容

text 复制代码
.
├── asm-7.1.jar
├── cglib-3.3.0.jar
└── Main.java

1 directory, 3 files

编译和运行

编译

执行以下命令就可以编译 Main.java

bash 复制代码
javac --class-path cglib-3.3.0.jar Main.java

编译之后,会看到当前目录下多了 Main.classAdder.class 两个文件。MainAdder 的类图如下 ⬇️

运行

执行以下命令就可以运行 Main.java 中的 main 方法

bash 复制代码
java --add-opens=java.base/java.lang=ALL-UNNAMED --class-path cglib-3.3.0.jar:asm-7.1.jar:. Main

运行结果如下

text 复制代码
CGLIB debugging enabled, writing to '.'
====================
Method index for addV1: 0
addV1(int, int) is called with 1, 1
1 + 1 = 2
====================
Method index for addV2: 1
addV2(int, int) is called with 1, 1
1 + 1 = 2
====================
Method index for addV3: 2
addV3(int, int) is called with 1, 1
1 + 1 = 2

可以看到当前目录下生成了 Adder$$FastClassByCGLIB$$3c2f0ee.class 文件。借助 IntelliJ IDEA (Community Edition) 可以查看这个 class 文件反编译后的内容(完整的内容比较长,这里就不展示了)。

结果分析

我给 Adder$$FastClassByCGLIB$$3c2f0ee 类画了简要的类图 ⬇️

getIndex(String, Class[]) 会给各个(不包含 private 级别的) add 方法分配编号。 invoke(int, Object, Object[]) 方法会用到这个编号

方法和编号的对应关系如下表所示 ⬇️

方法 编号 invoke(int var1, Object var2, Object[] var3) 方法中如何处理?
addV1(int, int) <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 return new Integer(((Adder)var2).addV1(((Number)var3[0]).intValue(), ((Number)var3[1]).intValue()));
addV2(int, int) <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 return new Integer(((Adder)var2).addV2(((Number)var3[0]).intValue(), ((Number)var3[1]).intValue()));
addV3(int, int) <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 return new Integer(((Adder)var2).addV3(((Number)var3[0]).intValue(), ((Number)var3[1]).intValue()));
equals(Object) <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 return new Boolean(((Adder)var2).equals(var3[0]));
toString() <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 return ((Adder)var2).toString();
hashCode() <math xmlns="http://www.w3.org/1998/Math/MathML"> 5 5 </math>5 return new Integer(((Adder)var2).hashCode());

我们看看 getIndex(String var1, Class[] var2)/invoke(int var1, Object var2, Object[] var3) 这两个方法的逻辑 ⬇️ (为了节约篇幅,其他方法都用 ... 代替了)

java 复制代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.lang.reflect.InvocationTargetException;
import net.sf.cglib.core.Signature;
import net.sf.cglib.reflect.FastClass;

public class Adder$$FastClassByCGLIB$$3c2f0ee extends FastClass {
    public Adder$$FastClassByCGLIB$$3c2f0ee(Class var1) {
        super(var1);
    }

    ...
    
    public int getIndex(String var1, Class[] var2) {
        switch (var1.hashCode()) {
            case -1776922004:
                if (var1.equals("toString")) {
                    switch (var2.length) {
                        case 0:
                            return 4;
                    }
                }
                break;
            case -1295482945:
                if (var1.equals("equals")) {
                    switch (var2.length) {
                        case 1:
                            if (var2[0].getName().equals("java.lang.Object")) {
                                return 3;
                            }
                    }
                }
                break;
            case 92659452:
                if (var1.equals("addV1")) {
                    switch (var2.length) {
                        case 2:
                            if (var2[0].getName().equals("int") && var2[1].getName().equals("int")) {
                                return 0;
                            }
                    }
                }
                break;
            case 92659453:
                if (var1.equals("addV2")) {
                    switch (var2.length) {
                        case 2:
                            if (var2[0].getName().equals("int") && var2[1].getName().equals("int")) {
                                return 1;
                            }
                    }
                }
                break;
            case 92659454:
                if (var1.equals("addV3")) {
                    switch (var2.length) {
                        case 2:
                            if (var2[0].getName().equals("int") && var2[1].getName().equals("int")) {
                                return 2;
                            }
                    }
                }
                break;
            case 147696667:
                if (var1.equals("hashCode")) {
                    switch (var2.length) {
                        case 0:
                            return 5;
                    }
                }
        }

        return -1;
    }

    ...

    public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
        Adder var10000 = (Adder)var2;
        int var10001 = var1;

        try {
            switch (var10001) {
                case 0:
                    return new Integer(var10000.addV1(((Number)var3[0]).intValue(), ((Number)var3[1]).intValue()));
                case 1:
                    return new Integer(var10000.addV2(((Number)var3[0]).intValue(), ((Number)var3[1]).intValue()));
                case 2:
                    return new Integer(var10000.addV3(((Number)var3[0]).intValue(), ((Number)var3[1]).intValue()));
                case 3:
                    return new Boolean(var10000.equals(var3[0]));
                case 4:
                    return var10000.toString();
                case 5:
                    return new Integer(var10000.hashCode());
            }
        } catch (Throwable var4) {
            throw new InvocationTargetException(var4);
        }

        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }

    ...
}

其他

文中的一些图是借助 PlantUML 的插件而生成的。画这些图所用到的原始代码列举如下

画 "MainAdder 的类图" 用到的原始代码

puml 复制代码
@startuml

title <i>Main</i> 和 <i>Adder</i> 的类图
caption \n\n
' caption 中的内容是为了防止掘金平台生成的水印遮盖图中的文字

class Main {
+ {static} void main(String[])
}

class Adder {
+ int addV1(int,int)
# int addV2(int,int)
~ int addV3(int,int)
- int addV4(int,int)
}

@enduml

画 "Adder <math xmlns="http://www.w3.org/1998/Math/MathML"> F a s t C l a s s B y C G L I B FastClassByCGLIB </math>FastClassByCGLIB3c2f0ee 的类图" 用到的原始代码

puml 复制代码
@startuml
'https://plantuml.com/class-diagram

title <i>Adder$$FastClassByCGLIB$$3c2f0ee</i> 的类图
caption 图中只画了本文关心的内容

abstract "net.sf.cglib.reflect.FastClass" as F

abstract class F {
    + {abstract} int getIndex(String name, Class[] parameterTypes)
    + {abstract} Object invoke(int index, Object obj, Object[] args) throws InvocationTargetException
}

class Adder$$FastClassByCGLIB$$3c2f0ee
F <|-- Adder$$FastClassByCGLIB$$3c2f0ee

class Adder$$FastClassByCGLIB$$3c2f0ee {
    + Adder$$FastClassByCGLIB$$3c2f0ee(Class var1)
    + int getIndex(String var1, Class[] var2)
    + Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException
}

note left of F::"getIndex(String name, Class[] parameterTypes)"
<code>
/**
 * Return the index of the matching method. The index may be used
 * later to invoke the method with less overhead. If more than one
 * method matches (i.e. they differ by return type only), one is
 * chosen arbitrarily.
 * @see #invoke(int, Object, Object[])
 * @param name the method name
 * @param parameterTypes the parameter array
 * @return the index, or <code>-1</code> if none is found.
 */
</code>
end note

note left of F::invoke
<code>
/**
 * Invoke the method with the specified index.
 * @see getIndex(name, Class[])
 * @param index the method index
 * @param obj the object the underlying method is invoked from
 * @param args the arguments used for the method call
 * @throws java.lang.reflect.InvocationTargetException if the underlying method throws an exception
 */
</code>
end note

@enduml
相关推荐
阿维的博客日记2 小时前
为什么会增加TreeMap和TreeSet这两类,有什么核心优势吗?可以解决什么核心痛点?
java·treeset·treemap
dllxhcjla2 小时前
黑马头条1
java
宠友信息2 小时前
一套基于uniapp+springboot完整社区系统是如何实现的?友猫社区源码级功能解析
java·spring boot·后端·微服务·微信·uni-app
humors2212 小时前
各厂商工具包网址
java·数据库·python·华为·sdk·苹果·工具包
无限进步_3 小时前
【C++&string】大数相乘算法详解:从字符串加法到乘法实现
java·开发语言·c++·git·算法·github·visual studio
海兰3 小时前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
历程里程碑3 小时前
二叉树---二叉树的中序遍历
java·大数据·开发语言·elasticsearch·链表·搜索引擎·lua
小信丶3 小时前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_3 小时前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio