Хорошо бы понимать различия между HTTP/1.1 и HTTP/2, поскольку gRPC использует HTTP/2 по умолчанию.
HTTP/1.1 vs HTTP/2
Характеристики HTTP/1.1:
-
Текстовый формат
-
Заголовки в текстовом формате
-
TCP-соединение требует «трехстороннего рукопожатия» (three-way handshake) — один запрос и ответ с одним единственным TCP-соединением.
Характеристики HTTP/2:
-
Бинарный формат
-
Сжатие заголовков
-
Управление потоком
-
Мультиплексирование (одно и то же TCP-соединение может быть повторно использовано для мультиплексирования. Потоковая передача с сервера — потоковая передача от клиента — возможна двунаправленная потоковая передача)

Посмотреть, как ведет себя загрузка каждой части для HTTP1 и HTTP2 (мультиплексирование) можно по этой ссылке.
В этом разделе постараемся разобраться в причинах, которые могут повлиять на решение о переходе с REST и JSON к gRPC с использованием Protocol Buffers.
JSON vs gRPC (использование Protocol Buffers)
JSON
-
Нет поддержки определения схемы документа.
-
Текстовый формат (поэтому сериализация/десериализация выполняется медленно и требует больших затрат ресурсов).
-
Используется в REST.
gPRC
-
Строгое определение схемы и безопасность типов (IDL: язык определения интерфейса для API).
-
Бинарный, что делает сериализацию/десериализацию быстрее.
-
Автоматическая генерация кода, оптимизированная для межсервисного взаимодействия.
-
HTTP/2 используется по умолчанию, поэтому поддерживается мультиплексирование.
Сравнение производительности представлено в этом руководстве. На видео показываю в действии:
Типы данных
int32 (для int) — значение по умолчанию: 0
int64 (для long) — значение по умолчанию: 0
float — значение по умолчанию: 0
double — значение по умолчанию: 0
bool — значение по умолчанию: false
string — значение по умолчанию: пустая строка
байт (для byte[])
repeated (для List/Collection)
map (для Map) — значение по умолчанию: empty map
enum — значение по умолчанию: первое значение в списке значений.
Есть также классы-обёртки (как Integer в Java), которые можно использовать, предварительно импортировав их в proto-файл.
import “google/protobuf/wrappers.proto”;
Использование:
google.protobuf.UInt64Value id_number = 1;
Если нужно добавить поле метки времени, можно добавить импорт, как описано здесь:
import "google/protobuf/timestamp.proto";
Использование:
google.protobuf.Timestamp timestamp = 2;
Настройка проекта
Рекомендуется создать отдельный модуль для proto-модели и определений сервисов для общего использования (как зависимость).
В соглашении об именовании proto-файлов рекомендуется использовать «lower_snake_case.proto«. Руководство по стилю можно посмотреть здесь.
Имена переменных также должны быть написаны в нижнем регистре с использованием знака подчеркивания в качестве разделителя между словами.
Я создал proto-файл с именем «city_score.proto»:
syntax = "proto3"; import "google/protobuf/timestamp.proto"; package cityscore; option java_package = "com.nils.gprc.cityscore"; option java_multiple_files = true; message CityScoreRequest { int32 city_code = 1; } message CityScoreResponse { int32 city_score = 1; } enum CityScoreErrorCode { INVALID_CITY_CODE_VALUE = 0; CITY_CODE_CANNOT_BE_NULL = 1; } message CityScoreExceptionResponse { google.protobuf.Timestamp timestamp = 1; CityScoreErrorCode error_code = 2; } service CityScoreService { // unary rpc calculateCityScore(CityScoreRequest) returns (CityScoreResponse) {}; }
При выполнении компиляции в Maven я получил ошибку:

Поэтому добавил эти свойства:
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>16</maven.compiler.source> <maven.compiler.target>16</maven.compiler.target>
Затем получил ошибку компиляции:

Для этого я обновил зависимости Maven до последней версии, как указано здесь:
<dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.41.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.41.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.41.0</version> </dependency> <dependency> <!-- necessary for Java 9+ --> <groupId>org.apache.tomcat</groupId> <artifactId>annotations-api</artifactId> <version>6.0.53</version> <scope>provided</scope> </dependency>
Все шло хорошо, пока я не получил в проекте сервиса ошибку несоответствия версий proto-зависимостей (которые появились после того, как я добавил общий proto-модуль в качестве зависимости) и «grpc-server-spring-boot-starter», поэтому мне пришлось понизить версии proto-зависимостей до 1.37. Именно эту версию использует последняя версия grpc-server-spring-boot-starter — 2.12.0.RELEASE:
<dependencies> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.37.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.37.0</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.37.0</version> </dependency> <dependency> <!-- necessary for Java 9+ --> <groupId>org.apache.tomcat</groupId> <artifactId>annotations-api</artifactId> <version>6.0.53</version> <scope>provided</scope> </dependency> </dependencies>
После успешной компиляции в Maven я получил сгенерированные исходники для proto-файла:

Структура моего проекта
-
City Score Service
-
Score Segment Service
-
Score Calculator Service (сервис-агрегатор, которая вызывает как City Score, так и Score Segment).
Как вы увидите из примеров кода, я буду использовать одиночные вызовы в реализации.
Разработка проекта сервиса
Вам необходима «серверная» версия grpc spring-boot-starter в файле pom.xml:
<dependency> <groupId>net.devh</groupId> <artifactId>grpc-server-spring-boot-starter</artifactId> <version>2.12.0.RELEASE</version> </dependency> <dependency> <groupId>com.nils.gprc</groupId> <artifactId>proto-common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Это моя реализация City Score Service:
package com.nils.microservices.cityscore.service; import com.nils.gprc.cityscore.CityScoreRequest; import com.nils.gprc.cityscore.CityScoreResponse; import com.nils.gprc.cityscore.CityScoreServiceGrpc; import io.grpc.stub.StreamObserver; import net.devh.boot.grpc.server.service.GrpcService; import org.springframework.beans.factory.annotation.Autowired; @GrpcService public class CityScoreService extends CityScoreServiceGrpc.CityScoreServiceImplBase { @Autowired private ValidationService validationService; @Override public void calculateCityScore(CityScoreRequest request, StreamObserver<CityScoreResponse> responseObserver) { // System.out.println("Request received from client:\n" + request); validationService.validateCityCode(request.getCityCode()); Integer cityScore = request.getCityCode() * 10; CityScoreResponse response = CityScoreResponse.newBuilder() .setCityScore(cityScore) .build(); responseObserver.onNext(response); responseObserver.onCompleted(); } }
Валидация запроса
Примеры и различные методы обработки ошибок в gPRC можно посмотреть здесь.
Если вы хотите, чтобы вместо выбрасывания исключения возвращался ответ об ошибке, можно использовать «oneof» и отправлять ответ success для успешных запросов и ответ error для исключений:
oneof response { SuccessResponse success_response = 1; ErrorResponse error_response = 2; } }
У меня будет выбрасываться исключение, поэтому я реализовал «GrpcAdvice» и «GrpcExceptionHandler», чтобы исключение было подробным с соответствующим кодом состояния gPRC. Узнать больше можно на официальном сайте документации Spring.
Есть два способа, с помощью которых можно передавать в исключении подробную информацию. Они описаны здесь.
-
Метаданные
-
Any.pack(yourCustomExceptionResponseObject)
CityScoreException — это кастомное исключение RuntimeException, которое я создал для ошибок валидации запроса. Чтобы проверить мое пользовательское сообщение «CityScoreExceptionResponse», вернитесь к моему proto-файлу. Это конечный класс «Grpc Exception Handler»:
package com.nils.microservices.cityscore.exception; import com.google.protobuf.Any; import com.google.protobuf.Timestamp; import com.google.rpc.Code; import com.google.rpc.Status; import com.nils.gprc.cityscore.CityScoreExceptionResponse; import io.grpc.StatusRuntimeException; import io.grpc.protobuf.StatusProto; import net.devh.boot.grpc.server.advice.GrpcAdvice; import net.devh.boot.grpc.server.advice.GrpcExceptionHandler; import java.time.Instant; @GrpcAdvice public class CityScoreExceptionHandler { @GrpcExceptionHandler(CityScoreException.class) public StatusRuntimeException handleValidationError(CityScoreException cause) { Instant time = Instant.now(); Timestamp timestamp = Timestamp.newBuilder().setSeconds(time.getEpochSecond()) .setNanos(time.getNano()).build(); CityScoreExceptionResponse exceptionResponse = CityScoreExceptionResponse.newBuilder() .setErrorCode(cause.getErrorCode()) .setTimestamp(timestamp) .build(); Status status = Status.newBuilder() .setCode(Code.INVALID_ARGUMENT.getNumber()) .setMessage("Invalid city code") .addDetails(Any.pack(exceptionResponse)) .build(); return StatusProto.toStatusRuntimeException(status); } }
Порты для сервиса
Порт по умолчанию для сервера gRPC — 9090. Другое значение можно установить с помощью свойства «grpc.server.port»:
grpc.server.port=8000
Разработка клиентской части
Вам нужна «клиентская» версия grpc spring-boot-starter в файле pom.xml:
<dependency> <groupId>net.devh</groupId> <artifactId>grpc-client-spring-boot-starter</artifactId> <version>2.12.0.RELEASE</version> </dependency> <dependency> <groupId>com.nils.gprc</groupId> <artifactId>proto-common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Это будет сервис-агрегатор, который будет собирать ответы от вызовов проекта сервиса, объединять их и возвращать конечному пользователю через RestController (поэтому он также будет использовать «spring-boot-starter-web»).
Вот реализация:
package com.nils.microservices.scorecalculator.service; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.UInt64Value; import com.google.rpc.Status; import com.nils.gprc.cityscore.CityScoreExceptionResponse; import com.nils.gprc.cityscore.CityScoreRequest; import com.nils.gprc.cityscore.CityScoreResponse; import com.nils.gprc.cityscore.CityScoreServiceGrpc; import com.nils.gprc.scoresegment.ScoreSegmentExceptionResponse; import com.nils.gprc.scoresegment.ScoreSegmentRequest; import com.nils.gprc.scoresegment.ScoreSegmentResponse; import com.nils.gprc.scoresegment.ScoreSegmentServiceGrpc; import com.nils.microservices.scorecalculator.domain.IncomeBracketMultiplierInfo; import com.nils.microservices.scorecalculator.exception.ScoreCalculatorException; import com.nils.microservices.scorecalculator.model.ScoreCalculatorErrorCode; import com.nils.microservices.scorecalculator.model.ScoreCalculatorRequest; import io.grpc.StatusRuntimeException; import io.grpc.protobuf.StatusProto; import net.devh.boot.grpc.client.inject.GrpcClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigInteger; import java.util.Optional; @Service public class ScoreCalculatorService { @GrpcClient("city-score") private CityScoreServiceGrpc.CityScoreServiceBlockingStub cityScoreStub; @GrpcClient("score-segment") private ScoreSegmentServiceGrpc.ScoreSegmentServiceBlockingStub scoreSegmentStub; @Autowired private IncomeBracketMultiplierInfoService incomeBracketMultiplierInfoService; public BigInteger calculateScore(ScoreCalculatorRequest scoreCalculatorRequest) { IncomeBracketMultiplierInfo selectedIncomeBracketMultiplerInfo = getIncomeBracketMultiplerInfo(scoreCalculatorRequest.getIncomeBracketMultiplierId()); BigInteger scoreSegment = getScoreSegment(scoreCalculatorRequest.getIdNumber()); Integer cityScore = getCityScore(scoreCalculatorRequest.getCityCode()); BigInteger score = BigInteger.valueOf(selectedIncomeBracketMultiplerInfo.getMultiplier().intValue()) .multiply(scoreSegment) .add(BigInteger.valueOf(cityScore.intValue())); return score; } private BigInteger getScoreSegment(BigInteger idNumber) { ScoreSegmentRequest scoreSegmentRequest = ScoreSegmentRequest.newBuilder() .setIdNumber(UInt64Value.newBuilder().setValue(idNumber.longValue()).build()) .build(); try { ScoreSegmentResponse scoreSegmentResponse = scoreSegmentStub.calculateScoreSegment(scoreSegmentRequest); return new BigInteger(scoreSegmentResponse.getScoreSegment().toString()); } catch (Exception e){ Status status = StatusProto.fromThrowable(e); for (Any any : status.getDetailsList()) { if (!any.is(ScoreSegmentExceptionResponse.class)) { continue; } try { ScoreSegmentExceptionResponse exceptionResponse = any.unpack(ScoreSegmentExceptionResponse.class); System.out.println("timestamp: " + exceptionResponse.getTimestamp() + ", errorCode : " + exceptionResponse.getErrorCode()); } catch (InvalidProtocolBufferException ex) { ex.printStackTrace(); } } // System.out.println(status.getCode() + " : " + status.getDescription()); } // return a default value return BigInteger.ONE; } private Integer getCityScore(Integer cityCode) { CityScoreRequest cityScoreRequest = CityScoreRequest.newBuilder() .setCityCode(cityCode) .build(); try { CityScoreResponse cityScoreResponse = cityScoreStub.calculateCityScore(cityScoreRequest); return cityScoreResponse.getCityScore(); } catch (StatusRuntimeException e){ Status status = StatusProto.fromThrowable(e); for (Any any : status.getDetailsList()) { if (!any.is(CityScoreExceptionResponse.class)) { continue; } try { CityScoreExceptionResponse exceptionResponse = any.unpack(CityScoreExceptionResponse.class); System.out.println("timestamp: " + exceptionResponse.getTimestamp() + ", errorCode : " + exceptionResponse.getErrorCode()); } catch (InvalidProtocolBufferException ex) { ex.printStackTrace(); } } // System.out.println(status.getCode() + " : " + status.getDescription()); } // return a default value return Integer.valueOf(1); } private IncomeBracketMultiplierInfo getIncomeBracketMultiplerInfo(Long incomeBracketMultiplerInfoId) { Optional<IncomeBracketMultiplierInfo> multiplierInfo = incomeBracketMultiplierInfoService.findById(incomeBracketMultiplerInfoId); if (!multiplierInfo.isPresent()) { throw new ScoreCalculatorException(ScoreCalculatorErrorCode.INVALID_INCOME_BRACKET_MULTIPLIER_ID, incomeBracketMultiplerInfoId); } return multiplierInfo.get(); } }
Наконец, не забудьте добавить урлы сервиса grpc в файл application.properties. Имена свойств должны быть такими же, как аннотированные @GrpcClient
grpc.client.city-score.address=static://localhost:8000 grpc.client.city-score.negotiation-type=plaintext grpc.client.score-segment.address=static://localhost:8100 grpc.client.score-segment.negotiation-type=plaintext
Время тестировать!
Для вызова сервисов можно использовать BloomRPC (как вы используете Postman для вызовов REST API).
Для установки на Mac используйте HomeBrew:
brew install --cask bloomrpc
После установки вы найдете его в приложениях.
Другой способ — установить gRPCurl для операций cURL с gPRC. Снова можно установить с помощью HomeBrew:
brew install grpcurl
Я буду использовать BloomRPC для тестирования эндпоинтов. Мы добавляем наши proto-файлы с помощью кнопки ”+”:

Нажимаем на метод, здесь это «calculateCityScore»:

Он автоматически создает образец запроса. Мы обновляем информацию о порте (grpc.server.port) для сервиса и нажимаем кнопку play:

Чтобы проверить кейс с исключением, я установил отрицательное значение для city_code и нажал кнопку play:

Наконец, мы можем вызвать наш сервис-агрегатор — Score Calculator, используя Postman:

Если я отправлю «-35» для cityCode и проверю обработанную часть исключения, то в консоль будут выведены значения исключений, полученных в ответ:

Последняя версия моего проекта лежит здесь.
Перевела: Ксения Мосеенкова
Скоро состоится открытое занятие «Свойства Spring-приложения». На встрече разберем, каким образом можно определять настройки приложения на чистом Spring, а также немного затронем тему конвертации типов.
ссылка на оригинал статьи https://habr.com/ru/articles/730740/
Добавить комментарий