Привет, Хабр!

Продолжая исследование новых фреймворков 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.