В мире микросервисов зачастую возникает нужда в быстром общении между сервисами, как альтернатива 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. Это моя первая статья, не судите строго. Очень буду рад обратной связи и помочь Вам в комментариях, если появятся вопросы.
BugM
Вы не Гугл. Не нужен вам grpc. У вас нет той нагрузки где его оптимальность перевесит проблемы его сложности.
h0riz4n Автор
Я согласен со следующими утверждениями:
1. Мы не Google.
2. У нас нет той нагрузки, где его оптимальность перевесит проблемы его сложности.
gRPC — это не панацея, и его применение должно быть обоснованным. В статье я старался показать саму технологию и её возможности, чтобы читатели могли поближе познакомиться с ней и с подводными камнями, которые могут встретиться при её интеграции.
BugM
Проблемы там мягко говоря другие.
Начнем со сокрытых за парой слоев абстракции пулов. Которые внезапно заканчиваются. На проде в черную пятницу в 4 утра по времени того кто чинить будет, естественно.
h0riz4n Автор
Поэтому статья ориентирована не для начинающих разработчиков, а для более опытных, которые уже успели столкнуться с неожиданными сюрпризами в проде и знают, как важно учитывать такие моменты заранее.