手写 Protobuf —— Java 代码生成

手写 Protobuf ------ Java 代码生成

在上一集手写 protobuf ------ 词法解析与语法解析完成了.proto 文件的词法分析和语法分析后,我们获得了一个抽象语法树(AST)。现在需要将这个 AST 转换为对应的 Java 代码。

代码生成器架构

核心组件

go 复制代码
type JavaProtoc struct {
    JavaOutput  string         // Java 文件输出目录
    PackageName string         // 包名
    ProtoName   string         // proto 文件名
    Protoc      *protoc.Protoc // AST 根节点
}

生成流程

  1. 生成外部类(Outer Class)
  2. 生成消息类(Message Class)
  3. 生成字段声明和访问器
  4. 生成序列化/反序列化方法

详细实现

外部类生成

go 复制代码
func (jp *JavaProtoc) generateOuterClass(innerStr strings.Builder) string {
    var fileStr strings.Builder
    // 生成包声明
    fileStr.WriteString(fmt.Sprintf("package %s;\n\n", jp.PackageName))
    
    // 生成类声明
    fileStr.WriteString("public final class " + toCamelCase(jp.ProtoName, true) + " {\n")
    fileStr.WriteString(innerStr.String())
    fileStr.WriteString("}\n")
    
    return fileStr.String()
}

消息类生成

go 复制代码
func (jp *JavaProtoc) generateMessageClass(msg *protoc.Message, inner bool) string {
    var builder strings.Builder
    
    // 生成类声明
    builder.WriteString(fmt.Sprintf("public final static class %s {\n", 
        toCamelCase(msg.Name, true)))
    
    // 生成字段
    for _, field := range msg.Fields {
        builder.WriteString(generateFieldDeclaration(field))
    }
    
    // 生成构造函数
    builder.WriteString(generateConstructor(msg))
    
    // 生成 getter/setter
    for _, field := range msg.Fields {
        builder.WriteString(generateGetterAndSetter(field))
    }
    
    // 生成序列化方法
    builder.WriteString(jp.generateToByteArray(msg))
    
    // 生成反序列化方法
    builder.WriteString(jp.generateParseFrom(msg))
    
    builder.WriteString("}\n")
    return builder.String()
}

字段处理

字段声明
go 复制代码
func generateFieldDeclaration(field *protoc.Field) string {
    javaType := toJavaType(field)
    return fmt.Sprintf("    private %s %s;\n", 
        javaType, toCamelCase(field.Name, false))
}
Getter/Setter
go 复制代码
func generateGetterAndSetter(field *protoc.Field) string {
    javaType := toJavaType(field)
    fieldName := toCamelCase(field.Name, true)
    
    return fmt.Sprintf(`
    public %s get%s() {
        return this.%s;
    }
    
    public void set%s(%s %s) {
        this.%s = %s;
    }`,
        javaType, fieldName, 
        toCamelCase(field.Name, false),
        fieldName, javaType, 
        toCamelCase(field.Name, false),
        toCamelCase(field.Name, false),
        toCamelCase(field.Name, false))
}

枚举类生成

让我来补充枚举类生成的实现部分:

枚举类生成

在 Protobuf 中,枚举是一种特殊的类型,它允许你为一个字段指定一组命名常量。在 Java 中,我们需要将其转换为 Java 的 enum 类型。

go 复制代码
func (jp *JavaProtoc) generateEnum(enum *protoc.Enum) string {
    className := toCamelCase(enum.Name, true)
    var builder strings.Builder

    // 生成枚举类声明
    builder.WriteString(fmt.Sprintf("public enum %s {\n", className))

    // 生成枚举值
    for i, value := range enum.Values {
        if i > 0 {
            builder.WriteString(",\n")
        }
        builder.WriteString(fmt.Sprintf("    %s(%d)", 
            strings.ToUpper(value.Name), value.Value))
    }
    builder.WriteString(";\n\n")

    // 生成值字段和构造函数
    builder.WriteString("    private final int value;\n\n")
    builder.WriteString(fmt.Sprintf("    %s(int value) {\n", className))
    builder.WriteString("        this.value = value;\n    }\n\n")

    // 生成获取数值的方法
    builder.WriteString("    public int getNumber() {\n")
    builder.WriteString("        return value;\n")
    builder.WriteString("    }\n")

    // 生成通过数值获取枚举实例的方法
    builder.WriteString(fmt.Sprintf("    public static %s forNumber(int value) {\n", className))
    builder.WriteString(fmt.Sprintf("        for (%s e : values()) {\n", className))
    builder.WriteString("            if (e.value == value) {\n")
    builder.WriteString("                return e;\n")
    builder.WriteString("            }\n")
    builder.WriteString("        }\n")
    builder.WriteString("        return null;\n")
    builder.WriteString("    }\n")

    builder.WriteString("}\n")
    return builder.String()
}

