Статьи

Краткое руководство по микросервисам с Quarkus на Openshift

У вас была возможность прочитать много статей о создании микросервисов с такими фреймворками, как Spring Boot или Micronaut, в моем блоге. Существует еще одна очень интересная структура, посвященная архитектуре микросервисов, которая становится все более популярной. Он вводится как нативная среда Java следующего поколения Kubernetes / Openshift. Он построен на основе хорошо известных стандартов Java, таких как CDI, JAX-RS и Eclipse MicroProfile, что отличает его от Spring Boot.

Некоторые другие функции, которые могут убедить вас использовать Quarkus, — это чрезвычайно быстрое время загрузки, минимальный объем памяти, оптимизированный для работы в контейнерах и меньшее время до первого запроса. Кроме того, несмотря на то, что это относительно новый фреймворк (текущая версия  0.21), он имеет множество расширений, включая поддержку Hibernate, Kafka, RabbitMQ, Openapi, Vert.x и многих других.

В этой статье я собираюсь рассказать вам о создании микросервисов с помощью Quarkus и их запуске в  OpenShift  (через Minishift ). Мы рассмотрим следующие темы:

  • Создание REST-приложения с проверкой входных данных.
  • Связь между микросервисами с RestClient.
  • Выставление проверок здоровья (живучесть, готовность).
  • Разоблачение OpenAPI / Swagger документации.
  • Запуск приложений на локальном компьютере с помощью плагина Quarkus Maven.
  • Тестирование с JUnit и RestAssured.
  • Развертывание и запуск приложений Quarkus на Minishift с использованием source-2-image.

Открытие дверей с использованием микросервисов на новой платформе.

Вам также может понравиться:
Настройка приложения Quarkus

1. Создание приложения — зависимости

При создании нового приложения вы можете выполнить одну команду Maven, которая использует quarkus-maven-plugin. Список зависимостей должен быть объявлен в параметре -Dextensions.

mvn io.quarkus:quarkus-maven-plugin:0.21.1:create \
    -DprojectGroupId=pl.piomin.services \
    -DprojectArtifactId=employee-service \
    -DclassName="pl.piomin.services.employee.controller.EmployeeController" \
    -Dpath="/employees" \
    -Dextensions="resteasy-jackson, hibernate-validator"

Вот структура нашего pom.xml:

