Знайомство з Quarkus фреймворком для Java-розробника
Вітаю всіх читачів ресурсу!
Приводом для написання цієї статті стало, в першу чергу, бажання поділитись враженнями від вже не зовсім нового фреймворку під назвою Quarkus, який крок за кроком стає відомим гравцем ринку Java-розробки. Цей фреймворк згадується на DOU переважно в контексті вебінарів, дайджестів та планах доповідей для конференцій, тому метою є спроба розказати про Quarkus тим, хто з ним не встиг познайомитись, показати деякі з його реалізацій та підходів, і, звісно, подискутувати в коментарях.
Розпочати пропоную з опису фреймворку від його розробників з команди Red Hat:
«Quarkus is a full-stack, Kubernetes-native Java framework made for Java virtual machines (JVMs) and native compilation, optimizing Java specifically for containers and enabling it to become an effective platform for serverless, cloud, and Kubernetes environments.»
Іншими словами, тепер, коли serverles архітектура і контейнери з різними оркестраторами перестали бути хайпом і зайняли свою частину ринку, починають з’являтись інструменти, які дозволяють працювати з такими технологіями більш ефективно, надаючи розробнику більше варіантів для вибору.
На сайті фреймворку нам доступний Quarkus bootstrap (аналог spring initializr), який дозволяє швидко зібрати проект на основі необхідного функціоналу. Використовувати для цього можна Maven або Gradle. Невеликий перелік бібліотек та імплементацій, які інтегровані у фреймворк:
— RESTeasy як JAX-RS імплементація
— Agroal для роботи з датасорсами (JDBC), Hibernate/Panache ORM, свої клієнти для MongoDB, Neo4j і т. д.
— Vert.x (core, RxJava2, Mutiny) для асинхронної взаємодії з базами, ресурсами, чергами та ін.
— JUnit5, Mockito для тестування, відмінна інтеграція з RESTassured та Testcontainers
— безліч імплементацій MicroProfile специфікацій для OpenAPI, health моніторів, метрик, fault tolerance інструментів (@CircuitBreaker, @Fallback, @Retry etc) та ін.
І ще дуже багато всього на будь-який смак...
Також з коробки доступна підтримка GraalVM та створення native image без складних конфігурацій.
Для розуміння що це таке і як працює дуже рекомендую прослухати випуск 86 підкасту Scalalaz, де безпосередньо один з розробників GraalVM Олег Шелаєв розказує багато цікавої інформації про цю технологію.
Девелоперам, яких цікавить швидкий процес розробки, буде приємно дізнатись про hot/live reloading, що дозволяє при роботі у dev mode автоматично підтягувати зміни в коді під час наступного запиту на ендпоінт без перезапуску сервіса.
Для всіх, кому цього опису буде замало, на сайті фреймворку є прекрасна шпаргалка з детально розписаним функціоналом і змінами по кожній версії. Також є не менш інформативні гайди від команди розробників Quarkus.
Подивимось на невеликі приклади практичного використання того функціоналу, з яким працює кожен бекенд розробник.
Конфігурація
Способи конфігурації досить стандартні — application.properties або yaml-файли. Писати кастомні провайдери конфігурацій можна.
Дефолтних конфігураційних профілів три: dev, test, prod. Перші два підтягуються для конкретного режима запуску, останній — якщо режим запуску не вказаний. Створювати свої профілі також можливо. Якщо потрібно змінювати режим запуску в рантаймі, або дізнатись/змінити конфігураційний профіль, то використовуємо сутність ProfileManager.
Приклад використання config property для двох режимів (application.yaml):
quarkus: #prod mode datasource: jdbc.url: jdbc:postgresql://{$DATABASE_URL}/dbname ssl: true "%dev": #dev mode quarkus: datasource: jdbc.url: jdbc:postgresql://localhost:5432/dbname ssl: false
Якщо проперті для конкретного режиму не вказана, вона підтягується з prod конфігурації.
Для інжекту використовуємо або конфігураційні класи з анотацією @ConfigProperties, або інжектимо безпосередньо в поле. Все звично і зручно:
@ConfigProperty(name = "greeting.message") String message;
Відсутність явного модифікатора доступу (package-private) до змінних такого типу зумовлена тим, що Quarkus рекомендує не використовувати приватні змінні для DI, або інжектити у приватну змінну за допомогою конструктору. Такі рекомендації направлені на покращення виконання коду у GraalVM.
REST-ресурси
Варіативність використання ресурсів для rest-взаємодії у Quarkus виходить за межі розповсюдженого підходу. Нас не те щоб підштовхують, але точно натякають всіма способами на те, що поділ бекенду на шари — не обов’язок, а конкретний шаблон для конкретних випадків. І імплементація функціоналу має розглядатись з урахуванням вимог до нього, а не будуватись за одним задерев’янілим патерном. Тому, oкрім звичайної імплементації ресурсу з опціонально вбудованими серіалізаторами, фреймворк надає як можливість працювати з базою через методи сутності (PanacheEntity підхід), так і напряму пов’язувати ресурс з ORM.
Трошки прикладів далі.
Entity-клас, успадковуємось від PanacheEntity або PanacheEntityBase:
@Entity public class Employee extends PanacheEntity { // fields }
Метод в рест-ресурсі:
@GET @Path("/employee/{id}") public Response getEmployee(@PathParam("id") Long id) { Employee employee = Employee.findById(id); // processing logic }
Зайвих прошарків для findById імплементувати не треба. Достатньо тільки прописати датасорс у конфігураційному файлі. Підготовлених методів чимало: окрім дефолтних вибірок з сортуванням є і використання HQL, і ланцюги операцій. Приклад отримання 4 сторінки виборки зі 100 елементів з конкретним статусом:
return Employee.find("status", Status.Fired) .page(Page.of(4, 100)) .list(Sort.by("reason");
Для транзакційних операцій використовуємо знайому @Transactional анотацію.
Якщо готового функціоналу не вистачає, а дописувати у Entity статичні методи не дозволяє ООП-релігія, то звичайний repository патерн до ваших послуг:
@ApplicationScoped public class PersonRepository implements PanacheRepository<Person> { public Person findByName(String name) { return find("name", name).firstResult(); } public void updateStatus(Long id, Status status) { int counter = update("status = ?1 where id = ?2", status, id); // counter handling logic } }
А от наступний приклад фанатичним прихильникам ООП краще взагалі не дивитись. Ресурс-репозиторій:
public interface PeopleResource extends PanacheEntityResource<Person, Long> { } @ResourceProperties(path = "people") public interface PeopleResource extends PanacheEntityResource<Person, Long> { @MethodProperties(path = "all") Response list(); @MethodProperties(exposed = false) void delete(Long id); }
Періодично, під час перегляду документації, у мене виникає бажання написати хлопцям з Quarkus-команди «горшочек не вари».
Особливо враховуючи те, що є багато простору для вдосконалення функціоналу там, де це дійсно потрібно.
Наприклад, для декількох датасорсів в Quarkus можна використовувати тільки Agroal (JDBC), а ось для Hibernate ORM цей функціонал тільки запланований. Або, з нещодавно відкритих незручностей, альтернатива використання Criteria у Panache ще на етапі планування. І там, де хочеться написати простенький HQL або щось по типу фільтра, доводиться тільки для отримання білдера розмазувати код на
Session session = Panache.getEntityManager().unwrap(Session.class); CriteriaBuilder cb = session.getCriteriaBuilder();
І це ми ще навіть не почали писати критерій...
Але, здебільшого, функціонал повний і інтуїтивно простий.
ContainerRequestFilter та ContainerResponseFilter допомагають перехоплювати запити відповідно до та після опрацювання на ресурсі. Можна інжектити HttpRequest або UriInfo напряму в ресурсний клас і витягувати з них необхідну інформацію.
Для валідації Quarkus використовує розповсюджений сьогодні Hibernate Validator.
Тестування
Тестовому фреймворку для виконання необхідна анотація @QuarkusTest з quarkus-junit5 модуля, в дефолтному білді також присутня io.rest-assured залежність і вже написаний тест із запитом на ендпоінт. Http-порт для тестів за замовчуванням 8081.
Над тестовим класом можна вказати назву rest-ресурсу:
@TestHTTPEndpoint(GreetingResource.class) public class GreetingResourceTest { @Test public void testHelloEndpoint() { given() .when().get() .then() .statusCode(200) .body(is("hello")); } }
Прописувати шлях при запиті до ендпоінту такого ресурсу не потрібно.
Якщо треба запустити конкретний тестовий ресурс до старту самого тесту, то використовуємо QuarkusTestResourceLifecycleManager. Це популярний кейс при використанні фреймворку Testcontainers, або деяких ресурсів Quarkus’а, доступних з коробки (H2DatabaseTestResource).
Все це потім інжектиться в клас з тестами анотацією @QuarkusTestResource.
Управління бінами
Quarkus використовує часткову імплементацію CDI 2.0 специфікації для менеджменту бінів. Ось тут можна подивитись, що відповідає специфікації, а тут що не відповідає, або поки що не імплементовано.
Межі (скоуп) бінів:
ApplicationScoped - сінглтон в межах аплікейшена, який створюється під час виклику його методу у місці інжекту (lazy)
Singleton - сінглтон в межах аплікейшена, але вже не lazy. Створюється одразу в точці інжекту
RequestScoped - один екземпляр на http-запит
Dependent - «non-shareable» бін, екземпляр якого створюється для кожної точки інжекту. Його життєвий цикл пов’язаний з біном, в який він інжектиться
SessionScoped - бін для роботи в межах HttpSession
Інжектити можна тільки у біни зі скоупом ширше за свій. Наприклад, RequestScoped біни у ApplicationScoped.
Також можливий інжект кастомних змінних або бінів (скоуп буде за замовчуванням Dependent):
@ApplicationScoped public class Producers { @Produces List<Integer> values() { List<Integer> values = new ArrayList<>(); values.add(100); return values; } } @ApplicationScoped public class SomeConsumer { @Inject List<Integer> values; // some logic }
Для управління життєвим циклом бінів використовуємо інтерсептори @PostConstruct, @PreDestroy, @AroundConstruct та @AroundInvoke.
Перші два досить відомі, щоб на них зупинятись. Oстанні два використовуються безпосередньо в інтерсепторах, @AroundConstruct викликається після того, як проінжектились всі залежності пов’язаних з цільовим класом інтерсепторів:
@AroundConstruct Object doSomething(InvocationContext context) throws Exception { // ...custom logic Object target = context.proceed(); // ...custom logic return target; }
Цільовий клас створюється після того, як відпрацювали всі InvocationContext.proceed() методи його інтерсепторів. Після цього виконується @PostConstruct логіка біна.
@AroundInvoke — відрізняється від AroundConstruct тим, що перехоплює виклики методів цільового класу («бізнес» інтерсептор).
Концепція CDI Events дозволяє створювати івенти і спостерігати за ними. Для цього інжектимо спеціальний event-бін у необхідний клас і використовуємо його у методі для створення івенту:
class MyEvent { } @ApplicationScoped class SomeService { @Inject Event<MyEvent> event; void doSomething() { // ... event.fire(new MyEvent()); } } @ApplicationScoped class SomeHandler { void handleMyEvent(@Observes MyEvent event) { // ...do something } }
Для обробки старту та закінчення аплікейшeна можна таким чином використовувати вбудовані StartupEvent (відбувається після ініціалізації контексту) та ShutdownEvent.
А якщо потрібно створити кастомний бін після ініціалізації контексту, то проставляємо над ним анотацію @Startup. Вона створює неявний спостерігач за StartupEvent у біні.
На цьому прикладі буду закінчувати цю частину статті.
В наступній спробую розписати більш детально як Quarkus працює в native режимі, його інструменти для роботи з Kubernetes, можливо щось цікаве про реактивність і все, з чим встигну попрацювати більш ніж поверхнево. Якщо це буде комусь цікаво, звісно :)
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
22 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів