Забудьте о сторонних стартерах и костылях — Spring gRPC 1.0 GA уже здесь. Теперь можно строить высокопроизводительные RPC-сервисы с Protocol Buffers прямо из коробки, без плясок с бубном.

В новом переводе от команды Spring АйО рассмотрим пошаговую миграцию со старых решений, генерацию кода из .proto, и сравнение с тем, как это работает в Quarkus. 


Эта статья объясняет, как использовать проект Spring gRPC для встроенной поддержки gRPC-сервисов в приложении Spring Boot. Проект Spring gRPC только что анонсировал выпуск версии 1.0 GA. gRPC — это современный фреймворк удаленного вызова процедур (RPC) с открытым исходным кодом, который работает в любой среде.

Комментарий Михаила Поливахи, эксперта сообщества Spring АйО

Строго говоря, для gRPC нужен в обязательном порядке HTTP 2. Это обсуловлено большим количеством причин. Это кстати логично, т.к. и HTTP 2 и gRPC родились внутри Google-а, где gRPC просто строился поверх SPDY, т.е. будущего HTTP 2, но это лирика. Технических причин тоже много.

Например, gRPC поддерживает стриминг данных с сервера (с клиента кстати тоже), и это реализовано через HTTP 2 мультиплексирование.

Или, например, gRPC умеет в backpressure, то есть gRPC клиент может попросить сервер остановить отправку данных. Это реализовано через механизм скользящего окна, он же sliding window, в рамках конкретного стрима данных в HTTP 2 (не путайте с TCP Sliding Window, это немного другое).

По умолчанию он использует Protocol Buffer от Google для сериализации и десериализации структурированных данных. Ранее в проектах Spring не было нативной поддержки gRPC. Поэтому, если вы хотели упростить создание таких приложений с помощью Spring Boot, приходилось использовать сторонние стартеры, такие как net.devh:grpc-server-spring-boot-starter. Этот конкретный проект какое-то время не поддерживался. Однако, если вы хотите использовать его со Spring Boot 3, см. статью.

Вы можете сравнить поддержку Spring, описанную в этой статье, с эквивалентными возможностями в Quarkus, прочитав следующую статью.

Исходный код

Не стесняйтесь использовать исходный код в данной статье, если хотите попробовать это самостоятельно. Для этого вам нужно клонировать мой репозиторий с примерами на GitHub. Он содержит четыре приложения. Два из них, account-service и customer-service, связаны с предыдущей статьей, которая знакомит с Protocol Buffers в Java. Для этой статьи обратите внимание на два других приложения: account-service-grpc и customer-service-grpc. Эти приложения уже были мигрированы на Spring Boot 4. После клонирования репозитория следуйте инструкциям ниже.

Классы моделей Protobuf и сервисы

На первом шаге мы сгенерируем классы моделей и gRPC-сервисы, используя манифесты .proto. Нам нужно включить стандартные схемы Protobuf от Google для использования STD-типов (1).

Комментарий Михаила Поливахи, эксперта сообщества Spring АйО

Речь так называемые Well Known Types, которые де-факто не входят в спецификацию protocol buffers, а являются расширением для формата, но тем не менее эти типы настолько часто используются, что для них написаны свои пакеты. 

Подробнее тут: https://protobuf.dev/reference/protobuf/google.protobuf/

Наш gRPC-сервис будет предоставлять методы для поиска аккаунтов по различным критериям и единственный метод для добавления нового аккаунта (2). Эти методы будут использовать примитивы из пакета google.protobuf.* и классы моделей, определенные внутри файла .proto как сообщения. Мы определяем два сообщения: сообщение Account (3), которое представляет собой единственный класс модели и содержит три поля (id, number и customer_id), и сообщение Accounts, которое содержит список объектов Account (4).

syntax = "proto3";
package model;
option java_package = "pl.piomin.services.grpc.account.model";
option java_outer_classname = "AccountProto";

// (1)
import "empty.proto";
import "wrappers.proto";

// (2)
service AccountsService {
  rpc FindByNumber(google.protobuf.StringValue) returns (Account) {}
  rpc FindByCustomer(google.protobuf.Int32Value) returns (Accounts) {}
  rpc FindAll(google.protobuf.Empty) returns (Accounts) {}
  rpc AddAccount(Account) returns (Account) {}
}

// (3)
message Account {
  int32 id = 1;
  string number = 2;
  int32 customer_id = 3;
}

// (4)
message Accounts {
  repeated Account account = 1;
}

