手写 Protobuf ------ Java 代码生成
在上一集手写 protobuf ------ 词法解析与语法解析完成了.proto
文件的词法分析和语法分析后,我们获得了一个抽象语法树(AST)。现在需要将这个 AST 转换为对应的 Java 代码。
代码生成器架构
核心组件
go
type JavaProtoc struct {
JavaOutput string // Java 文件输出目录
PackageName string // 包名
ProtoName string // proto 文件名
Protoc *protoc.Protoc // AST 根节点
}
生成流程
- 生成外部类(Outer Class)
- 生成消息类(Message Class)
- 生成字段声明和访问器
- 生成序列化/反序列化方法
详细实现
外部类生成
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 枚举类具有以下特点:
-
每个枚举值都关联一个整数值
javapublic enum UserType { UNKNOWN(0), ADMIN(1), GUEST(2); // ... }
-
提供值的访问方法
javapublic int getNumber() { return value; }
-
支持通过值查找枚举实例
javapublic static UserType forNumber(int value) { for (UserType e : values()) { if (e.value == value) { return e; } } return null; }
-
遵循 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;
}
}