生成的 Java 枚举类具有以下特点:

  1. 每个枚举值都关联一个整数值

    java 复制代码
    public enum UserType {
        UNKNOWN(0),
        ADMIN(1),
        GUEST(2);
        // ...
    }
  2. 提供值的访问方法

    java 复制代码
    public int getNumber() {
        return value;
    }
  3. 支持通过值查找枚举实例

    java 复制代码
    public static UserType forNumber(int value) {
        for (UserType e : values()) {
            if (e.value == value) {
                return e;
            }
        }
        return null;
    }
  4. 遵循 Protobuf 的命名约定

    • 枚举值使用大写
    • 保持原始的数值映射
    • 支持不连续的枚举值

这样的实现确保了:

  • 与 Protobuf 规范的兼容性
  • 类型安全的枚举使用
  • 双向的值转换支持
  • 良好的代码可读性

特殊类型处理

OneOf 字段
go 复制代码
func generateOneOf(oneOf *protoc.OneOf) string {
    var builder strings.Builder
    
    // 生成字段
    builder.WriteString(fmt.Sprintf("    private Object %s;\n", 
        toCamelCase(oneOf.Name, false)))
    builder.WriteString(fmt.Sprintf("    private int %sCase = 0;\n", 
        toCamelCase(oneOf.Name, false)))
    
    // 生成 Case 枚举
    builder.WriteString(generateOneOfEnum(oneOf))
    
    // 生成访问器
    for _, field := range oneOf.Fields {
        builder.WriteString(generateOneOfAccessors(oneOf, field))
    }
    
    return builder.String()
}

让我们继续完善 GeneratedMessage 和 Any 类型的实现。

GeneratedMessage 实现

GeneratedMessage 是所有生成的消息类的基类,它提供了基本的序列化和反序列化功能。

java 复制代码
public abstract class GeneratedMessage {
    // 编码解码方法
}

Any 类型实现

Any中有两个字段

  • typeUrl 是用来保证反序列化时,不会反序列化成错误的类型
  • value 就是序列化后的值

具体实现可以查看下面的代码

java 复制代码
package com.protoc.qiu;

public class Any {
    private String typeUrl;
    private byte[] value;

    public Any() {}

    public String getTypeUrl() {
        return typeUrl;
    }

    public void setTypeUrl(String typeUrl) {
        this.typeUrl = typeUrl;
    }

    public byte[] getValue() {
        return value;
    }

    public void setValue(byte[] value) {
        this.value = value;
    }

    public static Any pack(GeneratedMessage message) {
        Any any = new Any();
        any.typeUrl = message.getClass().getName();
        any.value = message.toByteArray();
        return any;
    }

    public <T extends GeneratedMessage> T unpack(Class<T> clazz) {
        if (!typeUrl.equals(clazz.getName())) {
            throw new RuntimeException("Type mismatch");
        }
        try {
            return clazz.getMethod("parseFrom", byte[].class)
                       .invoke(null, value);
        } catch (Exception e) {
            throw new RuntimeException("Failed to unpack", e);
        }
    }

    public byte[] toByteArray() {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        try {
            if (typeUrl != null) {
                writeString(stream, 1, typeUrl);
            }
            if (value != null) {
                writeBytes(stream, 2, value);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize", e);
        }
    }

    public static Any parseFrom(byte[] data) {
        Any result = new Any();
        ByteArrayInputStream stream = new ByteArrayInputStream(data);
        try {
            while (stream.available() > 0) {
                int tag = readTag(stream);
                switch (getFieldNumberFromTag(tag)) {
                    case 1:
                        result.typeUrl = readString(stream);
                        break;
                    case 2:
                        result.value = readBytes(stream);
                        break;
                    default:
                        skipField(stream, tag);
                        break;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse", e);
        }
        return result;
    }
}
相关推荐
purrrew18 分钟前
【Java ee 初阶】多线程(8)
java·java-ee
TPBoreas3 小时前
Jenkins 改完端口号启动不起来了
java·开发语言
金斗潼关3 小时前
SpringCloud GateWay网关
java·spring cloud·gateway
秋名RG4 小时前
深入解析建造者模式(Builder Pattern)——以Java实现复杂对象构建的艺术
java·开发语言·建造者模式
eternal__day4 小时前
Spring Boot 实现验证码生成与校验:从零开始构建安全登录系统
java·spring boot·后端·安全·java-ee·学习方法
陈大爷(有低保)5 小时前
swagger3融入springboot
java
weixin_376934638 小时前
JDK Version Manager (JVMS)
java·开发语言
月月大王8 小时前
easyexcel导出动态写入标题和数据
java·服务器·前端
大G哥9 小时前
Kotlin Lambda语法错误修复
android·java·开发语言·kotlin
行走__Wz9 小时前
计算机学习路线与编程语言选择(信息差)
java·开发语言·javascript·学习·编程语言选择·计算机学习路线