У нас также есть второе приложение customer-service-grpc и, следовательно, еще одна схема Protobuf. Этот gRPC-сервис предлагает несколько методов для поиска объектов и единственный метод для добавления нового клиента (1). Приложение customer-service-grpc взаимодействует с приложением account-service-grpc, поэтому нам нужно сгенерировать сообщения Account и Accounts (2). Конечно, вы можете создать дополнительный общий модуль со сгенерированными классами Protobuf и совместно использовать его в обоих наших примерах приложений. Наконец, нам нужно определить наши классы моделей. Класс Customer включает три примитивных поля: id, pesel и name, тип enum и список аккаунтов, назначенных конкретному клиенту (3). Также есть сообщение Customers, содержащее список объектов Customer (4).

syntax = "proto3";
package model;
option java_package = "pl.piomin.services.grpc.customer.model";
option java_outer_classname = "CustomerProto";
import "empty.proto";
import "wrappers.proto";

// (1)
service CustomersService {
  rpc FindByPesel(google.protobuf.StringValue) returns (Customer) {}
  rpc FindById(google.protobuf.Int32Value) returns (Customer) {}
  rpc FindAll(google.protobuf.Empty) returns (Customers) {}
  rpc AddCustomer(Customer) returns (Customer) {}
}

// (2)
message Account {
  int32 id = 1;
  string number = 2;
  int32 customer_id = 3;
}

message Accounts {
  repeated Account account = 1;
}

// (3)
message Customer {
  int32 id = 1;
  string pesel = 2;
  string name = 3;
  CustomerType type = 4;
  repeated Account accounts = 5;

  enum CustomerType {
    INDIVIDUAL = 0;
    COMPANY = 1;
  }
}

// (4)
message Customers {
  repeated Customer customers = 1;
}

Теперь наша задача — сгенерировать Java-классы из схем Protobuf. Лучше всего использовать для этого специализированный Maven-плагин. В этом упражнении я использую io.github.ascopes:protobuf-maven-plugin. В отличие от нескольких других плагинов, он активно разрабатывается и работает без какой-либо дополнительной настройки. Все, что вам нужно сделать, это разместить схемы в каталоге src/main/proto. По умолчанию классы генерируются в каталоге target/generated-sources/protobuf.

<plugin>
  <groupId>io.github.ascopes</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>4.1.1</version>
  <configuration>
    <protoc>4.33.1</protoc>
    <binaryMavenPlugins>
      <binaryMavenPlugin>
        <groupId>io.grpc</groupId>
        <artifactId>protoc-gen-grpc-java</artifactId>
        <version>1.77.0</version>
        <options>@generated=omit</options>
      </binaryMavenPlugin>
    </binaryMavenPlugins>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Мы также подключим сгенерированный Java-код из каталога target/generated-sources/protobuf как каталог, который содержит исходный код с помощью Maven-плагина build-helper-maven-plugin.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>add-source</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>add-source</goal>
      </goals>
      <configuration>
        <sources>
          <source>target/generated-sources/protobuf</source>
        </sources>
      </configuration>
    </execution>
  </executions>
</plugin>

Использование Spring gRPC на стороне сервера

Заглушки gRPC уже сгенерированы. Для приложения account-service-grpc вы найдете их здесь:

Я создал простой in-memory репозиторий для целей тестирования.

public class AccountRepository {
    List<AccountProto.Account> accounts;
    AtomicInteger id;

    public AccountRepository(List<AccountProto.Account> accounts) {
        this.accounts = accounts;
        this.id = new AtomicInteger();
        this.id.set(accounts.size());
    }

    public List<AccountProto.Account> findAll() {
        return accounts;
    }

    public List<AccountProto.Account> findByCustomer(int customerId) {
        return accounts.stream().filter(it -> it.getCustomerId() == customerId)
                .toList();
    }

    public AccountProto.Account findByNumber(String number) {
        return accounts.stream()
                .filter(it -> it.getNumber().equals(number))
                .findFirst()
                .orElseThrow();
    }

    public AccountProto.Account add(int customerId, String number) {
        return AccountProto.Account.newBuilder()
                .setId(id.incrementAndGet())
                .setCustomerId(customerId)
                .setNumber(number)
                .build();
    }
}

Чтобы использовать gRPC-стартер для Spring Boot, включите следующую зависимость и секцию управления зависимостями. Вы также можете включить модуль, предназначенный для JUnit-тестов.

<dependencies>
  <dependency>
    <groupId>org.springframework.grpc</groupId>
    <artifactId>spring-grpc-spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.grpc</groupId>
    <artifactId>spring-grpc-test</artifactId>
    <scope>test</scope>
  </dependency>

  ...

</dependencies>    

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.grpc</groupId>
      <artifactId>spring-grpc-dependencies</artifactId>
      <version>1.0.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Затем мы должны создать класс реализации gRPC-сервиса. Он должен расширять AccountsServiceImplBase, сгенерированный на основе объявления .proto. Нам также нужно аннотировать весь класс с помощью @GrpcService (1). Конечно, вы можете аннотировать его просто с помощью @Service, но я предпочитаю @GrpcService для большей прозрачности. После этого мы переопределим все методы, доступные через gRPC. Наш сервис использует простой in-memory репозиторий (2). Каждый метод предоставляет объект параметра и класс io.grpc.stub.StreamObserver, используемый для возврата ответов реактивным способом (3) (4).

@GrpcService
public class AccountsService extends AccountsServiceGrpc.AccountsServiceImplBase {

    AccountRepository repository;

    public AccountsService(AccountRepository repository) {
        this.repository = repository;
    }

    @Override
    public void findByNumber(StringValue request, StreamObserver<AccountProto.Account> responseObserver) {
        AccountProto.Account a = repository.findByNumber(request.getValue());
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }

    @Override
    public void findByCustomer(Int32Value request, StreamObserver<AccountProto.Accounts> responseObserver) {
        List<AccountProto.Account> accounts = repository.findByCustomer(request.getValue());
        AccountProto.Accounts a = AccountProto.Accounts.newBuilder().addAllAccount(accounts).build();
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }

    @Override
    public void findAll(Empty request, StreamObserver<AccountProto.Accounts> responseObserver) {
        List<AccountProto.Account> accounts = repository.findAll();
        AccountProto.Accounts a = AccountProto.Accounts.newBuilder().addAllAccount(accounts).build();
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }

    @Override
    public void addAccount(AccountProto.Account request, StreamObserver<AccountProto.Account> responseObserver) {
        AccountProto.Account a = repository.add(request.getCustomerId(), request.getNumber());
        responseObserver.onNext(a);
        responseObserver.onCompleted();
    }
}

Затем мы можем подготовить аналогичную реализацию для приложения customer-service-grpc. На этот раз приложение не только извлекает данные из in-memory базы данных, но и взаимодействует с предыдущим приложением через gRPC. Вот почему наш @GrpcService использует специализированный клиентский bean, о котором вы узнаете больше в следующем разделе.

@GrpcService
public class CustomersService extends CustomersServiceGrpc.CustomersServiceImplBase {
    CustomerRepository repository;
    AccountClient accountClient;

    public CustomersService(CustomerRepository repository, 
                            AccountClient accountClient) {
        this.repository = repository;
        this.accountClient = accountClient;
    }

    @Override
    public void findById(Int32Value request, StreamObserver<CustomerProto.Customer> responseObserver) {
        CustomerProto.Customer c = repository.findById(request.getValue());
        CustomerProto.Accounts a = accountClient.getAccountsByCustomerId(c.getId());
        List<CustomerProto.Account> l = a.getAccountList();
        c = CustomerProto.Customer.newBuilder(c).addAllAccounts(l).build();
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }

    @Override
    public void findByPesel(StringValue request, StreamObserver<CustomerProto.Customer> responseObserver) {
        CustomerProto.Customer c = repository.findByPesel(request.getValue());
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }

    @Override
    public void findAll(Empty request, StreamObserver<CustomerProto.Customers> responseObserver) {
        List<CustomerProto.Customer> customerList = repository.findAll();
        CustomerProto.Customers c = CustomerProto.Customers.newBuilder().addAllCustomers(customerList).build();
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }

    @Override
    public void addCustomer(CustomerProto.Customer request, StreamObserver<CustomerProto.Customer> responseObserver) {
        CustomerProto.Customer c = repository.add(request.getType(), request.getName(), request.getPesel());
        responseObserver.onNext(c);
        responseObserver.onCompleted();
    }
}

Взаимодействие между gRPC-сервисами с помощью Spring

Для приложения customer-service-grpc мы также сгенерировали заглушки для взаимодействия с приложением account-service-grpc. Список сгенерированных классов показан ниже.

Вот реализация bean'а AccountClient. Он оборачивает метод findByCustomer, предоставляемый сгенерированным клиентом AccountsServiceBlockingStub для вызова endpoint'а из приложения customer-service-grpc.

