null

Micronaut. Краткое руководство о том, как создавать микросервисы с помощью этого JVM-фреймворка

Введение

Работа многих JVM-фреймворков с инверсией управления (IoC frameworks) базируется на применении рефлексии (reflection-based). Это означает, что, для функционирования, таким фреймворкам требуется загружать и кэшировать данные рефлексии для каждого бина (bean) внутри контекста приложения.

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

Среди других ключевых особенностей Micronaut можно выделить:

  • поддержку нескольких языков разработки: Java, Groovy, Kotlin (в планах добавление Scala)
  • встроенную поддержку облачных технологий, включая обнаружение сервисов (служб), распределенную трассировку и облачные среды выполнения.
  • быстрое подключение/настройку многих популярных слоев доступа к данным (datalayer)
  • поддержку API для написания своего собственного слоя доступа к данным
  • использование стандартизированного синтаксиса. Можно использовать знакомые по другим фреймворкам аннотации привычным способом
  • возможность относительно легко покрывать код модульными тестами и мгновенно их запускать
  • ориентированность на применение аспектно-ориентированного стиля программирования
  • поддержку OpenAPI и Swagger
  • AOT компиляцию

В статье рассматривается создание приложения, состоящего из трех микросервисов:

  • микросервиса books (книги), написанного на Groovy
  • микросервиса inventory (книжный реестр), написанного на Kotlin
  • микросервиса gateway (шлюз), написанного на Java

Вы узнаете как:

  • писать эндпоинты и применять выполняемое во время компиляции внедрение зависимостей (complile-time DI)
  • писать функциональные тесты
  • интегрировать микросервисы с системой Consul (https://consul.io)
  • взаимодействовать с микросервисами с помощью декларативного HTTP-клиента Micronaut

Ниже схематично показана структура этого приложения:

Микросервис №1 - "Books" на Groovy

Для того чтобы создать приложение на Micronaut наиболее простым способом, можно воспользоваться интерфейсом командной строки (Micronaut CLI). Его можно установить без особых усилий с помощью менеджера наборов средств разработки SDKMan.

Итак, воспользуемся Micronaut CLI:

mn create-app example.micronaut.books --lang groovy

Micronaut даёт свободу в выборе необходимого фреймворка тестирования и не привязывает его к конкретному языку программирования. Однако, если фреймворк не задан явно, Micronaut выбирает его самостоятельно в зависимости от используемого языка. По умолчанию для Java используется Junit, для Groovy - Spock.

Что касается инструментов сборки, то можно использовать Maven или Gradle. По умолчанию будет использоваться Gradle.

Возвратимся к нашему приложению.

Основной задачей микросервиса «Books» является предоставление каталога книг (по запросу из вне).

Давайте создадим контроллер, в котором будет реализована эта задача:

books/src/main/groovy/example/micronaut/BooksController.groovy
    
package example.micronaut
    
import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
    
@CompileStatic
@Controller("/api")
class BooksController {
    
    private final BooksRepository booksRepository
 
    BooksController(BooksRepository booksRepository) {
        this.booksRepository = booksRepository
    }
    
    @Get("/books")
    List<Book> list() {
        booksRepository.findAll()
    }
}

Здесь стоит обратить внимание на несколько моментов:

  • Контроллер будет реагировать на GET-запрос по адресу /api/books (сработает метод List list())
  • Аннотации @Get и @Controller являются стандартизированными (точно такие же аннотации можно встретить в других фреймворках)
  • Репозиторий BooksRepository поставляется в контроллер посредством DI через конструктор
  • Контроллеры в Micronaut по умолчанию принимают и отдают данные в формате JSON

Ниже приведен код интерфейса BooksRepository и сущности Book:

books/src/main/groovy/example/micronaut/BooksRepository.groovy
    
package example.micronaut
    
interface BooksRepository {
    List<Book> findAll()
}
    
books/src/main/groovy/example/micronaut/Book.groovy
    
package example.micronaut
    
import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor
    
@CompileStatic
@TupleConstructor
class Book {
    String isbn
    String name
}

Создадим singleton-бин, который будет реализовывать интерфейс BooksRepository:

    books/src/main/groovy/example/micronaut/BooksRepositoryImpl.groovy
    
    package example.micronaut
    
    import groovy.transform.CompileStatic
    import javax.inject.Singleton
    
    @CompileStatic
    @Singleton
    class BooksRepositoryImpl implements BooksRepository {
    
        @Override
        List<Book> findAll() {
            [
                new Book("1491950358", "Building Microservices"),
                new Book("1680502395", "Release It!"),
            ]
        }
    }
    

Важно отметить, что Micronaut выполнит ассоциацию и внедрит этот бин во время компиляции.

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

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

Однако функциональные тесты в Micronaut - это радость. Потому что они быстрые, действительно быстрые.

Давайте напишем простой функциональный тест для нашего контроллера:

    books/src/test/groovy/example/micronaut/BooksControllerSpec.groovy
    
    package example.micronaut
    
    import io.micronaut.context.ApplicationContext
    import io.micronaut.core.type.Argument
    import io.micronaut.http.HttpRequest
    import io.micronaut.http.client.RxHttpClient
    import io.micronaut.runtime.server.EmbeddedServer
    import spock.lang.AutoCleanup
    import spock.lang.Shared
    import spock.lang.Specification
    
    class BooksControllerSpec extends Specification {
    
        @Shared
        @AutoCleanup
        EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
    
        @Shared @AutoCleanup RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())
    
        void "test books retrieve"() { 
            when:
            HttpRequest request = HttpRequest.GET('/api/books')
            List<Book> books = client.toBlocking().retrieve(request, Argument.of(List, Book))
    
            then:
            books books.size() == 2
        }
    }
    