<properties>
    <quarkus.version>0.21.1</quarkus.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-bom</artifactId>
            <version>${quarkus.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<build>
    <plugins>
        <plugin>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-maven-plugin</artifactId>
            <version>${quarkus.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>build</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Для создания простого REST-приложения с проверкой входных данных нам не нужно много модулей. Как вы, наверное, заметили, я объявил только два расширения, что совпадает со следующим списком зависимостей pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-validator</artifactId>
</dependency>

2. Создание приложения — код

Что может быть немного удивительным для пользователей Spring Boot или Micronaut, так это отсутствие основного работоспособного класса со статическим методом <codemain. Класс ресурса / контроллера является де-факто основным классом. Класс и методы ресурса / контроллера Quarkus должны быть помечены с использованием аннотаций из javax.ws.rsбиблиотеки. Вот реализация REST-контроллера внутри employee-service:

@Path("/employees")
@Produces(MediaType.APPLICATION_JSON)
public class EmployeeController {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
     
    @Inject
    EmployeeRepository repository;
     
    @POST
    public Employee add(@Valid Employee employee) {
        LOGGER.info("Employee add: {}", employee);
        return repository.add(employee);
    }
     
    @Path("/{id}")
    @GET
    public Employee findById(@PathParam("id") Long id) {
        LOGGER.info("Employee find: id={}", id);
        return repository.findById(id);
    }
 
    @GET
    public Set<Employee> findAll() {
        LOGGER.info("Employee find");
        return repository.findAll();
    }
     
    @Path("/department/{departmentId}")
    @GET
    public Set<Employee> findByDepartment(@PathParam("departmentId") Long departmentId) {
        LOGGER.info("Employee find: departmentId={}", departmentId);
        return repository.findByDepartment(departmentId);
    }
     
    @Path("/organization/{organizationId}")
    @GET
    public Set<Employee> findByOrganization(@PathParam("organizationId") Long organizationId) {
        LOGGER.info("Employee find: organizationId={}", organizationId);
        return repository.findByOrganization(organizationId);
    }
     
}

Мы используем CDI для внедрения зависимостей и SLF4J для регистрации. Класс контроллера использует компонент хранилища в памяти для хранения и извлечения данных. Компонент репозитория аннотируется CDI @ApplicationScopedи вводится в контроллер:

@ApplicationScoped
public class EmployeeRepository {
 
    private Set<Employee> employees = new HashSet<>();
 
    public EmployeeRepository() {
        add(new Employee(1L, 1L, "John Smith", 30, "Developer"));
        add(new Employee(1L, 1L, "Paul Walker", 40, "Architect"));
    }
 
    public Employee add(Employee employee) {
        employee.setId((long) (employees.size()+1));
        employees.add(employee);
        return employee;
    }
     
    public Employee findById(Long id) {
        Optional<Employee> employee = employees.stream().filter(a -> a.getId().equals(id)).findFirst();
        if (employee.isPresent())
            return employee.get();
        else
            return null;
    }
 
    public Set<Employee> findAll() {
        return employees;
    }
     
    public Set<Employee> findByDepartment(Long departmentId) {
        return employees.stream().filter(a -> a.getDepartmentId().equals(departmentId)).collect(Collectors.toSet());
    }
     
    public Set<Employee> findByOrganization(Long organizationId) {
        return employees.stream().filter(a -> a.getOrganizationId().equals(organizationId)).collect(Collectors.toSet());
    }
 
}

Последний компонент — это класс домена с проверкой:

public class Employee {
 
    private Long id;
    @NotNull
    private Long organizationId;
    @NotNull
    private Long departmentId;
    @NotBlank
    private String name;
    @Min(1)
    @Max(100)
    private int age;
    @NotBlank
    private String position;
     
    // ... GETTERS AND SETTERS
     
}

3. Модульное тестирование

Что касается большинства популярных Java-фреймворков, модульное тестирование с Quarkus очень простое. Если вы тестируете веб-приложение на основе REST, вы должны включить в свой список следующие зависимости pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>

Давайте проанализируем тестовый класс из организации-службы (наш другой микросервис наряду с службой-службой и службой-отделом). Тестовый класс должен быть помечен  @QuarkusTest. Мы можем добавить другие компоненты с помощью @Inject аннотации. Остальное типично для JUnit и RestAssured — мы тестируем методы API, предоставляемые контроллером. Поскольку мы используем репозиторий в памяти, нам не нужно ничего делать, кроме межсервисного взаимодействия (мы обсудим это позже в этой статье). У нас есть несколько положительных сценариев для методов GET, POST и один отрицательный сценарий, который не проходит проверку ввода ( testInvalidAdd).

@QuarkusTest
public class OrganizationControllerTests {
 
    @Inject
    OrganizationRepository repository;
 
    @Test
    public void testFindAll() {
        given().when().get("/organizations").then().statusCode(200).body(notNullValue());
    }
 
    @Test
    public void testFindById() {
        Organization organization = new Organization("Test3", "Address3");
        organization = repository.add(organization);
        given().when().get("/organizations/{id}", organization.getId()).then().statusCode(200)
                .body("id", equalTo(organization.getId().intValue()))
                .body("name", equalTo(organization.getName()));
    }
 
    @Test
    public void testFindByIdWithDepartments() {
        given().when().get("/organizations/{id}/with-departments", 1L).then().statusCode(200)
                .body(notNullValue())
                .body("departments.size()", is(1));
    }
 
    @Test
    public void testAdd() {
        Organization organization = new Organization("Test5", "Address5");
        given().contentType("application/json").body(organization)
                .when().post("/organizations").then().statusCode(200)
                .body("id", notNullValue())
                .body("name", equalTo(organization.getName()));
    }
 
    @Test
    public void testInvalidAdd() {
        Organization organization = new Organization();
        given().contentType("application/json").body(organization).when().post("/organizations").then().statusCode(400);
    }
 
}

4. Межсервисное общение

Поскольку Quarkus предназначен для работы в Kubernetes, он не предоставляет никакой встроенной поддержки для обнаружения сторонних сервисов (например, через Consul или Netflix Eureka) и HTTP-клиента, интегрированного с этим обнаружением. Quarkus предоставляет выделенную клиентскую поддержку для связи REST. Чтобы использовать его, нам сначала нужно включить следующую зависимость:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client</artifactId>
</dependency>

Quarkus предоставляет декларативный REST-клиент на основе MicroProfile REST Client. Вам нужно создать интерфейс с необходимыми методами и аннотировать его  @RegisterRestClient. Другие аннотации почти такие же, как на стороне сервера. Поскольку вы используете @RegisterRestClient для маркировки Quarkus, знайте, что этот интерфейс должен быть доступен для внедрения CDI в качестве клиента REST.

@Path("/departments")
@RegisterRestClient
public interface DepartmentClient {
 
    @GET
    @Path("/organization/{organizationId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganization(@PathParam("organizationId") Long organizationId);
 
    @GET
    @Path("/organization/{organizationId}/with-employees")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId);
     
}

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

@Path("/organizations")
@Produces(MediaType.APPLICATION_JSON)
public class OrganizationController {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(OrganizationController.class);
     
    @Inject
    OrganizationRepository repository;
    @Inject
    @RestClient
    DepartmentClient departmentClient;
    @Inject
    @RestClient
    EmployeeClient employeeClient;
     
    // ... OTHER FIND METHODS
 
    @Path("/{id}/with-departments")
    @GET
    public Organization findByIdWithDepartments(@PathParam("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setDepartments(departmentClient.findByOrganization(organization.getId()));
        return organization;
    }
     
    @Path("/{id}/with-departments-and-employees")
    @GET
    public Organization findByIdWithDepartmentsAndEmployees(@PathParam("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId()));
        return organization;
    }
     
    @Path("/{id}/with-employees")
    @GET
    public Organization findByIdWithEmployees(@PathParam("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setEmployees(employeeClient.findByOrganization(organization.getId()));
        return organization;
    }
     
}

Последней недостающей вещью, необходимой для связи, являются адреса целевых служб. Мы можем предоставить им с помощью поля  baseUri в @RegisterRestClient аннотации. Кажется, что лучшим решением было бы разместить их внутри application.properties. Имя свойства должно содержать полное имя клиентского интерфейса и суффикс mp-rest/url.

pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://localhost:8090
pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://localhost:8080

Я уже упоминал о модульном тестировании и межсервисном взаимодействии в предыдущем разделе. Чтобы протестировать метод API, который взаимодействует с другими приложениями, нам нужно смоделировать REST-клиент. Вот образец макета, созданного для DepartmentClient. Он должен быть виден только во время испытаний, поэтому мы должны поместить его внутрь src/test/java. Если мы аннотировать его @Mockи @RestClientQuarkus автоматически использовать этот компонент по умолчанию вместо декларативного REST определенного клиента внутри src/main/java.

@Mock
@ApplicationScoped
@RestClient
public class MockDepartmentClient implements DepartmentClient {
 
    @Override
    public List<Department> findByOrganization(Long organizationId) {
        return Collections.singletonList(new Department("Test1"));
    }
 
    @Override
    public List<Department> findByOrganizationWithEmployees(Long organizationId) {
        return null;
    }
 
}

5. Мониторинг и документация

Мы можем легко представить проверки работоспособности или документацию API с помощью Quarkus. Документация API построена с использованием OpenAPI / Swagger. Quarkus использует библиотеки, доступные в рамках проекта SmallRye . Мы должны включить в наш список следующие зависимости  pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

Мы можем определить два типа проверки здоровья: готовность и жизнеспособность. Есть доступные под  /health/ready и  /health/live контекстные пути. Чтобы выставить их вне приложения, нам нужно определить компонент, который реализует HealthCheck интерфейс MicroProfile  . Готовность конечной точки должна быть отмечена  @Readiness, а жизненность —  @Liveness.

@ApplicationScoped
@Readiness
public class ReadinessHealthcheck implements HealthCheck {
 
    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.named("Employee Health Check").up().build();
    }
 
}

Чтобы включить документацию Swagger, нам не нужно ничего делать, кроме добавления зависимости. Quarkus также предоставляет встроенный интерфейс для Swagger. По умолчанию он включен в режиме разработки, поэтому, если вы хотите использовать его на производстве, добавьте строку quarkus.swagger-ui.always-include=trueв свой application.propertiesфайл. Теперь, если запустить приложение employee-service локально в режиме разработки, выполнив команду Maven, mvn compile quarkus:devвы можете просмотреть спецификацию API, доступную по адресу http: // localhost: 8080 / swagger-ui .

quarkus-чванство-скриншот

Вот мой журнал запуска приложения. Он печатает порт прослушивания и список загруженных расширений.

quarkus-стартап-код

6. Запуск микросервисов на локальной машине

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

Отдел-Сервис:

quarkus-порт-отдел

Организация-Сервис:

quarkus-порт-организация

Давайте проверим межсервисную связь из Swagger UI. Я позвонил в конечную точку, GET /organizations/{id}/with-departmentsкоторая вызывает конечную точку, GET /departments/organization/{organizationId}предоставляемую службой отдела. Результат виден ниже.

quarkus-коммуникационно-скриншот

7. Запуск микросервисов в OpenShift

Мы уже завершили реализацию нашего примера архитектуры микросервисов и запустили их на локальном компьютере. Теперь мы можем перейти к последнему шагу и попытаться развернуть эти приложения в Minishift. У нас есть несколько разных подходов при развертывании приложения Quarkus в OpenShift. Сегодня я покажу вам, как использовать механизм сборки S2I.

Мы собираемся использовать Quarkus GraalVM Native S2I Builder . Он доступен на quai.io as quarkus/ubi-quarkus-native-s2i. Конечно, перед развертыванием наших приложений нам нужно запустить Minishift. Следуя документации Quarkus, собственная сборка на основе GraalVM потребляет много памяти и ЦП, поэтому я решил установить 6 ГБ и четыре ядра для Minishift.

$ minishift start --vm-driver=virtualbox --memory=6G --cpus=4

Также нам нужно немного изменить исходный код нашего приложения. Как вы, наверное, помните, мы использовали JDK 11 для их локального запуска. Quarkus S2I Builder поддерживает только JDK 8, поэтому нам нужно изменить его в нашем pom.xml. Нам также необходимо включить объявление nativeпрофиля, как показано ниже:

<properties>
    <quarkus.version>0.21.1</quarkus.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
...
<profiles>
    <profile>
        <id>native</id>
        <activation>
            <property>
                <name>native</name>
            </property>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <groupId>io.quarkus</groupId>
                    <artifactId>quarkus-maven-plugin</artifactId>
                    <version>${quarkus.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>native-image</goal>
                            </goals>
                            <configuration>
                                <enableHttpUrlHandler>true</enableHttpUrlHandler>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>2.22.1</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                            <configuration>
                                <systemProperties>
                                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                                </systemProperties>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Два других изменения должны быть выполнены внутри application.propertiesфайла. Нам не нужно переопределять номер порта, поскольку Minishift динамически назначает виртуальный IP для каждого модуля. Межсервисное взаимодействие осуществляется через обнаружение OpenShift, поэтому нам просто нужно указать имя службы вместо localhost.

quarkus.swagger-ui.always-include=true
pl.piomin.services.organization.client.DepartmentClient/mp-rest/url=http://department:8080
pl.piomin.services.organization.client.EmployeeClient/mp-rest/url=http://employee:8080

Наконец, мы можем развернуть наши приложения на Minishift. Для этого вы должны выполнить следующие команды, используя ваш ocклиент:

$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:19.1.1~https://github.com/piomin/sample-quarkus-microservices.git#openshift --context-dir=employee --name=employee
$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:19.1.1~https://github.com/piomin/sample-quarkus-microservices.git#openshift --context-dir=department --name=department
$ oc new-app quay.io/quarkus/ubi-quarkus-native-s2i:19.1.1~https://github.com/piomin/sample-quarkus-microservices.git#openshift --context-dir=organization --name=organization

Как видно из репозитория с приложениями, исходный код доступен в моей учетной записи GitHub по адресу https://github.com/piomin/sample-quarkus-microservices.git . Версия для запуска на Minishift была опубликована в ветке openshift . Версия для запуска на локальной машине доступна в основной ветке. Поскольку все приложения хранятся в одном репозитории, нам необходимо определить параметр context-dirдля каждого отдельного развертывания.

Я был очень разочарован. Хотя установка большего количества памяти и процессора для мини-сдвига мои сборки заняли очень много времени — около 25 минут.

quarkus-строит-скриншот

Наконец, после долгого ожидания все мои приложения были развернуты.

quarkus-OpenShift-обзор

Я выставил их вне Minishift, выполнив команды, показанные ниже. Их можно протестировать, используя маршрут OpenShift, доступный в DNS http://${APP_NAME}-myproject.192.168.99.100.nip.io.

Кроме того, вы можете включить проверки готовности и работоспособности в OpenShift, поскольку они по умолчанию отключены.

quarkus-здоровье-экран

Дальнейшее чтение: