В Java HTTP-запросы между сервисами реализуются весьма просто. Так как существует ряд известных открытых HTTP-клиентов, таких как OkHttp и RestTemplate в Spring, то сложность представляет не выбор подходящего кандидата, а дальнейшая с ним работа.

Среди растущего числа распределенных облачных сервисов, где сервера постоянно появляются и исчезают, конечные точки сервисов оказываются динамичны и их сложно знать наперед. По этой причине, прежде чем отправлять запрос сервису, REST-клиенту необходимо интегрироваться в реестр сервисов и находить там его конечную точку.

Помимо этого, нужно обработать сериализацию запрос-ответ, а также реализовать балансировку нагрузки, правильно распределив ее между серверами. Плюсом же к этому нужно организовать систему отказоустойчивости. Все перечисленное представляет собой сквозную функциональность распределенных систем. 

Именно здесь на помощь приходит Spring Cloud OpenFeign. Это не просто HTTP-клиент, а целостное решение задач, сопутствующих современным REST-клиентам. Spring Cloud OpenFeign обеспечивает интеграцию OpenFeign для Spring Boot путем автоматической настройки и привязки к среде Spring.

Сервис OpenFeign, изначально известный как Feign, является детищем Netflix. Он позволяет разработчикам использовать декларативный способ построения HTTP-клиентов с помощью аннотированных интерфейсов и без шаблонного кода. Spring Cloud OpenFeign обеспечивает балансировку нагрузки с помощью Ribbon и очень удобно интегрируется с другими облачными службами, например с Eureka для обнаружения сервисов и Hystrix для отказоустойчивости. Все это предусмотрено в OpenFeign изначально и не требует прописывания дополнительного кода.

В текущей статье вы узнаете:

  • Как с помощью Spring Cloud OpenFeign построить декларативный легко читаемый REST-клиент для вызова сервисов по HTTP.
  • Как настроить Ribbon и конечные точки для балансировки нагрузки. 
  • Как активировать Eureka-клиент в вашем сервисе Spring REST для интеграции с Eureka Server.

Создание Maven-проекта 

Для генерации Maven-проекта со Spring Boot 2.x можно использовать Spring Initializr. Добавьте в этот проект зависимости Spring Web, OpenFeign и Ribbon.

Spring Initializr  —  добавление зависимостей

Если вы начинаете Maven-проект с нуля, то импортируйте Spring Cloud Dependencies POM, чтобы он наследовал все версии артефактов семейства Spring CLoud.

<!-- spring-cloud -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-dependencies</artifactId>
  <version>Hoxton.SR6</version>
  <scope>import</scope>
  <type>pom</type>
</dependency>

Далее добавьте к зависимостям проекта модули Spring Boot Starter Web, Spring Cloud Starter OpenFeign и Spring Cloud Starter Netflix Ribbon.

<!-- spring -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency><dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

В качестве альтернативы можете загрузить весь проект с GitHub.

Создание класса Application для запуска

Как и в любом приложении Spring Boot, для запуска ApplicationContext необходим основной класс. Создайте его с помощью аннотации @SpringBootApplication, добавив главный метод, вызывающий SpringApplication.run() для запуска приложения.

@SpringBootApplication
@EnableFeignClients
public class Application {