Здесь стоит отметить несколько моментов:

  • приложение легко запустить из теста с помощью интерфейса встроенного сервера - EmbeddedServer.
  • не менее легко воспользоваться HTTP-клиентом, который будет связан с этим сервером
  • клиент выполнит всю необходимую работу по преобразованию JSON в объекты.

Микросервис №2 - "Inventory" на Kotlin

Теперь приступим к созданию второго микросервиса на Kotlin.

Выполните следующую команду в CLI:

    mn create-app example.micronaut.inventory --lang kotlin

Микросервис «Inventory», как видно из его названия, предоставляет информацию о текущем количестве книг на складе по каждой книге из каталога.

И так, в начале создадим класс данных (Kotlin Data Class), который будет описывать нашу предметную область:

    inventory/src/main/kotlin/example/micronaut/Book.kt
    
    package example.micronaut
    
    data class Book(val isbn: String, val stock: Int)
    

Далее создадим контроллер, который будет возвращать данные об имеющемся количестве книг:

    inventory/src/main/kotlin/example/micronaut/BookController.kt
    
    package example.micronaut
    
    import io.micronaut.http.HttpResponse 
    import io.micronaut.http.MediaType 
    import io.micronaut.http.annotation.Controller 
    import io.micronaut.http.annotation.Get 
    import io.micronaut.http.annotation.Produces
    import io.micronaut.security.annotation.Secured
    
    @Controller("/api") 
    class BooksController {
    
        @Produces(MediaType.TEXT_PLAIN) 
        @Get("/inventory/{isbn}") 
        fun inventory(isbn: String): HttpResponse<Int> {
            return when (isbn) { 
                "1491950358" -> HttpResponse.ok(2) 
                "1680502395" -> HttpResponse.ok(3) 
                else -> HttpResponse.notFound()
            }
        }
    }
    

Обратите внимание на то, что этот микросервис, в отличии от предыдущего, возвращает данные не в JSON, а в виде простого текста.

Микросервис №3 - "Gateway" на Java

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

Вновь воспользуемся Micronaut CLI:

    mn create-app example.micronaut.gateway

