Menyingkirkan kode boilerplate di Protokol Buffer 2

Jika Anda mengembangkan aplikasi perusahaan dan tidak hanya, Anda mungkin sudah terbiasa dengan protokol serialisasi Protokol Buffer dari Google. Pada artikel ini, mari kita bicara tentang versi keduanya. Dan dia memaksa kita untuk menulis banyak kode boilerplate, yang akan kita lawan.



Protobuff adalah hal yang hebat - Anda menggambarkan komposisi API Anda dalam file .proto, yang terdiri dari primitif, dan Anda dapat menghasilkan kode sumber untuk platform yang berbeda - misalnya, server di Jawa dan klien di C #, atau sebaliknya. Karena paling sering ini adalah API untuk sistem eksternal, lebih logis untuk membuatnya tidak berubah, dan kode ini sendiri menghasilkan generator standar untuk Java.



Mari kita pertimbangkan sebuah contoh:



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; // ,  
}


Hasilnya, kami mendapatkan kelas dengan antarmuka berikut:



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();
}


Perhatikan bahwa primitif digunakan di seluruh (yang efisien untuk serialisasi dan kinerja). Tetapi bidang usia adalah opsional, tetapi primitif selalu memiliki nilai default. Inilah yang mengherankan sekelompok kode boilerplate yang akan kita perjuangkan.



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


Tetapi saya benar-benar ingin menulis:



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


Protokol Buffer memiliki mekanisme ekstensibilitas plugin dan dapat ditulis dalam Java, yang akan kami lakukan.



Apa itu plugin protobuf?



Ini adalah file yang dapat dieksekusi yang membaca objek PluginProtos.CodeGeneratorRequest dari aliran input standar, menghasilkan PluginProtos.CodeGeneratorResponse dari aliran input standar dan menulisnya ke aliran output standar.



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);
    }


Mari kita lihat lebih dekat apa yang bisa kita hasilkan?



PluginProtos.CodeGeneratorResponse berisi koleksi PluginProtos.CodeGeneratorResponse.File.

Setiap "file" adalah kelas baru yang kita hasilkan sendiri. Terdiri dari:



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


Hal yang paling penting untuk menulis plugin - kita tidak harus membuat ulang semua kelas - kita bisa menambah kelas yang ada menggunakan insertionPoint . Jika kita kembali ke antarmuka yang dibuat di atas, kita akan melihat di sana:



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


di tempat-tempat inilah kita dapat menambahkan kode kita. Dengan demikian, kita tidak akan dapat menambahkan bagian kelas yang sewenang-wenang. Kami akan membangun ini. Bagaimana kita bisa menyelesaikan masalah ini? Kita dapat membuat antarmuka baru dengan metode default -
public interface PersonOptional extends PersonOrBuilder {
  default Optional<Integer> age() {
    return hasAge() ? Optional.of(getAge()) : Optional.empty();
  }
}


dan untuk kelas Person, tambahkan implementasi tidak hanya PersonOrBuilder, tetapi juga PersonOptional



Kode untuk menghasilkan antarmuka yang kita butuhkan
@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();
    }
}




Sekarang mari kita kembali dari plugin kode yang perlu dibuat



 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(),


Bagaimana kita akan menggunakan plugin baru kita? - via pakar, tambahkan dan konfigurasikan plugin kami:



<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>


Tetapi Anda juga dapat menjalankannya dari konsol - ada satu fitur untuk menjalankan tidak hanya plugin kami, tetapi sebelum itu Anda perlu memanggil kompiler java standar (tetapi Anda perlu membuat file executable - protoc-gen-java8 (dalam kasus saya, hanya skrip bash).



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


Kode sumber dapat dilihat di sini .



All Articles