@Service
public class AccountClient {

    private static final Logger LOG = LoggerFactory.getLogger(AccountClient.class);
    AccountsServiceGrpc.AccountsServiceBlockingStub accountsClient;

    public AccountClient(AccountsServiceGrpc.AccountsServiceBlockingStub accountsClient) {
        this.accountsClient = accountsClient;
    }

    public CustomerProto.Accounts getAccountsByCustomerId(int customerId) {
        try {
            return accountsClient.findByCustomer(Int32Value.newBuilder()
                    .setValue(customerId)
                    .build());
        } catch (final StatusRuntimeException e) {
            LOG.error("Error in communication", e);
            return null;
        }
    }
}

Затем AccountsServiceBlockingStub должен быть зарегистрирован как Spring bean. Мы должны внедрить GrpcChannelFactory в конфигурацию приложения и использовать его для создания gRPC-канала. Реализация GrpcChannelFactory по умолчанию создает "именованный" канал, используемый для получения конфигурации, необходимой для подключения к серверу.

@Bean
AccountsServiceGrpc.AccountsServiceBlockingStub accountsClient(GrpcChannelFactory channels) {
  return AccountsServiceGrpc.newBlockingStub(channels.createChannel("local"));
}

Наконец, мы должны установить целевой адрес для "именованного" канала в свойствах конфигурации Spring Boot. Кроме того, мы также должны переопределить порт gRPC по умолчанию для текущего приложения, поскольку порт по умолчанию 9090 уже занят приложением account-service-grpc.

spring.grpc.server.port: 9091
spring.grpc.client.channels.local.address: localhost:9090

Вызов gRPC-сервисов

В этом разделе мы будем использовать инструмент grpcurl для обнаружения и вызова gRPC-сервисов. Существует несколько вариантов установки GRPCurl. В macOS мы можем использовать следующую команду Homebrew:

brew install grpcurl

Давайте запустим оба наших примера приложений:
$ cd account-service-grpc
$ mvn spring-boot:run
$ cd customer-service-grpc
$ mvn spring-boot:run

После запуска приложения вы должны увидеть вывод, аналогичный показанному ниже.

Мы можем использовать CLI-инструмент grpcurl для вызова gRPC-сервисов, предоставляемых нашим примером приложения Spring Boot. По умолчанию gRPC-сервер запускается на порту 9090 в режиме PLAINTEXT. Чтобы вывести список доступных сервисов, нам нужно выполнить следующую команду:

$ grpcurl --plaintext localhost:9090 list
grpc.health.v1.Health
grpc.reflection.v1.ServerReflection
model.AccountsService

Затем давайте отобразим список методов, предоставляемых model.AccountService:

$ grpcurl --plaintext localhost:9090 list model.AccountsService
model.AccountsService.AddAccount
model.AccountsService.FindAll
model.AccountsService.FindByCustomer
model.AccountsService.FindByNumber

Теперь давайте вызовем endpoint, описанный командой, видимой выше. Имя нашего метода — model.AccountsService.FindByNumber. Мы также устанавливаем входной строковой параметр в значение 222222. Мы можем повторить вызов несколько раз с разными значениями параметров (111111, 222222, 333333, …).

$ grpcurl --plaintext -d '"222222"' localhost:9090 model.AccountsService.FindByNumber

{
  "id": 2,
  "number": "222222",
  "customer_id": 2
}

Наконец, мы можем вызвать метод добавления нового аккаунта. Он принимает JSON-объект в качестве входного параметра. Затем он вернет вновь созданный объект Account с увеличенным полем id.

$ grpcurl --plaintext -d '{"customer_id": 6, "number": "888888"}' localhost:9090 model.AccountsService.AddAccount

{

  "id": 8,

  "number": "888888",

  "customer_id": 6

}

Spring gRPC включает некоторые специфические метрики в endpoint metrics Actuator.

Метрики Actuator для gRPC позволяют нам измерять количество запросов и общее время обработки для конкретного сервиса. Чтобы проверить эту статистику для сервиса FindByNumber, вызовите метрику grpc.server, как показано ниже.

Чтобы протестировать взаимодействие между gRPC-сервисами, мы должны вызвать сервис FindById, предоставляемый приложением customer-service-gprc. Этот сервис использует клиентскую поддержку Spring gRPC для вызова сервиса FindByCustomer, предоставляемого приложением account-service-gprc. Ниже приведен пример вызова с ответом.

$ grpcurl --plaintext -d '1' localhost:9091 model.CustomersService.FindById