Как вы, наверное, заметили, в этой команде отсутствует флаг «lang». Дело в том, что если его опустить, то языком по умолчанию выбирается Java.

Создадим декларативный HTTP-клиент для взаимодействия с микросервисом «Books».

Сначала опишем интерфейс «получателя (fetcher)» каталога книг. Он пригодится для HTTP-клиента:

    gateway/src/main/java/example/micronaut/BooksFetcher.java
    
    package example.micronaut;
    
    import io.reactivex.Flowable;
    
    public interface BooksFetcher { 
        Flowable<Book> fetchBooks(); 
    }
    

Далее опишем сам декларативный HTTP-клиент – интерфейс с аннотацией @Client.

    gateway/src/main/java/example/micronaut/BooksClient.java
    
    package example.micronaut;
    
    import io.micronaut.context.annotation.Requires; 
    import io.micronaut.context.env.Environment; 
    import io.micronaut.http.annotation.Get; 
    import io.micronaut.http.client.annotation.Client; 
    import io.reactivex.Flowable;
    
    @Client("books") 
    
    @Requires(notEnv = Environment.TEST) 
    
    public interface BooksClient extends BooksFetcher {
    
        @Override @Get("/api/books") Flowable<Book> fetchBooks();
    
    }
    

Методы декларативного клиента будут автоматически реализованы (implemented) во время компиляции, что значительно упрощает разработку.

Также, Micronaut поддерживает концепцию разделения окружения, в котором выполняется приложение (application environment). В коде выше, можно увидеть, как с помощью аннотации @Requires легко отключается загрузка нужных бинов в зависимости от окружения (в данном случае BooksClient не будет доступен для тестового окружения).

Стоит добавить, что в Micronaut хорошо реализована поддержка реактивного программирования.

Так, метод BooksClient::fetchBooks() из листинга выше, возвращает неблокирующие объекты типа Flowable, где Book реализован так:

    gateway/src/main/java/example/micronaut/Book.java
    
    package example.micronaut;
    
    public class Book {
         private String isbn; 
         private String name; 
         private Integer stock;
         
         public Book() {}
    
         public Book(String isbn, String name) { 
             this.isbn = isbn; 
             this.name = name; 
         }
    
     //Getters and setters are omitted for brevity
    }
    

Теперь создадим еще один декларативный HTTP-клиент, но уже для взаимодействия с микросервисом «Inventory».

Интерфейс «получателя» количества книг:

    gateway/src/main/java/example/micronaut/InventoryFetcher.java
    
    package example.micronaut;
    
    import io.reactivex.Maybe;
    
    public interface InventoryFetcher { 
        Maybe<Integer> inventory(String isbn); 
    }
    

Сам клиент:

    gateway/src/main/java/example/micronaut/InventoryClient.java
    
    package example.micronaut;
    
    import io.micronaut.context.annotation.Requires; 
    import io.micronaut.context.env.Environment; 
    import io.micronaut.http.annotation.Get; 
    import io.micronaut.http.client.Client; 
    import io.reactivex.Maybe; 
    
    @Client("inventory") 
    @Requires(notEnv = Environment.TEST)
    public interface InventoryClient extends InventoryFetcher {
        @Override 
        @Get("/api/inventory/{isbn}") 
        Maybe<Integer> inventory(String isbn);
    }
    

А сейчас приступим к написанию контроллера, который объединяет всё воедино и возвращает неблокирующий ответ:

    gateway/src/main/java/example/micronaut/BooksController.java
    
    package example.micronaut;
    
    import io.micronaut.http.annotation.Controller; 
    import io.micronaut.http.annotation.Get;
    import io.micronaut.security.annotation.Secured; 
    import io.reactivex.Flowable;
    import java.util.List;
    
    @Controller("/api") 
    public class BooksController {
    
        private final BooksFetcher booksFetcher; 
        private final InventoryFetcher inventoryFetcher;
    
        public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher) {
            this.booksFetcher = booksFetcher;
            this.inventoryFetcher = inventoryFetcher; 
        }
    
        @Get("/books") Flowable<Book> findAll() { 
            return booksFetcher.fetchBooks()
                       .flatMapMaybe(b -> inventoryFetcher.inventory(b.getIsbn())
                            .filter(stock -> stock > 0)
                            .map(stock -> { 
                                b.setStock(stock); 
                                return b; 
                            })
                        );
    
        }
    }
    

Обратите внимание, что микросервис вернёт только те книги, которые имеются в наличии.

Не забудем и про тестирование.

Для примера покроем функциональным тестом контроллер.

Однако вначале создадим реализации BooksFetcher и InventoryFetcher для тестового окружения.

Бин, реализующий интерфейс BooksFetcher:

    gateway/src/test/java/example/micronaut/MockBooksClient.java
    
    package example.micronaut;
    
    import io.micronaut.context.annotation.Requires; 
    import io.micronaut.context.env.Environment; 
    import io.reactivex.Flowable;
    import javax.inject.Singleton;
    
    @Singleton 
    @Requires(env = Environment.TEST) 
    public class MockBooksClient implements BooksFetcher {
        @Override
        public Flowable<Book> fetchBooks() { 
            return Flowable.just(new Book("1491950358", "Building Microservices"), new Book("1680502395", "Release It!"), new Book("0321601912", "Continuous Delivery:"));
        } 
    }
    

Бин, реализующий интерфейс InventoryFetcher:

    gateway/src/test/java/example/micronaut/MockInventoryClient.java
    
    package example.micronaut;
    
    import io.micronaut.context.annotation.Requires; 
    import io.micronaut.context.env.Environment; 
    import io.reactivex.Maybe;
    import javax.inject.Singleton;
    
    @Singleton 
    @Requires(env = Environment.TEST) 
    public class MockInventoryClient implements InventoryFetcher {
    
        @Override 
        public Maybe<Integer> inventory(String isbn) { 
            if (isbn.equals("1491950358")) { 
                return Maybe.just(2); 
            } 
            if (isbn.equals("1680502395")) { 
                return Maybe.just(0); 
            } 
            return Maybe.empty();
        } 
    }
    

Теперь сам функциональный тест.

В первом микросервисе мы писали тест на Spock, а сейчас напишем с помощью JUnit:

    gateway/src/test/java/example/micronaut/BooksControllerTest.java
    
    package example.micronaut;
    
    import io.micronaut.context.ApplicationContext;
    import io.micronaut.core.type.Argument;
    import io.micronaut.http.HttpRequest;
    import io.micronaut.http.client.HttpClient;
    import io.micronaut.runtime.server.EmbeddedServer;
    import org.junit.AfterClass;
    import org.junit.BeforeClass;
    import org.junit.Test;
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNotNull;
    import java.util.List;
    
    public class BooksControllerTest {
    
        private static EmbeddedServer server; 
        private static HttpClient client;
    
        @BeforeClass 
        public static void setupServer() {
            server = ApplicationContext.run(EmbeddedServer.class);       client = server.getApplicationContext().createBean(HttpClient.class, server.getURL());
        }
    
        @AfterClass 
        public static void stopServer() {
            if (server != null) { 
                server.stop();
            }
            if (client != null) { 
                client.stop();
            }
         }
    
         @Test 
         public void retrieveBooks() { 
             HttpRequest request = HttpRequest.GET("/api/books");         
             List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class)); 
             assertNotNull(books); 
             assertEquals(1, books.size());
         } 
    }
    

Обнаружение сервисов (Service Discovery) с помощью Consul

Рассмотрим, как можно зарегистрировать наши микросервисы в Consul Service discovery.

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

Интеграция Micronaut с Consul довольно проста.

Первое что необходимо сделать — это добавить в каждый микросервис зависимость discovery-client:

    gateway/build.gradle
    runtime "io.micronaut:micronaut-discovery-client"
    
    books/build.gradle
    runtime "io.micronaut:micronaut-discovery-client"
    
    inventory/build.gradle
    runtime "io.micronaut:micronaut-discovery-client"
    

