Boilerplate-Code in Protokollpuffern 2 entfernen

Wenn Sie Unternehmensanwendungen entwickeln und nicht nur, kennen Sie wahrscheinlich bereits das Serialisierungsprotokoll für Protokollpuffer von Google. Lassen Sie uns in diesem Artikel über die zweite Version sprechen. Und dass er uns zwingt, viel Code zu schreiben, mit dem wir kämpfen werden.



Protobuff ist eine großartige Sache - Sie beschreiben die Zusammensetzung Ihrer API in einer .proto-Datei, die aus Grundelementen besteht, und Sie können Quellcode für verschiedene Plattformen generieren - zum Beispiel einen Server in Java und einen Client in C # oder umgekehrt. Da dies meistens eine API für externe Systeme ist, ist es logischer, sie unveränderlich zu machen, und dieser Code selbst generiert einen Standardgenerator für Java.



Betrachten wir ein Beispiel:



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


Als Ergebnis erhalten wir eine Klasse mit der folgenden Schnittstelle:



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


Beachten Sie, dass durchgehend Grundelemente verwendet werden (was für die Serialisierung und Leistung effizient ist). Das Feld Alter ist optional, aber das Grundelement hat immer einen Standardwert. Dies ist es, was eine Menge Boilerplate-Code verblüfft, mit dem wir kämpfen werden.



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


Aber ich möchte wirklich schreiben:



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


Protokollpuffer verfügen über einen Plugin-Erweiterungsmechanismus und können in Java geschrieben werden.



Was ist ein Protobuf-Plugin?



Dies ist eine ausführbare Datei, die ein PluginProtos.CodeGeneratorRequest-Objekt aus dem Standardeingabestream liest, eine PluginProtos.CodeGeneratorResponse aus dem Standardeingabestream generiert und in den Standardausgabestream schreibt.



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


Schauen wir uns genauer an, was wir generieren können.



PluginProtos.CodeGeneratorResponse enthält die Auflistung PluginProtos.CodeGeneratorResponse.File.

Jede "Datei" ist eine neue Klasse, die wir selbst generieren. Es besteht aus:



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


Das Wichtigste beim Schreiben von Plugins - wir müssen nicht alle Klassen neu generieren - wir können die vorhandenen Klassen mit insertionPoint ergänzen . Wenn wir zur oben generierten Schnittstelle zurückkehren, sehen wir dort:



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


An diesen Stellen können wir unseren Code hinzufügen. Daher können wir keinen beliebigen Abschnitt der Klasse hinzufügen. Darauf werden wir aufbauen. Wie können wir dieses Problem lösen? Wir können unsere neue Schnittstelle mit einer Standardmethode erstellen -
public interface PersonOptional extends PersonOrBuilder {
  default Optional<Integer> age() {
    return hasAge() ? Optional.of(getAge()) : Optional.empty();
  }
}


Fügen Sie für die Person-Klasse nicht nur PersonOrBuilder, sondern auch PersonOptional hinzu



Code zum Generieren der benötigten Schnittstelle
@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();
    }
}




Lassen Sie uns nun vom Plugin den Code zurückgeben, der generiert werden muss



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


Wie werden wir unser neues Plugin verwenden? - Füge über maven unser Plugin hinzu und konfiguriere es:



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


Sie können es aber auch von der Konsole aus ausführen - es gibt eine Funktion, mit der Sie nicht nur unser Plugin ausführen können, sondern zuvor den Standard-Java-Compiler aufrufen müssen (Sie müssen jedoch eine ausführbare Datei erstellen - protoc-gen-java8 (in meinem Fall nur ein Bash-Skript).



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


Der Quellcode kann hier eingesehen werden .



All Articles