Deshacerse del c贸digo repetitivo en Protocol Buffers 2

Si est谩 desarrollando aplicaciones empresariales y no solo, probablemente ya est茅 familiarizado con el protocolo de serializaci贸n Protocol Buffers de Google. En este art铆culo, hablemos de su segunda versi贸n. Y que nos obliga a escribir un mont贸n de c贸digo repetitivo, con el que lucharemos.



Protobuff es una gran cosa: usted describe la composici贸n de su API en un archivo .proto, que consta de primitivas, y puede generar c贸digo fuente para diferentes plataformas, por ejemplo, un servidor en Java y un cliente en C #, o viceversa. Como la mayor铆a de las veces se trata de una API para sistemas externos, es m谩s l贸gico hacerla inmutable, y este c贸digo genera un generador est谩ndar para Java.



Consideremos un ejemplo:



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


Como resultado, obtenemos una clase con la siguiente interfaz:



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


Tenga en cuenta que las primitivas se usan en todas partes (lo cual es eficiente para la serializaci贸n y el rendimiento). Pero el campo de edad es opcional, pero el primitivo siempre tiene un valor predeterminado. Esto es lo que sorprende a un mont贸n de c贸digo repetitivo con el que lucharemos.



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


Pero realmente quiero escribir:



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


Protocol Buffers tiene un mecanismo de extensibilidad de complemento y se puede escribir en Java, que es lo que haremos.



驴Qu茅 es un plugin protobuf?



Este es un archivo ejecutable que lee un objeto PluginProtos.CodeGeneratorRequest del flujo de entrada est谩ndar, genera un PluginProtos.CodeGeneratorResponse del flujo de entrada est谩ndar y lo escribe en el flujo de salida est谩ndar.



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


Echemos un vistazo m谩s de cerca a lo que podemos generar.



PluginProtos.CodeGeneratorResponse contiene la colecci贸n PluginProtos.CodeGeneratorResponse.File.

Cada "archivo" es una nueva clase que generamos nosotros mismos. Consiste en:



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


Lo m谩s importante para escribir complementos, no tenemos que regenerar todas las clases, podemos complementar las clases existentes usando insertionPoint . Si volvemos a la interfaz generada arriba, veremos all铆:



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


Es en estos lugares donde podemos agregar nuestro c贸digo. Por lo tanto, no podremos agregar a una secci贸n arbitraria de la clase. Vamos a construir sobre esto. 驴C贸mo podemos resolver este problema? Podemos hacer nuestra nueva interfaz con un m茅todo predeterminado:
public interface PersonOptional extends PersonOrBuilder {
  default Optional<Integer> age() {
    return hasAge() ? Optional.of(getAge()) : Optional.empty();
  }
}


y para la clase Person, agregue la implementaci贸n no solo de PersonOrBuilder, sino tambi茅n de PersonOptional



C贸digo para generar la interfaz que necesitamos
@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();
    }
}




Ahora regresemos del complemento el c贸digo que debe generarse



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


驴C贸mo vamos a usar nuestro nuevo complemento? - a trav茅s de Maven, agregue y configure nuestro complemento:



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


Pero tambi茅n puede ejecutarlo desde la consola: hay una caracter铆stica para ejecutar no solo nuestro complemento, sino que antes debe llamar al compilador est谩ndar de Java (pero debe crear un archivo ejecutable: protocol-gen-java8 (en mi caso, solo un script bash).



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


El c贸digo fuente se puede ver aqu铆 .



All Articles