摆脱协议缓冲区2中的样板代码

如果您不仅在开发企业应用程序,还可能已经熟悉Google的协议缓冲区序列化协议。在本文中,我们来讨论它的第二个版本。并且他迫使我们编写许多样板代码,我们将与之抗争。



Protobuff很棒,您可以在一个由原语组成的.proto文件中描述API的组成,并且可以为不同平台生成源代码-例如,Java中的服务器和C#中的客户端,反之亦然。由于大多数情况下这是用于外部系统的API,因此使其变得不可变是更合乎逻辑的,并且此代码本身生成了Java的标准生成器。



让我们考虑一个例子:



syntax = "proto2";

option java_multiple_files = true;
package org.example.api;

message Person { //     
  required int32 id = 1; // ,  
  required string name = 2; // ,  
  optional int32 age = 3; // ,  
}


结果,我们得到一个具有以下接口的类:



public interface PersonOrBuilder extends
    // @@protoc_insertion_point(interface_extends:org.example.api.Person)
    com.google.protobuf.MessageOrBuilder {


  boolean hasId();
  int getId();

  boolean hasName();
  java.lang.String getName();
  com.google.protobuf.ByteString getNameBytes();

  boolean hasAge();
  int getAge();
}


请注意,原语在整个过程中都使用(对于序列化和性能有效)。但是age字段是可选的,但原语始终具有默认值。这就是使我们将要使用的一堆样板代码的惊人之处。



Integer johnAge = john.hasAge() ? john.getAge() : null;


但是我真的很想写:



Integer johnAge = john.age().orElse(null); //  age() -  Optional<Integer>


协议缓冲区具有插件可扩展性机制,可以用Java编写,这就是我们要做的。



什么是protobuf插件?



这是一个可执行文件,可从标准输入流中读取PluginProtos.CodeGeneratorRequest对象,从标准输入流中生成PluginProtos.CodeGeneratorResponse,并将其写入标准输出流。



public static void main(String[] args) throws IOException {
        PluginProtos.CodeGeneratorRequest codeRequest = PluginProtos.CodeGeneratorRequest.parseFrom(System.in);
        PluginProtos.CodeGeneratorResponse codeResponse;
        try {
            codeResponse = generate(codeRequest);
        } catch (Exception e) {
            codeResponse = PluginProtos.CodeGeneratorResponse.newBuilder()
                    .setError(e.getMessage())
                    .build();
        }
        codeResponse.writeTo(System.out);
    }


让我们仔细看看我们可以产生什么?



PluginProtos.CodeGeneratorResponse包含PluginProtos.CodeGeneratorResponse.File集合。

每个“文件”都是我们自己生成的新类。它包括:



String name; //  ,          package
String content; //    
String insertionPoint; //  


编写插件最重要的事情-我们不必重新生成所有类-我们可以使用insertingPoint补充现有的类如果返回到上面生成的接口,我们将在此处看到:



 // @@protoc_insertion_point(interface_extends:org.example.api.Person)


在这些地方,我们可以添加代码。因此,我们将无法添加到该类的任意部分。我们将以此为基础。我们如何解决这个问题?我们可以使用默认方法创建新界面-
public interface PersonOptional extends PersonOrBuilder {
  default Optional<Integer> age() {
    return hasAge() ? Optional.of(getAge()) : Optional.empty();
  }
}


对于Person类,不仅要添加PersonOrBuilder的实现,还要添加PersonOptional的实现。



生成我们需要的接口的代码
@Builder
public class InterfaceWriter {

