У вас была возможность прочитать много статей о создании микросервисов с такими фреймворками, как 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
и @RestClient
Quarkus автоматически использовать этот компонент по умолчанию вместо декларативного 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 .
Вот мой журнал запуска приложения. Он печатает порт прослушивания и список загруженных расширений.
6. Запуск микросервисов на локальной машине
Поскольку мы хотим запустить более одного приложения на одном компьютере, нам необходимо переопределить их прослушивающий порт HTTP по умолчанию. В то время как служба работника все еще работает на 8080
порте по умолчанию , другие микросервисы используют другие порты, как показано ниже.
Отдел-Сервис:
Организация-Сервис:
Давайте проверим межсервисную связь из Swagger UI. Я позвонил в конечную точку, GET /organizations/{id}/with-departments
которая вызывает конечную точку, GET /departments/organization/{organizationId}
предоставляемую службой отдела. Результат виден ниже.
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 минут.
Наконец, после долгого ожидания все мои приложения были развернуты.
Я выставил их вне Minishift, выполнив команды, показанные ниже. Их можно протестировать, используя маршрут OpenShift, доступный в DNS http://${APP_NAME}-myproject.192.168.99.100.nip.io
.
Кроме того, вы можете включить проверки готовности и работоспособности в OpenShift, поскольку они по умолчанию отключены.