{
  "id": 1,
  "pesel": "12345",
  "name": "Adam Kowalski",
  "accounts": [
    {
      "id": 1,
      "number": "111111",
      "customer_id": 1
    },
    {
      "id": 5,
      "number": "555555",
      "customer_id": 1
    }
  ]
}

Поддержка тестирования Spring для gRPC

Spring предоставляет поддержку тестирования для gRPC. Мы можем запустить внутрипроцессный gRPC-сервер как часть теста @SpringBootTest с аннотацией @AutoConfigureInProcessTransport. Такой сервер не прослушивает сетевой порт. Чтобы подключить тестовый клиент к внутрипроцессному серверу, мы должны использовать автоматически сконфигурированный GrpcChannelFactory. Bean AccountsServiceBlockingStub создается в классе @TestConfiguration, который использует GrpcChannelFactory для создания канала для целей тестирования. Затем мы можем внедрить клиентский bean AccountsServiceBlockingStub и использовать его для вызова gRPC-сервисов.

@SpringBootTest
@AutoConfigureInProcessTransport
public class AccountServicesTests {

    @Autowired
    AccountsServiceGrpc.AccountsServiceBlockingStub service;

    @Test
    void shouldFindAll() {
        AccountProto.Accounts a = service.findAll(Empty.newBuilder().build());
        assertNotNull(a);
        assertFalse(a.getAccountList().isEmpty());
    }

    @Test
    void shouldFindByCustomer() {
        AccountProto.Accounts a = service.findByCustomer(Int32Value.newBuilder().setValue(1).build());
        assertNotNull(a);
        assertFalse(a.getAccountList().isEmpty());
    }

    @Test
    void shouldFindByNumber() {
        AccountProto.Account a = service.findByNumber(StringValue.newBuilder().setValue("111111").build());
        assertNotNull(a);
        assertNotEquals(0, a.getId());
    }

    @Test
    void shouldAddAccount() {
        AccountProto.Account a = AccountProto.Account.newBuilder()
                .setNumber("123456")
                .setCustomerId(10)
                .build();
        a = service.addAccount(a);
        assertNotNull(a);
        assertNotEquals(0, a.getId());
    }

    @TestConfiguration
    static class Config {

        @Bean
        AccountsServiceGrpc.AccountsServiceBlockingStub stub(GrpcChannelFactory channels) {
            return AccountsServiceGrpc.newBlockingStub(channels.createChannel("local"));
        }
    }
}

Давайте запустим наши тесты. Я использую IDE, но вы можете выполнить их с помощью команды mvn test.

Заключение

Встроенная поддержка gRPC в Spring — это значительный шаг вперед. До сих пор эта функциональность отсутствовала, но разработанные сообществом проекты, такие как этот, в конечном итоге были заброшены. Проект Spring gRPC все еще находится на относительно ранней стадии разработки. Чуть более недели назад была официально выпущена версия 1.0. Стоит следить за его развитием, пока мы ждем новых функций. Однако на данном этапе мы уже можем значительно упростить работу.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Комментарии (3)


  1. ilyakharlamov
    20.12.2025 12:50

    Транскодинг в JSON поддерживается? Как сделано в Аrmeria и в Microsoft MVC, чтобы из браузера тоже можно было делать запросы?


  1. lokrusta
    20.12.2025 12:50

    Спасибо за статью. Есть несколько моментов (не про Spring, но про grpc), которые представляют сложность, особенно, при миграции из json/openApi на grpc:

    1) Что с логированием Grpc-объектов? В общем случае хочется залогировать в удобно-читаемом виде "все что пришло на вход" и "все что ушло", замаскировав чувствительные данные. В json это делается на раз, с grpc с этим были проблемы - стандартное логирование не кастомизируется, ObjectMapper не применим из-за сложной структуры grpc-объектов

    2) proto-схемы поддерживают указание ограничений на поля, аналогично схемам данных json? Спринговая валидация это поддерживает?

    3) Java 17 достаточна для Spring gRPC 1.0? (вроде, да)

    4) Еще один момент: правила маршрутизации на основе Istio (Virtual Service+Destination Rule) часто завязываются на URL, а в случае с GRPC URL всегда корневой, и могут понадобиться дополнительные доработки (например, добавление заголовков) для того, чтобы понимать, какой именно сервис вызван


  1. skipper-mouse
    20.12.2025 12:50

    Хорошие новости, этого действительно не хватало.
    Нужно будет потыкать