[Java] 自己写程序,来解析方法的 descriptor

9 阅读4分钟

背景

[Java] 如何理解 class 文件中方法的 descriptor? 一文中,我们已经了解了 class\text{class} 文件中方法的 descriptor\text{descriptor} 的结构,一些示例值如下

  • ()I
  • (Ljava/lang/Object;)Z
  • (JI)V
  • (Ljava/lang/Object;)Ljava/util/stream/Stream;
  • ([Ljava/lang/Object;)Ljava/util/stream/Stream;
  • ([[[I)V
  • ([[[Ljava/lang/String;)V

我们能否通过解析这个 descriptor\text{descriptor} 来得到原始的类型信息呢?即类似下表这样的效果 ⬇️

descriptor\text{descriptor}入参的类型信息返回值的类型信息
()I(无)int
(Ljava/lang/Object;)Zjava.lang.Objectboolean
(JI)Vlong, intvoid
(Ljava/lang/Object;)Ljava/util/stream/Stream;java.lang.Objectjava.util.stream.Stream
([Ljava/lang/Object;)Ljava/util/stream/Stream;java.lang.Object[]java.util.stream.Stream
([[[[I)Vint[][][]void
([[[Ljava/lang/String;)Vjava.lang.String[][][]void

先明确一个问题,因为方法的 descriptor\text{descriptor} 中不包含泛型中的类型信息(例子:如果返回值的类型是 java.util.List<Integer> ,那么其中的 Integer 不会保存在方法的 descriptor\text{descriptor} 里),所以我们无法通过方法的 descriptor\text{descriptor} 找到泛型中的类型信息(但是可以找到 java.util.List 这个类型自身)。根据方法的 descriptor\text{descriptor} 的构造规则(细节可以参考 [Java] 如何理解 class 文件中方法的 descriptor? 一文),看起来可以从方法的 descriptor\text{descriptor} 中找到方法的完整类型信息(即各个入参的类型信息以及返回值的类型信息)。我们来实战一下。

正文

[Java] 自己写程序,来解析字段的 descriptor 一文中,我写了一个 java\text{java} 程序,它可以解析字段的 descriptor\text{descriptor}。在此基础上,我们根据方法的 descriptor\text{descriptor} 的构造规则(细节可以参考 [Java] 如何理解 class 文件中方法的 descriptor? 一文),可以写出对应的 Parser\text{Parser},写程序的细节就不赘述了,代码应该还算比较直白。具体的代码如下

代码

请将以下代码保存为 MethodDescriptorParser.java\text{MethodDescriptorParser.java}

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class MethodDescriptorParser {
    private static final char LEFT_PARENTHESIS = '(';
    private static final char RIGHT_PARENTHESIS = ')';
    private static final char CLASS_TYPE_POSTFIX = ';';
    private static final char ARRAY_TYPE_PREFIX = '[';
    private static final char CLASS_TYPE_PREFIX = 'L';

    private static final Map<String, String> baseTypes = Map.ofEntries(
            Map.entry("B", "byte"),
            Map.entry("C", "char"),
            Map.entry("D", "double"),
            Map.entry("F", "float"),
            Map.entry("I", "int"),
            Map.entry("J", "long"),
            Map.entry("S", "short"),
            Map.entry("Z", "boolean")
    );

    private int currIndex;

    public ParseResult parse(String descriptor) {
        currIndex = 0;
        if (descriptor.charAt(0) != LEFT_PARENTHESIS) {
            throw new IllegalArgumentException("Bad descriptor: " + descriptor);
        }

        // Drop '('
        currIndex++;

        List<String> parameterTypes = parseParameterDescriptors(descriptor);

        // Drop ')'
        currIndex++;

        String returnType = parseReturnDescriptor(descriptor);
        return new ParseResult(parameterTypes, returnType);
    }

    private List<String> parseParameterDescriptors(String descriptor) {
        List<String> parameterTypes = new ArrayList<>();
        while (descriptor.charAt(currIndex) != RIGHT_PARENTHESIS) {
            parameterTypes.add(parseParameterDescriptor(descriptor));
        }
        return parameterTypes;
    }

    private String parseParameterDescriptor(String descriptor) {
        return parseFieldType(descriptor);
    }

    private String parseFieldType(String descriptor) {
        char start = descriptor.charAt(currIndex);

        if (start == ARRAY_TYPE_PREFIX) {
            currIndex++;
            String componentName = parseFieldType(descriptor);
            return componentName + "[]";
        }

        if (start == CLASS_TYPE_PREFIX) {
            int endIndex = currIndex + 1;
            while (descriptor.charAt(endIndex) != CLASS_TYPE_POSTFIX) {
                endIndex++;
            }

            // Drop 'L' and ';' and convert.
            // Example: "Ljava/lang/Object;" -> "java.lang.Object"
            String className = descriptor.substring(currIndex + 1, endIndex).replace('/', '.');
            currIndex = endIndex + 1;
            return className;
        }

        String baseTypeName = baseTypes.get(descriptor.substring(currIndex, currIndex + 1));
        currIndex++;
        return baseTypeName;
    }

    private String parseReturnDescriptor(String descriptor) {
        if (descriptor.charAt(currIndex) == 'V') {
            return "void";
        }
        return parseFieldType(descriptor);
    }

    public record ParseResult(List<String> parameterTypes, String returnType) {
        public void display() {
            if (parameterTypes.isEmpty()) {
                System.out.println("Parameter types: (EMPTY)");
            } else {
                System.out.println("Parameter types: " + String.join(", ", parameterTypes));
            }
            System.out.println("Return type: " + returnType);
        }
    }

    private void printUsage() {
        System.out.println("Usage: java MethodDescriptorParser [methodDescriptor ...]");
    }

    public static void main(String[] args) {
        MethodDescriptorParser parser = new MethodDescriptorParser();
        if (args.length == 0) {
            parser.printUsage();
            return;
        }

        for (String arg : args) {
            System.out.println("=".repeat(50));
            System.out.println("Parse result for method descriptor: " + arg);

            parser.parse(arg).display();
        }
    }
}

简要的类图如下 ⬇️ image.png

编译和运行

使用如下的命令可以编译 MethodDescriptorParser.java\text{MethodDescriptorParser.java}

javac MethodDescriptorParser.java

编译之后,当前目录会多出一个名为 MethodDescriptorParser.class\text{MethodDescriptorParser.class} 的文件。下方的命令可以运行 MethodDescriptorParser\text{MethodDescriptorParser} 类中的 main\text{main} 方法

java MethodDescriptorParser

该命令的输出如下

Usage: java MethodDescriptorParser [methodDescriptor ...]

说明我们的使用方法不对(至少应该提供一个 descriptor\text{descriptor} 作为参数)。

为了验证各种情况,我使用了很多 descriptor\text{descriptor} ⬇️ (这里的 descriptor\text{descriptor} 都来自 [Java] 如何理解 class 文件中方法的 descriptor? 一文)

java MethodDescriptorParser '()I' '(Ljava/lang/Object;)Z' '(JI)V' \
'(Ljava/lang/Object;)Ljava/util/stream/Stream;' \
'([Ljava/lang/Object;)Ljava/util/stream/Stream;' '([[[I)V' \
'([[[Ljava/lang/String;)V'

这个命令的运行结果如下 ⬇️

==================================================
Parse result for method descriptor: ()I
Parameter types: (EMPTY)
Return type: int
==================================================
Parse result for method descriptor: (Ljava/lang/Object;)Z
Parameter types: java.lang.Object
Return type: boolean
==================================================
Parse result for method descriptor: (JI)V
Parameter types: long, int
Return type: void
==================================================
Parse result for method descriptor: (Ljava/lang/Object;)Ljava/util/stream/Stream;
Parameter types: java.lang.Object
Return type: java.util.stream.Stream
==================================================
Parse result for method descriptor: ([Ljava/lang/Object;)Ljava/util/stream/Stream;
Parameter types: java.lang.Object[]
Return type: java.util.stream.Stream
==================================================
Parse result for method descriptor: ([[[I)V
Parameter types: int[][][]
Return type: void
==================================================
Parse result for method descriptor: ([[[Ljava/lang/String;)V
Parameter types: java.lang.String[][][]
Return type: void

可以整理成如下的表格

descriptor\text{descriptor}入参的类型信息返回值的类型信息
()I(无)int
(Ljava/lang/Object;)Zjava.lang.Objectboolean
(JI)Vlong, intvoid
(Ljava/lang/Object;)Ljava/util/stream/Stream;java.lang.Objectjava.util.stream.Stream
([Ljava/lang/Object;)Ljava/util/stream/Stream;java.lang.Object[]java.util.stream.Stream
([[[[I)Vint[][][]void
([[[Ljava/lang/String;)Vjava.lang.String[][][]void

解析结果符合预期

参考资料

其他

版权

MethodDescriptorParser.java\text{MethodDescriptorParser.java} 的核心逻辑是我自己写的(在 IntelliJ IDEA (Community Edition) 中写的)。写代码时,我参考了 [Java] 自己写程序,来解析字段的 descriptor 一文中的代码。所以整体而言,没有版权问题,读者朋友可以随意使用 MethodDescriptorParser.java\text{MethodDescriptorParser.java}(在遵守法律的前提下),不用告知我。

如何绘制“简要的类图”

借助 traeIntelliJ IDEA (Community Edition) 中的 PlantUML 的插件,我画了“简要的类图”,完整的代码如下

@startuml

title 简要的类图
caption \n\n
' caption 的内容用于避免掘金平台生成的水印遮盖图中的文字

class MethodDescriptorParser {
    - {static} final char LEFT_PARENTHESIS
    - {static} final char RIGHT_PARENTHESIS
    - {static} final char CLASS_TYPE_POSTFIX
    - {static} final char ARRAY_TYPE_PREFIX
    - {static} final char CLASS_TYPE_PREFIX
    - {static} final Map<String, String> baseTypes
    - int currIndex

    + MethodDescriptorParser$ParseResult parse(String descriptor)
    - List<String> parseParameterDescriptors(String descriptor)
    - String parseParameterDescriptor(String descriptor)
    - String parseFieldType(String: descriptor)
    - String parseReturnDescriptor(String: descriptor)
    - void printUsage()
    + {static} void main(String[] args)
}

@enduml