Реализация gRPC с помощью Java и Spring Boot

от автора

Хорошо бы понимать различия между 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 используется по умолчанию, поэтому поддерживается мультиплексирование.

gPRC клиент-сервисная коммуникация

gPRC клиент-сервисная коммуникация
Настройка проекта (состоит из интерфейсного, серверного и клиентского проектов)

Настройка проекта (состоит из интерфейсного, серверного и клиентского проектов)

Сравнение производительности представлено в этом руководстве. На видео показываю в действии:

Типы данных

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/


Комментарии

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

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