Микросервисы с Netflix, часть 1: Feign

In English

Недавно на работе исследовал возможность разделения нашего монолитного приложения на сервисы (работаю Java разработчиком). Конечно же, в разумное время распилить наше бэкенд-приложение было невозможно, поэтому исследование было сконцентрировано на постепенном переносе монолитной архитектуры на сервисную. Это, конечно, тоже сложно, но уже не невозможно!

В результате яросного гугления (хихи) к своему удивлению я узнал, что гигант стриминга фильмов и сериалов из США, а именно Netflix, полностью открыл код своего микросервисного стека.

Вот что я узнал пока почитывал про все это: микросервисная архитектура отлично масштабируется и проще в отладке и изменении кода а также то, что в Интернете очень мало гайдов с примерами использования микросервисов Netflix на Spring Cloud без Eureka (которая ответственна за построение регистра сервисов).

Поэтому в этой серии постов я постараюсь пролить немного света на стек микросервисов от Netflix, начиная с Feign - библиотеки для создания декларативных REST-клиентов. За ним последует пост на горячую тему балансера нагрузки  Ribbon (с хардкоднутым списком сервисов), и, надеюсь, еще хватит сил на третий пост о добавлении в эту связку динамического списка сервисов при помощи Eureka.

Немного теории

Прежде чем начинать работать с полностью новой технологией, неплохо бы почитать на эту тему. Вот составленный мной список полезных ссылок по микросервисам на Netflix:
Feign - это первый шаг в реализации архитектуры микросервиса тулами Netflix. В реальности слабо связанных сервисов очень важно чтобы общение между ними было легковесным и простым для отладки. Поэтому для этой цели зачастую используют REST, хотя для некоторых случаев это может быть не лучшим выбором (смотрим, например, дискуссии на эту тему тут и тут). Для упрощения связи по REST мы и используем Feign: при помощи него мы будем поглощать сообщения от других сервисов и автоматически превращать их в Java объекты.

Практика!

Начнем с того, что создадим для нашего REST клиента Spring Boot приложение. Это не то чтобы обязательно, но Boot упрощает конфигурацию и запуск, да и Spring Cloud Feign starter (что такое стартер?) которой мы будем использовать все равно подтягивает Spring Boot в качестве зависимости.

Если "создадим spring boot приложение" не звучит супер просто, то можно воспользоваться этим туториалом. С ним Вы научитесь этому делу буквально минут за 10.

Добавим зависимость от feign starter в наш pom.xml (секция <dependencies>):
Щелкните дважды чтобы выбрать весь код
1
2
3
4
5
<dependency>
 <groupid>org.springframework.cloud</groupId>
 <artifactid>spring-cloud-starter-openfeign</artifactId>
 <version>1.4.0.RELEASE</version>
</dependency>

Теперь нужно понять, где взять REST API из которого будем брать данные. Для полноты демонстрации в этом примере мы потестим все CRUD операции. Так где же взять такой API, который будет соответствовать нашим требованиям? Как ни странно, в существуют бесплатные веб-сайты которые делают именно то что нам надо: предоставляют простой REST API для тестирования. Я нашел 2 таких: ReqRes и JSONplaceholder:

ReqRes и JSONplaceholder предоставляют полноценный REST API чтобы мы могли потестить наше Feign приложение.

Feign использует интерфейсы аннотированные @FeignClient чтобы генерировать API запросы и мапить ответ на Java классы. Давайте сначала составим список вызовов к выбранному нами REST API:

Действие HTTP метод URL
(C) Создать пользователя POST https://jsonplaceholder.typicode.com/users
(R) Получить список пользователей GET https://jsonplaceholder.typicode.com/users
(R)Достать одного пользователя GET https://jsonplaceholder.typicode.com/users/{id}
(U) Обновить детали пользователя PUT or PATCH https://jsonplaceholder.typicode.com/users/{id}
(D) Удалить пользователя DELETE https://jsonplaceholder.typicode.com/users/{id}

Теперь можно создать модели и описать вызовы REST API Feign'у:
  • Добавить в список зависимостей Jackson - сериализатор/десериализатор JSON. Feint будет использовать его для HTTP запросов и ответов.
    (Опционально) Добавить в список зависимостей Lombok.
    Lombok автоматически генерирует стандартные геттеры/сеттеры/конструкторы для POJO. Можно не добавлять, но тогда придется писать весь boilerplate самому.
    Щелкните дважды чтобы выбрать весь код
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <dependencies>
    ...
     <dependency>
        <groupid>org.projectlombok</groupId>
        <artifactid>lombok</artifactId>
        <version>1.16.20</version>
     </dependency>
     <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-jackson</artifactId>
        <version>10.7.0</version>
     </dependency>
    ...
    </dependencies>
  • Создадим модели в пакете model под корневым пакетом.
    Щелкните дважды чтобы выбрать весь код
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    @Data
    public class User {
     
     Long id;
     String name;
     String username;
     String email;
     Address address;
     String phone;
     String website;
     Company company;
    }
     
    @Data
    public class Address {
     
     String street;
     String suite;
     String city;
     String zipcode;
     Geo geo;
    }
     
    @Data
    public class Geo {
     
     String lat;
     String lng;
    }
     
    @Data
    public class Company {
     
     String name;
     String catchphrase;
     String bs;
    }
  • Создадим интерфейс клиента Feign  в пакете feign под корневым пакетом.
    Щелкните дважды чтобы выбрать весь код
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface UserClient {
     
     @RequestMapping(method = RequestMethod.POST, value = "/users")
     User createUser(User user);
     
     @RequestMapping(method = RequestMethod.GET, value = "/users")
     List<user> getUsers();
     
     @RequestMapping(method = RequestMethod.GET, value = "/users/{userId}")
     User getUser(@PathVariable("userId") Long userId);
     
     @RequestMapping(method = RequestMethod.PUT, value = "/users/{userId}")
     User updateUser(@PathVariable("userId") Long userId, User user);
     
     @RequestMapping(method = RequestMethod.DELETE, value = "/users/{userId}")
     void deleteUser(@PathVariable("userId") Long userId);
    }
  • Создадим контроллер с вызовом клиента Feint, просто чтобы убедиться что все работает.
    Щелкните дважды чтобы выбрать весь код
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Controller
    public class UserController {
     
     @RequestMapping("/read")
     @ResponseBody
     String read() {
      UserClient userClient = Feign.builder()
       .contract(new SpringMvcContract())
       .encoder(new JacksonEncoder())
       .decoder(new JacksonDecoder())
       .logger(new Logger.ErrorLogger())
       .logLevel(Logger.Level.FULL)
       .target(UserClient.class, "https://jsonplaceholder.typicode.com");
     
      List<user> users = userClient.getUsers();
     
      return String.format("Retrieved %d users total", users.size());
     }
    }
    Детальное описание что делает каждая строка:

    .contract определяет какими аннотациями мы будем метить Feign интерфейсы. А именно, благодаря SpringMvcContract() можно использовать спринговые аннотации @RequestMapping. Без этого получим UnsatisfiedDependencyException: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userController': Unsatisfied dependency expressed through field 'userClient';
    .encoder и .decoder заставят Feint клент мапить JSON в Java объекты и обратно,
    .logger и .logLevel - логировать каждый запрос и ответ в System.error. Очень удобно для операций create, update и delete - чтобы понять, сработали они или тихо зафейлились,
    .target - в общем и так понятно. Сюда наше приложение будет слать запросы и получать ответы.

  • Запустите получившееся приложение. Теперь заходим на http://localhost:8080/read.
    Сработало! Получается соединиться и получить список юзеров. Можно также обратиться в консоль или окно консоли в IDE за деталями HTTP  коммуникаций:
Это был абсолютный минимум который надо проделать, чтобы попробовать Feint в Spring Cloud.

Если Вам хочется посмотреть как работают другие методы (create, update, delete) и узнать как извлечь UserClient в бин вместо того чтобы создавать его каждый раз когда он понадобится, буду рад продолжить повествование😊

Представим себе настоящее приложение, в котором точно есть как минимум 50 классов. Скажем, около 20 из них будут использовать UserClient для аутентификации. Думаете, писать в каждом классе Feign.build().... - это боль? Тогда подумайте о вполне вероятном сценарии смене транспорта с  JSON на XML! Вот здесь очень помогут замечательные бины. Прежде чем начать реализацию остальных CRUD методов, превратим UserClient в бин:
  • Создадим класс с конфигурацией для контекста приложения Spring в пакете config под корневым пакетом.
    Щелкните дважды чтобы выбрать весь код
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Configuration
    public class Config {
     
     @Value("${feign.userclient.url}")
     String userClientUrl;
     
     @Bean
     UserClient userClient() {
      return Feign.builder()
       .contract(new SpringMvcContract())
       .encoder(new JacksonEncoder())
       .decoder(new JacksonDecoder())
       .logger(new Logger.ErrorLogger())
       .logLevel(Logger.Level.FULL)
       .target(UserClient.class, userClientUrl);
     }
    }

    В каталоге /src/main/resources надо создать файл application.properties с одной-единственной строкой:
    Щелкните дважды чтобы выбрать весь код
    1
  • Теперь UserClient можно автовайрить в любой класс. Теперь можно переписать контроллер, чтобы он включал в себя все CRUD операции и намного более простой автовайреный UserClient:
    Щелкните дважды чтобы выбрать весь код
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @Controller
    public class UserController {
     
     @Autowired
     UserClient userClient;
     
     @RequestMapping("/create")
     @ResponseBody
     String create() {
      User user = userClient.getUser(1L);
      user.setId(null);
      user = userClient.createUser(user);
     
      return String.format("Created a user with id %d", user.getId());
     }
     
     @RequestMapping("/read")
     @ResponseBody
     String read() {
      List<user> users = userClient.getUsers();
     
      return String.format("Retrieved %d users total", users.size());
     }
     
     @RequestMapping("/update")
     @ResponseBody
     String update() {
      User user = userClient.getUser(1L);
      String oldName = user.getName();
      user.setName("John");
      userClient.updateUser(1L, user);
     
      return String.format("Update successful. User id: %d, old name: %s, new name: %s",
       user.getId(), oldName, user.getName());
     }
     
     @RequestMapping("/delete")
     @ResponseBody
     String delete() {
      userClient.deleteUser(1L);
     
      return "Deleted";
     }
    }
  • Если теперь приложение запустилось без ошибок, то автовайринг уже творит свою магию. Наконец, можно опробовать каждый метод при помощи браузера:

Спасибо за то что дочитали до конца! Скоро появится часть 2 этого поста, где мы добавим балансировщик нагрузки Ribbon с захардкоженным списом сервисов!

Как всегда, полный код проекта можно найти тут: GitHub

Comments

  1. "...и намного более простой автовайреный UserService..." так мы вроде внедряем UserClient ?

    ReplyDelete
  2. Здравствуйте,
    com.netflix.feign в поледний раз обновлялся Jul 14, 2016
    лучше взять от github:
    https://mvnrepository.com/artifact/io.github.openfeign/feign-jackson

    ReplyDelete
    Replies
    1. Благодарю! Я так понимаю, вы будете пробовать запускать с github? Напишите, получится или нет, если будет время.

      Delete
  3. Уже было испробовано на момент написания комментария - отлично работает (пробовал с vk.api). Даже код в примере менять не нужно, только зависимость.
    Спасибо за статью :)

    ReplyDelete
    Replies
    1. Наконец нашел время обновить зависимость в посте!

      Delete
  4. Большое спасибо за такое прекрасное объяснение! Сразу всё понятно, и написано по человечески, без непонятной мути

    ReplyDelete
  5. А почему используется Feign.builder() вместо @FeignClient(...) к интерфейсу ?

    ReplyDelete
    Replies
    1. Уже не помню, но полагаю с аннотацией будет даже лучше.

      Delete

Post a Comment