Избавляемся от boilerplate кода в Protocol Buffers 2

от автора

Если вы разрабатываете корпоративные приложения, и не только, вероятно вы уже знакомы с протоколом сериализации Protoloc Buffers от Google. В данной статье поговорим о его второй версии. И о том, что он заставляет писать много boilerplate кода, с которым мы и будем бороться.

Protobuff — это отличная штука — вы описываете состав вашего API в .proto файле, состоящий из примитивов, и можете сгенерировать исходный код для разных платформ — например сервер на 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 у нас необязательное, но примитив всегда имеет дефолтное значение. Именно это и пораждает кучу boilerplate кода, с которым мы и будем бороться.

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

А ведь очень хочется написать:

Integer johnAge = john.age().orElse(null); // здесь age() - Возвращает Optional<Integer> 

Protocol Buffers имеет механизм расширения с помощью plugins, и его можно написать на 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.
Каждый «file» — это новый класс, который мы генерируем самостоятельно. Он состоит из:

String name; // имя файла, здесь должен быть путь к файлу с учетом его package String content; // Собственно исходный код класса String insertionPoint; // Точка вставки 

Самое важное для написания плагинов — мы не должны генерировать все классы заново — мы можем дополнять уже существующие классы используя insertionPoint. Если вернуться к сгенерированному интерфейсу выше — мы там увидим:

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

именно в эти места мы можем дописать свой код. Таким образом дописать в произвольный участок класса у нас не получится. От этого и будем отталкиваться. Как мы можем решить данную проблему? Мы можем сделать свой новый интерфейс с default методом —

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  

Исходный код можно посмотреть здесь.

ссылка на оригинал статьи https://habr.com/ru/post/510310/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *