[Java] Избавляемся от boilerplate кода в Protocol Buffers 2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Если вы разрабатываете корпоративные приложения, и не только, вероятно вы уже знакомы с протоколом сериализации 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
Код для генерации нужного нам интерфейса
SPL
@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
Исходный код можно посмотреть здесь.
===========
Источник:
habr.com
===========
Похожие новости:
- [*nix, Java, Программирование, Сетевые технологии] Как скачать файл порциями?
- [JavaScript, Разработка веб-сайтов] Интеграция ЭЦП НУЦ РК в информационные системы на базе веб технологий
- [Разработка веб-сайтов, JavaScript, Node.JS] Управление зависимостями JavaScript
- [Firefox, JavaScript, Python, Реверс-инжиниринг, Системы обмена сообщениями] Магия WebPush в Mozilla Firefox. Взгляд изнутри
- [JavaScript, Программирование, Учебный процесс в IT] Двоичное кодирование вместо JSON (перевод)
- [Java, Visual Studio, Apache, Тестирование веб-сервисов] Создаем Gatling скрипты с помощью VS Code (перевод)
- [Canvas, JavaScript, WebGL, Математика, Работа с 3D-графикой] Canvas и геометрия. Это почти просто
- [Разработка веб-сайтов, JavaScript] Самый sexy framework для веб-приложений
- [JavaScript, Игры и игровые приставки, Разработка веб-сайтов, Социальные сети и сообщества] Программист создал аналог Club Penguin для взрослых, где можно одновременно общаться как в Zoom и играть
- [Java, Конференции] JPoint 2020: новый формат, новые возможности
Теги для поиска: #_java, #_protobuf, #_proto2, #_java8, #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 04:04
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Если вы разрабатываете корпоративные приложения, и не только, вероятно вы уже знакомы с протоколом сериализации 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 Код для генерации нужного нам интерфейсаSPL@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
Исходный код можно посмотреть здесь. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 04:04
Часовой пояс: UTC + 5