Привет, Хабр!
Продолжая исследование новых фреймворков Java и учитывая ваш интерес к книге о Spring Boot, мы присматриваемся к новому фреймворку Quarkus для Java. Подробное описание его вы найдете здесь, а мы сегодня предлагаем почитать перевод простой статьи, демонстрирующей, как удобно при помощи Quarkus придерживаться чистой архитектуры.
Quarkus быстро приобретает статус фреймворка, от которого никуда не деться. Поэтому я решил лишний раз по нему пройтись и проверить, в какой степени он располагает к соблюдению принципов Чистой Архитектуры.
В качестве отправной точки я взял простой проект Maven, в котором 5 стандартных модулей для создания приложения CRUD REST в соответствии с принципами чистой архитектуры:
Кроме того, мы создадим модуль
Планируя работать с Quarkus, первым делом нужно добавить спецификацию BOM к файлу POM вашего проекта. Эта BOM станет управлять всеми версиями зависимостей, которыми вы будете пользоваться. Также вам понадобится сконфигурировать стандартные плагины для проектов maven в вашем инструменте управления плагинами, как, например, плагин surefire. По мере работы с Quarkus, вы здесь же сконфигурируете и одноименный плагин. Последнее, но немаловажное: здесь понадобится сконфигурировать плагин для работы с каждым из модулей (в <build><plugins>...</plugins></build>), а именно, плагин Jandex. Поскольку Quarkus использует CDI, плагин Jandex добавляет файл индекса в каждый модуль; файл содержит записи обо всех аннотациях, используемых в данном модуле и ссылки с указанием на то, где используется какая аннотация. В результате обращение с CDI значительно упрощается, впоследствии приходится выполнять значительно меньше работы.
Теперь, когда базовая структура готова, можно приступать к созданию полноценного приложения. Чтобы это сделать, необходимо убедиться, что main-partition создает исполняемое приложение Quarkus. Данный механизм проиллюстрирован в любом примере «для быстрого старта», предоставляемом в Quarkus.
Сначала конфигурируем сборку для использования плагина Quarkus:
Далее давайте добавим зависимости в каждый из модулей приложения, где они будут вместе с зависимостями
В качестве варианта, можно добавить профиль, позволяющий позже собрать нативное приложение. Для этого вам в самом деле потребуется дополнительный стенд для разработки (GraalVM,
Теперь, если вы запустите
В данном упражнении пойдем с периферии к сути. Для начала создадим REST-контроллер, который будет возвращать пользовательские данные (в данном примере к ним относится всего лишь имя).
Чтобы использовать JAX-RS API, необходимо добавить зависимость к infra-web POM:
Простейший код контроллера выглядит так:
Если мы сейчас запустим приложение, то сможем вызвать localhost:8080/customer и увидим
Далее добавим кейс и реализацию для данного практического случая. В
В
Чтобы CDI мог увидеть компонент
Для пользования аннотациями CDI необходимо добавить следующие зависимости в POM-файл
Далее нам потребуется изменить контроллер REST, чтобы использовать его в кейсах
Если теперь вы запустите приложение и вызовете localhost:8080/customer, то увидите
Далее займемся предметной областью (domain). Здесь сущность
Также нам понадобится предоставить реализацию шлюза прежде, чем мы сможем приступить к его использованию. Такой интерфейс мы предоставляем в
Для данной реализации мы воспользуемся поддержкой JPA, имеющейся в Quarkus, а также применим фреймворк Panache, который немного упростит нам жизнь. В дополнение к domain нам придется добавить к
Сначала определяем сущность JPA, соответствующую потребителю.
Работая с Panache, можно выбрать один из двух вариантов: либо ваши сущности будут наследовать PanacheEntity, либо вы воспользуетесь паттерном репозиторий/DAO. Я не являюсь фанатом паттерна ActiveRecord, поэтому сам останавливаюсь на репозитории, но с чем будете работать вы – решать вам.
Теперь, когда у нас есть наша сущность JPA и репозиторий, можно реализовать шлюз
Теперь можно изменить код в реализации нашего кейса, так, чтобы в ней использовался шлюз.
Пока мы не можем запустить наше приложение, поскольку приложение Quarkus еще требуется сконфигурировать с необходимыми параметрами персистентности. В
Чтобы просмотреть исходные данные, также добавим файл
Если теперь вы запустите приложение и вызовете localhost:8080/customer, то увидите
Если вы желаете собрать нативное приложение, то делать это необходимо при помощи команды
Протестировать приложение Quarkus можно при помощи соответствующего тестового фреймворка Quarkus. Если снабдить тест аннотацией
Во многих отношениях Quarkus – лютый конкурент Spring Boot. На мой взгляд, некоторые вещи в Quarkus решены даже лучше. Даже притом, что в app-impl есть зависимость фреймворка, это всего лишь зависимость для аннотаций (в случае со Spring, когда мы добавляем
Quarkus быстр, реально быстр. С приложениями такого типа он быстрее Spring Boot. Поскольку, согласно Чистой Архитектуре, большинство (если не все) зависимостей фреймворка должны находиться на внешней стороне приложения, выбор между Quarkus и Spring Boot становится очевиден. В данном отношении достоинство Quarkus заключается в том, что он сразу создавался с учетом поддержки GraalVM, и поэтому ценой минимальных усилий позволяет превратить приложение в нативное. Spring Boot пока отстает от Quarkus в этом отношении, но не сомневаюсь, что скоро наверстает.
Правда, эксперименты с Quarkus также помогли мне осознать, какие многочисленные несчастья ожидают тех, кто попытается применить Quarkus с классическими серверами приложений Jakarta EE. Хотя, пока при помощи Quarkus можно сделать не так много, его генератор кода поддерживает разнообразные технологии, которые пока не так просто использовать в контексте Jakarta EE с традиционным сервером приложений. Quarkus охватывает все основы, которые понадобятся людям, знакомым с Jakarta EE, а разработка на нем – гораздо более гладкая. Будет интересно посмотреть, как экосистема Java переварит такую конкуренцию.
Весь код к данному проекту выложен на Github.
Продолжая исследование новых фреймворков Java и учитывая ваш интерес к книге о Spring Boot, мы присматриваемся к новому фреймворку Quarkus для Java. Подробное описание его вы найдете здесь, а мы сегодня предлагаем почитать перевод простой статьи, демонстрирующей, как удобно при помощи Quarkus придерживаться чистой архитектуры.
Quarkus быстро приобретает статус фреймворка, от которого никуда не деться. Поэтому я решил лишний раз по нему пройтись и проверить, в какой степени он располагает к соблюдению принципов Чистой Архитектуры.
В качестве отправной точки я взял простой проект Maven, в котором 5 стандартных модулей для создания приложения CRUD REST в соответствии с принципами чистой архитектуры:
domain
: объекты предметной области и интерфейсы шлюза для этих объектовapp-api
: интерфейсы приложения, соответствующие практическим кейсамapp-impl
: реализация этих кейсов средствами предметной области. Зависит отapp-api
иdomain
.infra-persistence
: реализует шлюзы, обеспечивающие взаимодействие предметной области с API базы данных. Зависит отdomain
.infra-web
: Открывает рассматриваемые кейсы для взаимодействия с внешним миром при помощи REST. Зависит отapp-api
.
Кроме того, мы создадим модуль
main-partition
, который послужит нам развертываемым артефактом приложения.Планируя работать с Quarkus, первым делом нужно добавить спецификацию BOM к файлу POM вашего проекта. Эта BOM станет управлять всеми версиями зависимостей, которыми вы будете пользоваться. Также вам понадобится сконфигурировать стандартные плагины для проектов maven в вашем инструменте управления плагинами, как, например, плагин surefire. По мере работы с Quarkus, вы здесь же сконфигурируете и одноименный плагин. Последнее, но немаловажное: здесь понадобится сконфигурировать плагин для работы с каждым из модулей (в <build><plugins>...</plugins></build>), а именно, плагин Jandex. Поскольку Quarkus использует CDI, плагин Jandex добавляет файл индекса в каждый модуль; файл содержит записи обо всех аннотациях, используемых в данном модуле и ссылки с указанием на то, где используется какая аннотация. В результате обращение с CDI значительно упрощается, впоследствии приходится выполнять значительно меньше работы.
Теперь, когда базовая структура готова, можно приступать к созданию полноценного приложения. Чтобы это сделать, необходимо убедиться, что main-partition создает исполняемое приложение Quarkus. Данный механизм проиллюстрирован в любом примере «для быстрого старта», предоставляемом в Quarkus.
Сначала конфигурируем сборку для использования плагина Quarkus:
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Далее давайте добавим зависимости в каждый из модулей приложения, где они будут вместе с зависимостями
quarkus-resteasy
и quarkus-jdbc-mysql
. В последней зависимости можно заменить базу данных на ту, что вам больше нравится (учитывая, что впоследствии мы собираемся пойти по нативному пути разработки, и поэтому не можем использовать встраиваемую базу данных, например, H2).В качестве варианта, можно добавить профиль, позволяющий позже собрать нативное приложение. Для этого вам в самом деле потребуется дополнительный стенд для разработки (GraalVM,
native-image
и XCode, если вы используете OSX).<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
Теперь, если вы запустите
mvn package quarkus:dev
из корня проекта, у вас будет действующее приложение Quarkus! Смотреть пока особенно не на что, поскольку у нас пока нет ни контроллеров, ни контента.Добавляем REST-контроллер
В данном упражнении пойдем с периферии к сути. Для начала создадим REST-контроллер, который будет возвращать пользовательские данные (в данном примере к ним относится всего лишь имя).
Чтобы использовать JAX-RS API, необходимо добавить зависимость к infra-web POM:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
Простейший код контроллера выглядит так:
@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerResource {
@GET
public List<JsonCustomer> list() {
return getCustomers.getCustomer().stream()
.map(response -> new JsonCustomer(response.getName()))
.collect(Collectors.toList());
}
public static class JsonCustomer {
private String name;
public JsonCustomer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Если мы сейчас запустим приложение, то сможем вызвать localhost:8080/customer и увидим
Joe
в формате JSON.Добавляем конкретный кейс
Далее добавим кейс и реализацию для данного практического случая. В
app-api
определим следующий кейс:public interface GetCustomers {
List<Response> getCustomers();
class Response {
private String name;
public Response(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
В
app-impl
создадим простейшую реализацию данного интерфейса.@UseCase
public class GetCustomersImpl implements GetCustomers {
private CustomerGateway customerGateway;
public GetCustomersImpl(CustomerGateway customerGateway) {
this.customerGateway = customerGateway;
}
@Override
public List<Response> getCustomers() {
return Arrays.asList(new Response("Jim"));
}
}
Чтобы CDI мог увидеть компонент
GetCustomersImpl
, вам понадобится специальная аннотация UseCase
в том виде, как она определена ниже. Также можно использовать стандартный ApplicationScoped и аннотацию Transactional
, но, создавая собственную аннотацию, вы получаете возможность более логично группировать код и откреплять код вашей реализации от таких фреймворков как CDI.@ApplicationScoped
@Transactional
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
}
Для пользования аннотациями CDI необходимо добавить следующие зависимости в POM-файл
app-impl
в дополнение к зависимостям app-api
и domain
.<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
</dependency>
Далее нам потребуется изменить контроллер REST, чтобы использовать его в кейсах
app-api
....
private GetCustomers getCustomers;
public CustomerResource(GetCustomers getCustomers) {
this.getCustomers = getCustomers;
}
@GET
public List<JsonCustomer> list() {
return getCustomers.getCustomer().stream()
.map(response -> new JsonCustomer(response.getName()))
.collect(Collectors.toList());
}
...
Если теперь вы запустите приложение и вызовете localhost:8080/customer, то увидите
Jim
в формате JSON.Определение и реализация предметной области
Далее займемся предметной областью (domain). Здесь сущность
domain
довольно проста, она состоит из Customer
и интерфейса шлюза, через который мы будем получать потребителей.public class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public interface CustomerGateway {
List<Customer> getAllCustomers();
}
Также нам понадобится предоставить реализацию шлюза прежде, чем мы сможем приступить к его использованию. Такой интерфейс мы предоставляем в
infra-persistence
.Для данной реализации мы воспользуемся поддержкой JPA, имеющейся в Quarkus, а также применим фреймворк Panache, который немного упростит нам жизнь. В дополнение к domain нам придется добавить к
infra-persistence
следующую зависимость:<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
Сначала определяем сущность JPA, соответствующую потребителю.
@Entity
public class CustomerJpa {
@Id
@GeneratedValue
private Long id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Работая с Panache, можно выбрать один из двух вариантов: либо ваши сущности будут наследовать PanacheEntity, либо вы воспользуетесь паттерном репозиторий/DAO. Я не являюсь фанатом паттерна ActiveRecord, поэтому сам останавливаюсь на репозитории, но с чем будете работать вы – решать вам.
@ApplicationScoped
public class CustomerRepository implements PanacheRepository<CustomerJpa> {
}
Теперь, когда у нас есть наша сущность JPA и репозиторий, можно реализовать шлюз
Customer
.@ApplicationScoped
public class CustomerGatewayImpl implements CustomerGateway {
private CustomerRepository customerRepository;
@Inject
public CustomerGatewayImpl(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@Override
public List<Customer> getAllCustomers() {
return customerRepository.findAll().stream()
.map(c -> new Customer(c.getName()))
.collect(Collectors.toList());
}
}
Теперь можно изменить код в реализации нашего кейса, так, чтобы в ней использовался шлюз.
...
private CustomerGateway customerGateway;
@Inject
public GetCustomersImpl(CustomerGateway customerGateway) {
this.customerGateway = customerGateway;
}
@Override
public List<Response> getCustomer() {
return customerGateway.getAllCustomers().stream()
.map(customer -> new GetCustomers.Response(customer.getName()))
.collect(Collectors.toList());
}
...
Пока мы не можем запустить наше приложение, поскольку приложение Quarkus еще требуется сконфигурировать с необходимыми параметрами персистентности. В
src/main/resources/application.properties
в модуле main-partition
добавим следующие параметры.quarkus.datasource.url=jdbc:mysql://localhost/test
quarkus.datasource.driver=com.mysql.cj.jdbc.Driver
quarkus.hibernate-orm.dialect=org.hibernate.dialect.MySQL8Dialect
quarkus.datasource.username=root
quarkus.datasource.password=root
quarkus.datasource.max-size=8
quarkus.datasource.min-size=2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql
Чтобы просмотреть исходные данные, также добавим файл
import.sql
в тот же каталог, из которого добавляются данные.insert into CustomerJpa(id, name) values(1, 'Joe');
insert into CustomerJpa(id, name) values(2, 'Jim');
Если теперь вы запустите приложение и вызовете localhost:8080/customer, то увидите
Joe
и Jim
в формате JSON. Итак, у нас получилось полноценное приложение, от REST до базы данных.Нативный вариант
Если вы желаете собрать нативное приложение, то делать это необходимо при помощи команды
mvn package -Pnative
. На это может потребоваться около пары минут, в зависимости от того, каков ваш стенд для разработки. Quarkus довольно быстр при запуске и без поддержки нативного режима, стартует за 2-3 секунды, но, когда он скомпилирован в нативный исполняемый файл при помощи GraalVM, соответствующее время сокращается менее чем до 100 миллисекунд. Для приложения на Java это просто молниеносная скорость.Тестирование
Протестировать приложение Quarkus можно при помощи соответствующего тестового фреймворка Quarkus. Если снабдить тест аннотацией
@QuarkusTest
, то JUnit сначала запустит контекст Quarkus, а затем выполнит тест. Тест целого приложения в main-partition
будет выглядеть примерно так:@QuarkusTest
public class CustomerResourceTest {
@Test
public void testList() {
given()
.when().get("/customer")
.then()
.statusCode(200)
.body("$.size()", is(2),
"name", containsInAnyOrder("Joe", "Jim"));
}
}
Заключение
Во многих отношениях Quarkus – лютый конкурент Spring Boot. На мой взгляд, некоторые вещи в Quarkus решены даже лучше. Даже притом, что в app-impl есть зависимость фреймворка, это всего лишь зависимость для аннотаций (в случае со Spring, когда мы добавляем
spring-context
, чтобы получить @Component
, мы тем самым добавляем множество зависимостей ядра Spring). Если вам такое не нравится, вы также можете добавить файл Java в главный раздел, использующий аннотацию @Produces
из CDI и создающий там компонент; в таком случае вам не понадобится никаких дополнительных зависимостей в app-impl
. Но по какой-то причине зависимость jakarta.enterprise.cdi-api
мне хочется видеть там меньше, чем зависимость spring-context
.Quarkus быстр, реально быстр. С приложениями такого типа он быстрее Spring Boot. Поскольку, согласно Чистой Архитектуре, большинство (если не все) зависимостей фреймворка должны находиться на внешней стороне приложения, выбор между Quarkus и Spring Boot становится очевиден. В данном отношении достоинство Quarkus заключается в том, что он сразу создавался с учетом поддержки GraalVM, и поэтому ценой минимальных усилий позволяет превратить приложение в нативное. Spring Boot пока отстает от Quarkus в этом отношении, но не сомневаюсь, что скоро наверстает.
Правда, эксперименты с Quarkus также помогли мне осознать, какие многочисленные несчастья ожидают тех, кто попытается применить Quarkus с классическими серверами приложений Jakarta EE. Хотя, пока при помощи Quarkus можно сделать не так много, его генератор кода поддерживает разнообразные технологии, которые пока не так просто использовать в контексте Jakarta EE с традиционным сервером приложений. Quarkus охватывает все основы, которые понадобятся людям, знакомым с Jakarta EE, а разработка на нем – гораздо более гладкая. Будет интересно посмотреть, как экосистема Java переварит такую конкуренцию.
Весь код к данному проекту выложен на Github.
hippoage
Вряд ли Spring Boot быстро наверстает поддержку GraalVM. Основная проблема в рефлексии. Quarkus как раз основное что делает — избавляется от рефлексии.
> Правда, эксперименты с Quarkus также помогли мне осознать, какие многочисленные несчастья ожидают тех, кто попытается применить Quarkus с классическими серверами приложений Jakarta EE.
С одной стороны — это не цель Quarkus (цель — cloud native микросервисы и функции). А с другой непонятно о чем речь.
zlob
>Вряд ли Spring Boot быстро наверстает поддержку GraalVM
Стоит посмотреть micronaut reflection-free GraalVM-friendly nextgen-SpringBoot