  public static final void main(final String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

@SpringBootApplication активирует в приложении сканирование компонентов и автоматическую настройку. Аннотацию @EnableFeignClients мы добавляем, чтобы включить сканирование компонентов для интерфейсов, аннотированных с @FeignClient.

Создание интерфейса REST-клиента

Создайте интерфейс PostmanEchoClient, добавьте к нему аннотацию @FeinClient и назовите ее postman-echo. Spring автоматически просканирует наш интерфейс и создаст реализацию для REST-клиента в среде выполнения. 

@FeignClient(name = "postman-echo")
public interface PostmanEchoClient {
}

Spring использует имя postman-echo, сопровождаемое аннотацией @FeignClient в качестве идентификатора, чтобы создать RibbonClient для клиентского балансировщика нагрузки.

Есть несколько способов предоставить RibbonClient конечную точку сервера для балансировки нагрузки, например использовать настройки Java, свойства приложения Spring или интеграцию с Eureka Server для поиска этой конечной точки. 

Чтобы перейти от жестко закодированного URL конечной точки сервера к решению с балансировкой нагрузки, давайте настроим Ribbon со статическим listOfServers. В разделе src/main/resources файла application.yaml добавьте следующие свойства:

postman+echo:
  ribbon:
    listOfServers: https://postman-echo.com/, https://postman-echo.com/

Postman Echo  —  это сервис, который можно использовать для тестирования REST-клиентов и совершения пробных вызовов API. Он предоставляет конечные точки для GET, POST и PUT с различными механизмами аутентификации.

На официальном сайте Postman Echo можно найти всю документацию по конечным точкам вместе с примерами ответов. 

Добавление клиентского метода GET-запроса 

Далее добавьте в интерфейсе клиента Postman Echo клиентский метод getEcho, который принимает параметры запроса String foo и String bar, возвращая объект EchoGetResponse. Прикрепите к этому GET-запросу path/get при помощи аннотации GetMapping. При вызове этот метод будет вызывать конечную точку GET-запроса Postman Echo.

@GetMapping(
    path = "/get", 
    consumes = "application/json")
EchoGetResponse getEcho(
    @RequestParam("foo") String foo, 
    @RequestParam("bar") String bar
    );

Конечная точка GET-запроса Postman Echo будет повторять все переданные параметры запроса в теле ответа в элементе args. Документация по GET-запросу Postman Echo лежит здесь.

{
  "args": {
    "foo": "abc",
    "bar": "123"
  }
}

Создайте новый класс EchoGetResponse, который будет представлять ответ JSON. EchoGetResponse  —  это класс простого Java-объекта (POJO). Ответ JSON будет десериализован в нашем методе GET-запроса Postman Echo как EchoGetResponse.

public class EchoGetResponse {

  private Args args;

  public Args getArgs() {
    return args;
  }

  public void setArgs(Args args) {
    this.args = args;
  }

  public static class Args {
    private String foo;
    private String bar;
    public String getFoo() {
      return foo;
    }

    public void setFoo(String foo) {
      this.foo = foo;
    }

    public String getBar() {
      return bar;
    }

    public void setBar(String bar) {
      this.bar = bar;
    }
  }
}

Тестирование метода GET-запроса с помощью ‘SpringBootTest’

Мы тестируем вызов клиентского метода GET-запроса к удаленной конечной точке с помощью SpringBootTest. Для этого нужно создать класс PostmanEchoClientTests. Установите для теста случайный порт с помощью аннотации @SpringBootTest.

@SpringBootTest(
    webEnvironment = WebEnvironment.RANDOM_PORT)
class PostmanEchoClientTests {
}

Выполните автоматическое внедрение (autowire) созданного Spring bean-компонента PostmanEchoClient в класс модульного теста. Bean-компонент должен использовать настроенный список серверов Ribbon для балансировки нагрузки на клиентской стороне. 

@Autowired private PostmanEchoClient client;

Создайте метод getEcho с аннотацией @Test, использующий внедренный bean-компонент PostmanEchoClient для вызова конечной точки GET-запроса Postman Echo.

@Test
void getEcho() throws Exception {
  
  final EchoGetResponse response = 
      client.getEcho("abc", "123");
  
  assertThat(
      response.getArgs().getFoo()
      ).isEqualTo("abc");
  assertThat(
      response.getArgs().getBar()
      ).isEqualTo("123");
  
}

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

Добавление клиентского метода POST-запроса

Теперь, протестировав GET, давайте перейдем к запросу POST. Добавьте в интерфейс PostmanEchoClient клиентский метод postEcho, принимающий параметры запроса String foo и String bar. У него должно быть тело запроса объекта EchoPostRequest, а возвращаться им должен объект EchoPostResponse.

Добавьте к этому POST-запросу path/post при помощи аннотации @PostMapping. При вызове этот клиентский метод будет вызывать конечную точку POST-запроса Postman Echo.

@PostMapping(
    path = "/post", 
    consumes = "application/json")
EchoPostResponse postEcho(
    @RequestParam("foo") String foo,
    @RequestParam("bar") String bar,
    @RequestBody EchoPostRequest body);

Конечная точка будет повторять переданные параметры и body запроса в виде ответа в элементах args и data.

{
  "args": {
    "foo": "abc",
    "bar": "123"
  },
  "data": {
    "message": "hello"
  }
}

Создайте новый класс EchoPostRequest, который будет представлять тело JSON-запроса. EchoPostRequest  —  это класс простого объекта Java (POJO), содержащий одно свойство сообщения String. При вызове к конечной точке POST-запроса Postman Echo объект EchoPostRequest будет сериализован в тело JSON-запроса.

public class EchoGetResponse {

  private Args args;

  public Args getArgs() {
    return args;
  }

  public void setArgs(Args args) {
    this.args = args;
  }

  public static class Args {

    private String foo;

    private String bar;
    public String getFoo() {
      return foo;
    }

    public void setFoo(String foo) {
      this.foo = foo;
    }

    public String getBar() {
      return bar;
    }

    public void setBar(String bar) {
      this.bar = bar;
    }
  }
}

Создайте другой класс EchoPostResponse, который будет представлять ответ JSON. Этот класс содержит args со свойствами foo и bar, а также data со свойством message. Ответ JSON будет десериализован как EchoPostResponse в нашем клиентском методе Post-запроса Postman Echo.

Тестирование метода POST-запроса с помощью ‘SpringBootTest’

Снова включаем метод теста postEcho в ранее добавленный класс PostmanEchoClientTests для только что добавленного клиентского метода postEcho. Как и ранее используйте для вызова конечной точки POST-запроса Postman Echo автоматическое внедрение PostmanEchoClient.

@Test
void postEcho() {
  
  final EchoPostRequest request = 
      new EchoPostRequest();
  request.setMessage("xyz");

  final EchoPostResponse response = 
      client.postEcho("abc", "123", request);

  assertThat(
      response.getArgs().getFoo()
      ).isEqualTo("abc");
  assertThat(
      response.getArgs().getBar()
      ).isEqualTo("123");
  assertThat(
      response.getData().getMessage()
      ).isEqualTo("xyz");
  
}

После вызова удаленной точки убедитесь, что возвращаемый ответ EchoPostResponse совпадает с переданными аргументами и телом запроса, подтверждающими его верную десериализацию из ожидаемого формата JSON.

Интеграция с Eureka Server

С помощью простой аннотации и некоторой настройки можно быстро активировать в сервисе Spring REST клиента Netflix Eureka Discovery.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Далее добавьте в класс application аннотацию @EnableDiscoveryClient, чтобы активировать реализацию клиента Netflix Eureka Discovery. Затем он зарегистрируется в реестре сервисов Netflix Eureka Server и будет использовать абстракцию Spring Cloud DiscoveryClient для запроса метаданных, содержащих конечные точки сервисов, которые клиент Ribbon будет использовать для балансировки нагрузки. 

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class Application {

  public static final void main(final String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

И наконец, укажите имя приложения и URL конечной точки Eureka Server в application.yaml.

Клиент Eureka использует имя приложения для регистрации на Eureka Server. Если это имя не указать, ваш сервис будет отображаться на Eureka Server как неизвестный.

spring:
  application:
    name: feignclient

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka

Проделав эти несколько шагов, вы интегрировали сервис Spring REST в Eureka Server.

Теперь, когда ваш REST-сервис готов к подключению к Eureka Server, нужно удалить свойство listOfServers для Ribbon из application.yaml или закомментировать его.

# postmanEcho:
#   ribbon:
#     listOfServers: https://postman-echo.com/

Создание Eureka Server

Создайте с помощью Spring Initializr другой проект  —  на этот раз для Eureka Server, добавив в него зависимость Eureka Server.

Spring Initialzr  —  добавление зависимостей

Если вы начинаете новый Maven-проект, импортируйте Spring Cloud Dependencies POM и добавьте зависимость Spring Cloud Starter Netflix Eureka Server.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

В качестве альтернативы можно загрузить проект Eureka Server с GitHub.

Вам нужно создать стандартный класс точки входа с аннотацией @SpringBootApplication. Для активации реализации Eureka Server также добавьте в него аннотацию @EnableEurekaServer.

@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {

  public static final void main(final String[] args) {
    SpringApplication.run(EurekaServer.class, args);
  }
}

Далее измените порт сервера на 8761.

server:
port: 8761

Наконец, деактивируйте в application.yaml функции саморегистрации и запроса реестра.

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka
    registerWithEureka: false
    fetchRegistry: false

Теперь Eureka Server готов, можете запускать!

Поскольку Postman Echo является внешним сервисом, нужно вручную зарегистрировать его на Eureka Server. Список REST-операций, поддерживаемых Eureka, можно найти здесь. Этот API понадобится вам только для регистрации нового приложения.

Отправьте с помощью Postman запрос на Eureka Server.

Postman  —  запрос на Eureka Server

URL для регистрации Postman Echo на Eureka Server следующий:

http://localhost:8761/eureka/apps/POSTMAN-ECHO

Запросите содержимое для регистрации Postman Echo на Eureka Server так:

{
  "instance": {
    "app": "POSTMAN-ECHO",
    "hostName": "postman-echo.com",
    "vipAddress": "postman-echo",
    "secureVipAddress": "postman-echo",
    "ipAddr": "54.90.58.153",
    "status": "UP",
    "port": {"$": "80", "@enabled": "true"},
    "securePort": {"$": "443", "@enabled": "true"},
    "healthCheckUrl": "http://postman-echo.com/get",
    "statusPageUrl": "http://postman-echo.com/get",
    "homePageUrl": "http://postman-echo.com",
    "dataCenterInfo": {
      "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", 
      "name": "MyOwn"
    }
  }
}

Нужно зарегистрировать приложение как POSTMAN-ECHO, потому что ранее мы назвали @FeignClient как postman-echo.

В результате вы должны получить успешный ответ без содержимого.

Postman — ответ от Eureka Server

Откройте панель Eureka, где должна отразиться регистрация POSTMAN-ECHO.

Eureka Server  —  зарегистрированные инстансы

Еще раз запустите PostmanRestClientTests, чтобы убедиться, что все работает как надо.

Заключение

Мы научились создавать декларативного REST-клиента при помощи Spring Cloud OpenFeign, задействовав Spring Cloud Netflix Ribbon для обеспечения на клиентской стороне балансировки нагрузки и отказоустойчивости. Плюсом к этому мы узнали, как настраивать конечную точку статического сервера для Ribbon и как выполнять интеграцию с Eureka Server для получения списка конечных точек зарегистрированных серверов. Весь исходный код доступен на GitHub.

Благодарю за чтение и надеюсь, что статья оказалась для вас полезна.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Andy Lian: Building a REST Client with Spring Cloud OpenFeign and Netflix Ribbon

Предыдущая статьяНезаслуженно забытый ForkJoinPool
Следующая статьяПравильная мотивация  -  залог успешных сторонних проектов