Поддержка 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 -&gt; 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 -&gt; new StatusException(e.getStatus(), e.getTrailers());
            case AuthenticationException ignored -&gt; new StatusException(Status.UNAUTHENTICATED);
            case AccessDeniedException ignored -&gt; new StatusException(Status.PERMISSION_DENIED);
            default -&gt; 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();
    }
}