В мире микросервисов зачастую возникает нужда в быстром общении между сервисами, как альтернатива Rest API к нам на помощь приходит gRPC. Статья будет посвящена реализации gRPC с помощью Spring Boot и Java 17, и будет полезна тем, кто начинает своё знакомство с gRPC.
Немного теории
Что такое gRPC?
gRPC (Google Remote Procedure Calls) — это современная и высокопроизводительная система вызова удалённой процедуры (RPC) с открытом исходным кодом, разработанная IT-гигантом Google. Данное решение позволяет эффективно передавать данные между сервисами, используя протокол HTTP/2, а для определения процедуры используется Protocol Buffers. Технология поддерживается многими языками, такими как: Java, C++, Python и другие. Более подробно можете узнать в официальной документации gRPC.
Преимущества
-
Высокая производительность – Благодаря использованию HTTP/2 и protobuf, gRPC обеспечивает минимальные задержки и высокую пропускную способность.
-
Строгая типизация – Применение protobuf для описания сервисов и сообщений позволяет строго задавать структуру данных, что уменьшает вероятность ошибок на этапе компиляции.
-
Мультиплатформенность – Поддержка множества языков программирования позволяет объединять компоненты, написанные на разных технологиях, в единую систему, упрощая интеграцию и переиспользование кода.
-
Двунаправленный стриминг – gRPC поддерживает не только запрос-ответ, но и двусторонние потоки, а также полный дуплекс, что делает его отличным выбором для работы с данными в реальном времени, например, в чатах или системах мониторинга.
-
Автоматическая генерация кода – gRPC автоматически создает клиентские и серверные заглушки (stubs), избавляя разработчиков от написания шаблонного кода и снижая вероятность ошибок. Это ускоряет процесс разработки.
Недостатки
-
Высокий порог входа – Для новичков gRPC может показаться сложным из-за необходимости изучения protobuf и особенностей работы с HTTP/2. Однако с опытом освоение технологии становится проще.
-
Ограниченная поддержка в браузерах – Большинство браузеров не поддерживают gRPC напрямую, что требует использования дополнительных решений, таких как gRPC-Web или прокси-серверы, что усложняет разработку веб-приложений.
-
Зависимость от Protocol Buffers – Применение protobuf в качестве основного формата сериализации может быть неудобным для тех, кто привык к JSON или XML. Хотя protobuf более эффективен, он требует дополнительных шагов для преобразования данных.
-
Требования к инфраструктуре – Эффективное использование gRPC возможно только при поддержке HTTP/2 на уровне сетевой инфраструктуры, что может потребовать дополнительных настроек и ресурсов, особенно если существующая система не адаптирована под HTTP/2.
Работа с Protocol Buffers
ProtoBuf — это язык описания интерфейса и система сериализации данных, разработанные Google. Они используются для сериализации структурированных данных. Структура данных в ProtoBuf описывается в файлах с расширением .proto. Эти файлы содержат определения сообщений (аналогично классам в ООП) и сервисов (опционально). Более подробно можно почитать здесь.
Вот, пример структуры сообщения:
message Area { string id = 1; string title = 2; string description = 3; string address = 4; google.protobuf.Timestamp creationDateTime = 5; google.protobuf.Timestamp updateDateTime = 6; Coordinate Coordinate = 7; }
Объявление сервиса:
service AreaService { rpc GetAreas (google.protobuf.Empty) returns (AreaList) {}; rpc GetAreaById (AreaId) returns (Area) {}; rpc CreateArea (AreaToCreate) returns (AreaId) {}; rpc SaveFile (File) returns (google.protobuf.Empty) {}; rpc StreamingFile (stream File) returns (google.protobuf.Empty) {}; }
Основные типы данных ProtoBuf
int32 (для int) — значение по умолчанию: 0
int64 (для long) — значение по умолчанию: 0
float — значение по умолчанию: 0
double — значение по умолчанию: 0
bool — значение по умолчанию: false
string — значение по умолчанию: пустая строка
byte (для byte[])
repeated (для List/Collection)
map (для Map) — значение по умолчанию: empty map
enum — значение по умолчанию: первое значение в списке значений.
Есть также классы-обёртки, например, как «google/protobuf/timestamp.proto» для даты и времени.
Перейдём к реализации
В рамках статьи разработаем один модуль и два микросервиса. Рекомендуется реализовать проекты с gRPC с разными модулями:
-
gRPC-interface: Содержит файлы формата .proto и генерирует Java классы.
-
gRPC-server: Содержит реализацию gRPC «эндпоинтов» и gRPC-interface в качестве зависимости через Maven локальный репозиторий.
-
gRPC-client: Любой клиент на Java, который обращается к нашим gRPC «эндпоинтам».
Разделение проекта на модули при разработке с gRPC помогает сделать код более модульным, переиспользуемым и удобным для сопровождения.
Технологии разработки будут:
-
Spring Boot
-
Spring Cloud
-
gRPC
-
Rest API
-
Swagger
-
OAuth 2.0
-
MapStruct
-
Spring Data Jpa/Hibernate
-
PostgreSQL
Наш проект будет иметь микросервисную архитектуру.
-
Eureka Server
-
API Gateway
-
grpc-interface
-
area-client
-
area-server
В статье будут рассматриваться grpc-interface, area-client, area-server.
grpc-interface
Для начала опеределим area.proto файл и опишим процедуры.
syntax = "proto3"; import "google/protobuf/timestamp.proto"; import "google/protobuf/empty.proto"; package ru.acgnn.grpc.area; option java_multiple_files = true; option java_package = "ru.acgnn.grpc"; option java_outer_classname = "AreaServerGrpcProto"; service AreaService { rpc GetAreas (google.protobuf.Empty) returns (AreaList) {}; rpc GetAreaById (AreaId) returns (Area) {}; rpc CreateArea (AreaToCreate) returns (AreaId) {}; rpc SaveFile (File) returns (google.protobuf.Empty) {}; rpc StreamingFile (stream File) returns (google.protobuf.Empty) {}; } message AreaId { string id = 1; } message AreaList { repeated Area areas = 1; } message Coordinate { double longitude = 1; double latitude = 2; } message Area { string id = 1; string title = 2; string description = 3; string address = 4; google.protobuf.Timestamp creationDateTime = 5; google.protobuf.Timestamp updateDateTime = 6; Coordinate Coordinate = 7; } message AreaToCreate { string title = 1; string description = 2; string address = 3; Coordinate Coordinate = 4; } message File { string content_type = 1; bytes content = 2; }
Блок message отвечает за определение структуры сообщения в rpc эндпоинтах, блок service — за эндпоинты, что они возвращают и принимают на вход.
Напишем pom.xml, чтобы проект правильно собрался и скомпилировался.
Добавляем следующие зависимости:
<dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>${grpc.version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>${grpc.version}</version> </dependency> <dependency> <!-- Java 9+ compatibility - Do NOT update to 2.0.0 --> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>1.3.5</version> <optional>true</optional> </dependency>
Блок build будет выглядить следующим образом:
<build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.7.0</version> </extension> </extensions> <plugins> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>${protobuf-plugin.version}</version> <configuration> <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact> <protoSourceRoot>${basedir}/src/main/proto/</protoSourceRoot> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Обратите внимание на артефакты, они будут нужны, чтобы добавлять интерфейс в другие проекты, как зависимость. В статье будем рассматривать сборщик Maven.
<groupId>ru.acgnn.grpc</groupId> <artifactId>grpc-interface</artifactId> <version>1.0.0</version>
Структура проекта выглядет так:
Запустив сборку проекта, появятся скомпилированные Java классы.
area-server
В этом микросервисе напишем небольшую бизнес-логику для grpc эндпоинтов.
Для начала добавим следующие зависимости:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>ru.acgnn.grpc</groupId> <artifactId>grpc-interface</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-spatial</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </dependency>
Дальше приступим к реализации бизнес-логики. В пакете grpc объявим следующий класс:
import java.util.UUID; import org.springframework.security.access.prepost.PreAuthorize; import com.google.protobuf.Empty; import io.grpc.stub.StreamObserver; import lombok.RequiredArgsConstructor; import net.devh.boot.grpc.server.service.GrpcService; import ru.acgnn.area_server.mapper.AreaMapper; import ru.acgnn.area_server.service.AreaService; import ru.acgnn.area_server.service.FileService; import ru.acgnn.grpc.Area; import ru.acgnn.grpc.AreaId; import ru.acgnn.grpc.AreaList; import ru.acgnn.grpc.AreaServiceGrpc.AreaServiceImplBase; import ru.acgnn.grpc.AreaToCreate; import ru.acgnn.grpc.File; @GrpcService @RequiredArgsConstructor @PreAuthorize("hasRole('admin')") public class AreaGrpc extends AreaServiceImplBase { private final AreaService areaService; private final FileService fileService; private final AreaMapper areaMapper; @Override public void getAreaById(AreaId request, StreamObserver<Area> responseObserver) { responseObserver.onNext(areaMapper.toDto(areaService.getById(UUID.fromString(request.getId())))); responseObserver.onCompleted(); } @Override public void getAreas(Empty request, StreamObserver<AreaList> responseObserver) { responseObserver.onNext(AreaList.newBuilder().addAllAreas(areaMapper.toListDto(areaService.getAll())).build()); responseObserver.onCompleted(); } @Override public void createArea(AreaToCreate request, StreamObserver<AreaId> responseObserver) { responseObserver.onNext( AreaId.newBuilder() .setId(areaService.createArea(areaMapper.toEntity(request)).getId().toString()) .build() ); responseObserver.onCompleted(); } @Override public void saveFile(File file, StreamObserver<Empty> responseObserver) { fileService.saveFile(file); responseObserver.onNext(Empty.newBuilder().build()); responseObserver.onCompleted(); } }
Здесь расширяемся классом AreaServiceImplBase, который был сгенерирован модулем grpc-interface, и из него делаем импорт нужных нам классов. В проекте соблюдается цепочка: repository -> service -> mapper -> controller. То есть на уровне сервиса мы работаем с сущностями и все методы возвращают сущность, а в ответ grpc ручки с помощью маппера конвертируем сущность в нужный dto-класс.
Сервис у нас будет максимально простым.
import java.util.List; import java.util.UUID; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import ru.acgnn.area_server.exception.ApiServiceException; import ru.acgnn.area_server.model.entity.AreaEntity; import ru.acgnn.area_server.repository.AreaRepository; @Slf4j @Service @RequiredArgsConstructor public class AreaService { private final AreaRepository areaRepo; public AreaEntity getById(UUID id) { return areaRepo.findById(id) .orElseThrow(() -> new ApiServiceException("Площадка не найдена", HttpStatus.NOT_FOUND)); } @Transactional public AreaEntity createArea(AreaEntity area) { if (areaRepo.existsByTitleOrCoordinateOrAddress(area.getTitle(), area.getCoordinate(), area.getAddress())) { throw new ApiServiceException("Такая площадка уже существует", HttpStatus.CONFLICT); } return areaRepo.save(area); } public List<AreaEntity> getAll() { return areaRepo.findAll(); } }
Теперь обратим внимание на Mapper:
import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.List; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.PrecisionModel; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingConstants; import org.mapstruct.Named; import org.mapstruct.NullValuePropertyMappingStrategy; import com.google.protobuf.Timestamp; import ru.acgnn.grpc.Area; import ru.acgnn.grpc.AreaToCreate; import ru.acgnn.grpc.Coordinate; import ru.acgnn.area_server.model.entity.AreaEntity; @Mapper( componentModel = MappingConstants.ComponentModel.SPRING, nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE ) public interface AreaMapper { @Mapping(source = "area.creationDateTime", target = "creationDateTime", qualifiedByName = "toTimestamp") @Mapping(source = "area.updateDateTime", target = "updateDateTime", qualifiedByName = "toTimestamp") @Mapping(source = "area.coordinate", target = "coordinate", qualifiedByName = "toCoordinate") Area toDto(AreaEntity area); @Mapping(source = "area.coordinate", target = "coordinate", qualifiedByName = "toPoint") AreaEntity toEntity(AreaToCreate area); List<Area> toListDto(List<AreaEntity> areas); @Named("toTimestamp") default Timestamp toTimestamp(LocalDateTime dateTime) { return Timestamp.newBuilder() .setSeconds(dateTime.atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()) .build(); } @Named("toCoordinate") default Coordinate toCoordinate(Point coordinate) { return Coordinate.newBuilder() .setLatitude(coordinate.getY()) .setLongitude(coordinate.getX()) .build(); } @Named("toLocalDateTime") default LocalDateTime toLocalDateTime(Timestamp timestamp) { return Instant .ofEpochSecond(timestamp.getSeconds()) .atZone(ZoneId.of("Europe/Moscow")) .toLocalDateTime(); } @Named("toPoint") default Point toPoint(Coordinate coordinate) { GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); return geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coordinate.getLongitude(), coordinate.getLatitude())); } }
Здесь благодаря зависимости mapstruct можно реализовывать маппинг объектов на уровне interface-класса, который поднимается как Сomponent на уровне приложения.
import org.springframework.http.HttpStatus; import lombok.Getter; @Getter public class ApiServiceException extends RuntimeException { private final HttpStatus status; public ApiServiceException(String message, HttpStatus status) { super(message); this.status = status; } }
Мой кастомный класс ошибки. Теперь возникает вопрос, как настроить Controller Advice так, чтобы он возвращал ошибки с корректными статусами.
import org.springframework.http.HttpStatus; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import io.grpc.Status; import lombok.extern.slf4j.Slf4j; import net.devh.boot.grpc.server.advice.GrpcAdvice; import net.devh.boot.grpc.server.advice.GrpcExceptionHandler; import ru.acgnn.area_server.exception.ApiServiceException; @Slf4j @GrpcAdvice public class GprcHandler { @GrpcExceptionHandler(ApiServiceException.class) public Status handleInvalidArgument(ApiServiceException e) { log.debug("ApiServiceException: {}", e.getMessage()); return getStatus(e.getStatus()).withDescription(e.getMessage()); } @GrpcExceptionHandler(InvalidBearerTokenException.class) public Status handleInvalidBearerTokenException(InvalidBearerTokenException e) { log.debug("InvalidBearerTokenException: {}", e.getMessage()); return Status.UNAUTHENTICATED.withDescription(e.getMessage()); } @GrpcExceptionHandler(IllegalArgumentException.class) public Status handleIllegalArgumentException(IllegalArgumentException e) { log.debug("IllegalArgumentException: {}", e.getMessage()); return Status.INVALID_ARGUMENT.withDescription(e.getMessage()); } private Status getStatus(HttpStatus status) { return switch (status) { case NOT_FOUND -> Status.NOT_FOUND; case BAD_REQUEST -> Status.INVALID_ARGUMENT; case CONFLICT -> Status.ALREADY_EXISTS; case FORBIDDEN -> Status.PERMISSION_DENIED; case UNAUTHORIZED -> Status.UNAUTHENTICATED; case SERVICE_UNAVAILABLE -> Status.UNAVAILABLE; default -> Status.INTERNAL; }; } }
Пришла идея реализовать маппинг HttpStatus с GrpsStatus с помощь switch-case. Не очень креативно, но эффективно! Этот хендлер работает только для gRPC эндпоинтов, для HTTP ручек он работать не будет, будет лучше создать отдельный handler-класс, аннотируемый @RestControllerAdvice.
import java.time.LocalDateTime; import java.util.Objects; import java.util.UUID; import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.proxy.HibernateProxy; import org.locationtech.jts.geom.Point; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Getter @Setter @Builder @Table( name = "area", uniqueConstraints = { @UniqueConstraint(name = "unique_area", columnNames = { "title", "address", "coordinate" }) } ) @NoArgsConstructor @AllArgsConstructor public class AreaEntity { @Id @Column(name = "id") @Comment("ID записи") @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @CreationTimestamp @Column(name = "creation_date_time", nullable = false, updatable = false) @Comment("Дата и время создания записи") private LocalDateTime creationDateTime; @UpdateTimestamp @Column(name = "update_date_time", nullable = false) @Comment("Дата и время обновления записи") private LocalDateTime updateDateTime; @NotNull @Comment("Название площадки") @Column(name = "title") private String title; @NotNull @Comment("Описание площадки") @Column(name = "description", columnDefinition = "text") private String description; @NotNull @Comment("Адресс площадки") @Column(name = "address") private String address; @NotNull @Column(name = "coordinate") @Comment("Координаты площадки") private Point coordinate; @Override public final boolean equals(Object o) { if (this == o) return true; if (o == null) return false; Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; AreaEntity area = (AreaEntity) o; return getId() != null && Objects.equals(getId(), area.getId()); } @Override public final int hashCode() { return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); } }
Сущность выглядит следующим образом. На данный момент, особого внимания ей уделяться не будет.
В application.properties укажите следующие настройки:
# Service properties spring.application.name=area-server server.port=0 server.servlet.context-path=/ # Discovery client properties eureka.client.service-url.default-zone=http://localhost:8761/eureka # gRPC-server properties grpc.server.port=0
Это будет необходимо, чтобы мы могли обращаться к микросервису по его названию, а не по адресу и порту.
Так выглядит структура микросервиса:
Теперь мы увидели как на grpc-server’е реализуется бизнес-логика приложения.
area-client
В этом микросервисе рассмотрим как обращаться к gRPC-эндпоинтам нашего сервиса.
Добавляем зависимости:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Наш gRPC-interface --> <dependency> <groupId>ru.acgnn.grpc</groupId> <artifactId>grpc-interface</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
В application.properties укажем следующие параметры:
# Discovery client properties eureka.client.service-url.default-zone=http://localhost:8761/eureka grpc.client.area.address=discovery:///area-server grpc.client.area.negotiation-type=plaintext grpc.client.area.enable-keep-alive=true
grpc.client.area.address — в этом параметре мы указываем, что к клиенту под названием «area» будем обращаться по названию микросервиса через discovery-server (Eureka Server).
Структура сервиса будет выглядить следующим образом:
Рассмотрим, как обращаться к grpc-server’у:
import java.io.IOException; import java.util.UUID; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import com.google.protobuf.ByteString; import com.google.protobuf.Empty; import io.grpc.CallCredentials; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.devh.boot.grpc.client.inject.GrpcClient; import net.devh.boot.grpc.client.security.CallCredentialsHelper; import ru.acgnn.area_client.mapper.ApiMapper; import ru.acgnn.grpc.Area; import ru.acgnn.grpc.AreaId; import ru.acgnn.grpc.AreaList; import ru.acgnn.grpc.AreaServiceGrpc.AreaServiceBlockingStub; import ru.acgnn.grpc.AreaToCreate; import ru.acgnn.grpc.File; @Slf4j @Service @RequiredArgsConstructor public class ApiService { @GrpcClient("area") private AreaServiceBlockingStub grpcClient; private final ApiMapper apiMapper; public Area getById(UUID id) { return grpcClient .withCallCredentials(bearerAuth()) .getAreaById(AreaId.newBuilder().setId(id.toString()).build()); } public AreaList getAll() { return grpcClient .withCallCredentials(bearerAuth()) .getAreas(Empty.getDefaultInstance()); } public UUID create(AreaToCreate area, MultipartFile file) { try { grpcClient .withCallCredentials(bearerAuth()) .saveFile( File.newBuilder() .setContent(ByteString.copyFrom(file.getBytes(), 100, file.getBytes().length)) .setContentType(file.getContentType()) .build() ); } catch (IOException e) { log.debug("IOException: {}", e.getMessage()); } return UUID.fromString(grpcClient .withCallCredentials(bearerAuth()) .createArea(area) .getId() ); } private CallCredentials bearerAuth() { JwtAuthenticationToken token = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); return CallCredentialsHelper.bearerAuth(() -> token.getToken().getTokenValue()); } }
Таким образом, при помощи @GrpcClient мы можем легко интегрироваться с gRPC-сервером, используя аннотацию и указав имя сервиса, зарегистрированного в Eureka Server. Это позволяет нам абстрагироваться от конкретных настроек и сконцентрироваться на бизнес-логике.
Метод withCallCredentials() добавляет токен в каждый запрос. В данном примере токен извлекается из SecurityContextHolder, который содержит JwtAuthenticationToken. Это позволяет нам реализовать защищённый доступ к методам gRPC, используя OAuth 2.0 JWT.
При создании новой сущности через метод create() мы отправляем как данные для создания (AreaToCreate), так и файл (MultipartFile). Файл преобразуется в ByteString и отправляется с указанием MIME-типа, что может быть полезно при обработке файлов на сервере.
Такой подход позволяет гибко взаимодействовать с gRPC-сервисами, не заботясь о низкоуровневой реализации протокола.
Итог
Таким образом, мы разобрали как реализовать микросервисную архитектуру на Java с помощью Spring Boot, Spring Cloud и т.д., где основным средством коммуникации является gRPC. В текущей статье, я старался максимально выжать основное, без воды, делая упор на интеграции gRPC в наши Spring Boot приложения. Если статья будет интересной сообществу, в следующей части, я бы хотел показать, как интегрировать OAuth2.0 в наш gRPC-сервер.
P.S. Это моя первая статья, не судите строго. Очень буду рад обратной связи и помочь Вам в комментариях, если появятся вопросы.
ссылка на оригинал статьи https://habr.com/ru/articles/910092/
Добавить комментарий