Поддержка gRPC в последних версиях Spring Boot шагнула довольно далеко, и теперь можно сделать реализацию и сервера, и клиента, без особых усилий. Когда практически всю конфигурацию можно сделать через properties
файлы с минимальным конфигурированием Spring бинов вручную.
Посмотрим как это можно сделать средствам Spring Boot версии 3.5.3 на примере простеньких сервера и клиента.
Начнем с создания нашего проекта. В качестве системы сборки будем использовать Maven
.
Нам потребуются следующий набор зависимостей:
<dependency>
<groupid>org.springframework.grpc</groupid>
<artifactid>spring-grpc-spring-boot-starter</artifactid>
<version>0.8.0</version>
</dependency>
<dependency>
<groupid>io.grpc</groupid>
<artifactid>grpc-services</artifactid>
<version>1.73.0</version>
</dependency>
<dependency>
<groupid>com.google.protobuf</groupid>
<artifactid>protobuf-java-util</artifactid>
<version>4.30.2</version>
</dependency>
А так же protobuf-maven-plugin
с помощью которого сгенерируем большую часть необходимых Java классов.
<plugin>
<groupid>io.github.ascopes</groupid>
<artifactid>protobuf-maven-plugin</artifactid>
<version>3.4.1</version>
<configuration>
<protocversion>4.30.2</protocversion>
<binarymavenplugins>
<binarymavenplugin>
<groupid>io.grpc</groupid>
<artifactid>protoc-gen-grpc-java</artifactid>
<version>1.73.0</version>
<options>@generated=omit</options>
</binarymavenplugin>
</binarymavenplugins>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
Теперь необходимо сформировать .proto
файл в котором опишем наш сервис.
В качестве примера сделаем простой сервис, который привызове функции SayHi
с именем в теле запроса будет отвечать Hi, ${name}!
Опишен наш сервис в .proto файле, а так же укажем несколько параметров для кодогенерации
syntax = "proto3";
// Описание нашего сервиса
service HelloService {
// Метод который будет принимать запрос с именем и возвращать ответ с приветствием
rpc SayHi (Msg) returns (Response) {
}
}
// Запрос в котором будет передано имя
message Msg {
string name = 1;
}
// Ответ, который будет содержать приветствие
message Response {
string response = 1;
}
// Параметры кодогенерации
option java_multiple_files = true;
option java_package = "pro.nikolaev.grpc_sample.proto";
option java_outer_classname = "SayHiProto";
Файл необходимо разместить в src/main/proto
.
Теперь мы можем сгенерировать Java классы из нашего .proto файла. Для этого достаточно скомпилировать наше приложение средствами Maven:
mvn compile
После чего наши сгенерированные файлы будут находиться в директории target/generated-sources/protobuf
.
Теперь нам нужно отметить эту директорию как исходную для компиляции, чтобы IDE могла использовать эти файлы при компиляции проекта.
Для IDE семейства IntelliJ IDEA необходимо выбрать **Project Structure** -> **Modules** -> **Sources** -> **Add Content Root** -> **target/generated-sources/protobuf**
.
Большая часть необходимого нам кода теперь уже сгенрирована и подключена в проект, нам осталось только добавить немного конфигурации средствами Spring Boot и реализовать логику нашего метода sayHi
.
Начнем с конфигурации Spring Boot.
В application.properties
включим grpc сервер, и отключим клиент. Также можно указать порт на котором будет поднят Netty
и наш gRPC сервер.
# отключим формирование клиента
spring.grpc.client.enabled=false
# включим сервер
spring.grpc.server.enabled=true
# укажем порт сервера
spring.grpc.server.port=9090
Теперь перейдем к реализации метода sayHi
. Для этого нам достаточно сделать сервис расширяющий сгенерированный класс HelloServiceGrpc.HelloServiceImplBase
и реализовать метод sayHi()
. Сделаем что-то вроде этого:
@Service
public class GrpcService extends HelloServiceGrpc.HelloServiceImplBase {
@Override
public void sayHi(Msg request, StreamObserver<response> responseObserver) {
responseObserver.onNext(Response.newBuilder().setResponse("Hi, %s!".formatted(request.getName())).build());
responseObserver.onCompleted();
}
}
В принципе, всё - нвш сервер готов. Можно запустить приложение и проверить его работу.
Что ещё можно сделать?
Можно добавить авторизацию средствами Spring Security
. Посмотрим как это сделать на примере Basic авторизации.
Сначала добавим зависимость Spring Security в pom.xml
:
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-security</artifactid>
</dependency>
Теперь определим бин AuthenticationProcessInterceptor
, например так:
@Bean
@GlobalServerInterceptor
public AuthenticationProcessInterceptor grpcSecurityFilterChain(GrpcSecurity grpc,
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) throws Exception {
return grpc
.authenticationManager(authManager(grpc, userDetailsService, passwordEncoder))
.authorizeRequests(requests -> requests
// метод сервис и метод указанные в нашем .proto файле
.methods("HelloService/SayHi").hasAnyAuthority("USER")
.allRequests().denyAll())
.httpBasic(Customizer.withDefaults())
.build();
}
private AuthenticationManager authManager(final GrpcSecurity grpcSecurity,
final UserDetailsService userDetailsService,
final PasswordEncoder passwordEncoder) throws Exception {
AuthenticationManagerBuilder auth = grpcSecurity.getSharedObject(AuthenticationManagerBuilder.class);
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(daoAuthenticationProvider);
auth.parentAuthenticationManager(providerManager);
auth.authenticationProvider(daoAuthenticationProvider);
return auth.build();
}
Что ещё?
Можно добавить собственную реализацию для глобальной обработки ошибок. Для этого нам нужен бин реализующий GrpcExceptionHandler
.
Так как теперь у нас есть авторизация средствами Spring Security, мы можем использовать сделать собственную реализацию ошибок авторизации.
@Component
public class GrpcErrorHandler implements GrpcExceptionHandler {
@Override
public StatusException handleException(Throwable exception) {
return switch (exception) {
case StatusException e -> new StatusException(e.getStatus(), e.getTrailers());
case AuthenticationException ignored -> new StatusException(Status.UNAUTHENTICATED);
case AccessDeniedException ignored -> new StatusException(Status.PERMISSION_DENIED);
default -> new StatusException(Status.UNKNOWN);
};
}
}
Теперь посмотрим на реализацию клиента.
Нам понадобиться тот же набор зависимостей, что и для сервера, за исключением Spring Security.
Используя тот же .proto файл снова сгенерируем Java классы теперь уже для клиента. После чего в properties файле добавим следующую конфигурацию:
spring.grpc.client.enabled=true
# укажем адрес сервера
spring.grpc.client.default-channel.address=static://localhost:9090
# отключим стандартную авторизацию, потому что наш сервер использует Basic auth
spring.grpc.client.default-channel.secure=false
# отключим сервер
spring.grpc.server.enabled=false
Теперь определим необходимые бины.
Практически вся реализация клиента уже сгенерирована за нас, остается только связать её с контекстом Spring.
Для этого создадим класс конфигурации и с помощью аннотации @ImportGrpcClients
укажем какую реализацию клиента использовать.
@ImportGrpcClients(target = "stub", types = HelloServiceGrpc.HelloServiceBlockingStub.class)
@Configuration
public class GrpcConfig {
}
Наш клиент практически готов.
Осталось добавить бин, который будет отвечать за подстановку заголовков Basic авторизации:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
@GlobalClientInterceptor
public ClientInterceptor authInterceptor(@Value("${app.grpc.client.basic-auth.username}") String username,
@Value("${app.grpc.client.basic-auth.password}") String password) {
return new BasicAuthenticationInterceptor(username, password);
}
Теперь посмотрим на пример использования, для этого создадим сервис с внедренным в него бином HelloServiceGrpc.HelloServiceBlockingStub
:
@Service
public class GrpcClient {
private final HelloServiceGrpc.HelloServiceBlockingStub helloService;
public GrpcClient(HelloServiceGrpc.HelloServiceBlockingStub helloService) {
this.helloService = helloService;
}
public String sayHi(String name) {
return helloService.sayHi(Msg.newBuilder().setName(name).build()).getResponse();
}
}