Далее нужно внести некоторые изменения в конфигурационные файлы каждого из приложений, таким образом, чтобы при их (приложений) запуске они регистрировались в системе Consul:

    gateway/src/main/resources/application.yml
    
    micronaut:
        application:
            name: gateway 
        server:
            port: 8080
    consul:
        client:
            registration: 
                enabled: true
            defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    
    
    
    books/src/main/resources/application.yml
    micronaut:
        application:
            name: books
        server:
            port: 8082
    consul:
        client:
            registration: 
                enabled: true
            defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    
    
    
    inventory/src/main/resources/application.yml
    micronaut:
        application:
            name: inventory
        server:
            port: 8081
    consul:
        client:
            registration: 
                enabled: true
            defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    

Идентификаторы, под которыми сервисы регистрируются в Consul, задаются равными значениям свойства «micronaut.application.name».

Важно чтобы имена, которые мы указываем в аннотациях @Client совпадали с этими идентификаторами.

Micronaut позволяет в конфигурационных файлах интерполировать значения переменных окружения со значениями по умолчанию (другими словами - если значение переменной окружения не задано, вместо него подставляется значение по умолчанию):

    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

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

К примеру, создадим файл «application-test.yml», который будет использоваться в тестовом окружении, и зададим другие настройки для регистрации в системе Consul:

    gateway/src/test/resources/application-test.yml
    consul:
        client:
            registration: enabled: false
    
    
    books/src/test/resources/application-test.yml
    consul:
        client:
            registration: enabled: false
    
    
    inventory/src/test/resources/application-test.yml
    consul:
        client:
            registration: enabled: false
    

Запуск системы микросервисов

Начнем с запуска Consul. Самый простой способ это сделать - через Docker:

    docker run -p 8500:8500 consul

Теперь создадим мультипроектную сборку (multi-project build) с помощью Gradle.

Добавим файл settings.gradle в корневую папку:

    settings.gradle
    
    include 'books'
    include 'inventory'
    include 'gateway'
    

Остается запустить каждое приложение параллельно друг другу.

Для этого в Gradle есть удобный параметр ( -parallel ):

    ./gradlew -parallel run

После выполнения этой команды, микросервисы начнут свою работу и будут доступны по портам, указанным в конфигурационных файлах: 8080, 8081 и 8082.

Система Consul имеет свой пользовательский интерфейс. Если перейти в браузере по адресу http://localhost:8500/ui, то мы увидим нечто подобное:

Если в пользовательском интерфейсе в списке служб будут отображаться наши микросервисы (как на картинке), это будет означать, что они при запуске успешно зарегистрировались в системе и готовы к работе (отмечены зеленой галочкой).

Взаимодействовать с микросервисами можно любым удобным для вас способом.

Например, давайте обратимся к микросервису «Gateway» с помощью cURL:

    $ curl http://localhost:8080/api/books 

Ответ:

    [{"isbn":"1680502395","name":"Release It!","stock":3}, {"isbn":"1491950358","name":"Building Microservices","stock":2}]

Если вы дочитали до этого момента, примите от нас поздравления :) Теперь вы знаете как создавать сеть из микросервисов с помощью Micronaut!

Заключение

В статье поэтапно рассматривалось создание трех микросервисов на разных языках: Java, Kotlin и Groovy. В ходе рассмотрения было показано как использовать декларативный HTTP-клиент Micronaut, как создавать функциональные тесты, которые благодаря особенностям фреймворка, имеют высокую скорость выполнения. Также в статье, в исходных кодах, были отображены применение аспектно-ориентированного программирования (AOP) и реализация внедрения зависимостей на этапе компиляции (без применения рефлексии).

Данная статья является отредактированным и дополненным переводом статьи

«Micronaut Tutorial: How to Build Microservices with This JVM-Based Framework» автора Sergio del Amo Caballero.