    private static final Map<DescriptorProtos.FieldDescriptorProto.Type, Class<?>> typeToClassMap = ImmutableMap.<DescriptorProtos.FieldDescriptorProto.Type, Class<?>>builder()
            .put(TYPE_DOUBLE, Double.class)
            .put(TYPE_FLOAT, Float.class)
            .put(TYPE_INT64, Long.class)
            .put(TYPE_UINT64, Long.class)
            .put(TYPE_INT32, Integer.class)
            .put(TYPE_FIXED64, Long.class)
            .put(TYPE_FIXED32, Integer.class)
            .put(TYPE_BOOL, Boolean.class)
            .put(TYPE_STRING, String.class)
            .put(TYPE_UINT32, Integer.class)
            .put(TYPE_SFIXED32, Integer.class)
            .put(TYPE_SINT32, Integer.class)
            .put(TYPE_SFIXED64, Long.class)
            .put(TYPE_SINT64, Long.class)
            .build();

    private final String packageName;
    private final String className;
    private final List<DescriptorProtos.FieldDescriptorProto> fields;

    public String getCode() {
        List<MethodSpec> methods = fields.stream().map(field -> {
            ClassName fieldClass;
            if (typeToClassMap.containsKey(field.getType())) {
                fieldClass = ClassName.get(typeToClassMap.get(field.getType()));
            } else {
                int lastIndexOf = StringUtils.lastIndexOf(field.getTypeName(), '.');
                fieldClass = ClassName.get(field.getTypeName().substring(1, lastIndexOf), field.getTypeName().substring(lastIndexOf + 1));
            }

            return MethodSpec.methodBuilder(field.getName())
                    .addModifiers(Modifier.DEFAULT, Modifier.PUBLIC)
                    .returns(ParameterizedTypeName.get(ClassName.get(Optional.class), fieldClass))
                    .addStatement("return has$N() ? $T.of(get$N()) : $T.empty()", capitalize(field.getName()), Optional.class, capitalize(field.getName()), Optional.class)
                    .build();
        }).collect(Collectors.toList());

        TypeSpec generatedInterface = TypeSpec.interfaceBuilder(className + "Optional")
                .addSuperinterface(ClassName.get(packageName, className + "OrBuilder"))
                .addModifiers(Modifier.PUBLIC)
                .addMethods(methods)
                .build();

        return JavaFile.builder(packageName, generatedInterface).build().toString();
    }
}




现在让我们从插件返回需要生成的代码



 PluginProtos.CodeGeneratorResponse.File.newBuilder() //     InsertionPoint,       
                    .setName(String.format("%s/%sOptional.java", clazzPackage.replaceAll("\\.", "/"), clazzName))
.setContent(InterfaceWriter.builder().packageName(clazzPackage).className(clazzName).fields(optionalFields).build().getCode())
                    .build();

PluginProtos.CodeGeneratorResponse.File.newBuilder()
                            .setName(String.format("%s/%s.java", clazzPackage.replaceAll("\\.", "/"), clazzName))
                            .setInsertionPoint(String.format("message_implements:%s.%s", clazzPackage, clazzName)) //     -  message -     
                            .setContent(String.format(" %s.%sOptional, ", clazzPackage, clazzName))
                            .build(),


我们将如何使用我们的新插件?-通过Maven,添加并配置我们的插件:



<plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <extensions>true</extensions>
                <configuration>
                    <pluginId>java8</pluginId>
                    <protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot>
                    <protocPlugins>
                        <protocPlugin>
                            <id>java8</id>
                            <groupId>org.example.protobuf</groupId>
                            <artifactId>optional-plugin</artifactId>
                            <version>1.0-SNAPSHOT</version>
                            <mainClass>org.example.proto2plugin.OptionalPlugin</mainClass>
                        </protocPlugin>
                    </protocPlugins>
                </configuration>
            </plugin>


但是您也可以从控制台运行它-有一个功能不仅可以运行我们的插件,还可以在运行之前调用标准的Java编译器(但是您需要创建一个可执行文件-protoc-gen-java8(在我的情况下,只是一个bash脚本)。



protoc -I=./src/main/resources/ --java_out=./src/main/java/  --java8_out=./src/main/java/ ./src/main/resources/example.proto 


可以在这里查看源代码



All Articles