- PVSM.RU - https://www.pvsm.ru -

Quarkus: Сверхзвуковая субатомная ветклиника

Quarkus: Сверхзвуковая субатомная ветклиника - 1

Это вольный пересказ моего Lightning Talk с конференции Joker 2019. С тех пор вышло несколько новых версий Quarkus, доклад приведен в соответствие с текущим положением вещей.

В рамках разработки нашего фреймворка CUBA Platform [1], мы уделяем много внимания тому, что происходит в индустрии. Хотя фреймворк CUBA выстроен вокруг Spring, нельзя игнорировать то, что происходит вокруг. И, наверняка, все заметили, что в последнее время очень много шума вокруг cloud-native [2] фреймворков. Два новичка — Micronaut [3] и Quarkus [4] достаточно уверенно начинают вступать на территорию Spring Boot. В один прекрасный день было решено сделать небольшое RnD, по результатам которого я расскажу об опыте работы с фреймворком Quarkus на примере хорошо известного приложения – Petclinic [5].

Quarkus?

Короткий обзор фреймворка Quarkus был в этой статье [6]. Вкратце: вместо того, чтобы писать свой фреймворк с нуля, разработчики RedHat взяли готовые библиотеки (Hibernate ORM, Eclipse Vert.x, Netty, RESTEasy) и собрали их в единый пакет, а поверх этой сборки сделали совместимый с Java стандартами API. Quarkus соответствует спецификации [7] Eclipse Microprofile 3.2. Кроме того, фреймворк построен так, чтобы минимизировать использование рефлексии при старте приложения, что дает выигрыш по времени старта, использованию памяти и дает возможность использовать AOT компиляцию с GraalVM.
Также в последних версиях Quarkus добавили движок-шаблонизатор для создания UI — Qute [8]. На момент RnD он не был доступен (и, если честно, я плохо понимаю, зачем он нужен), а в нашем проекте использовался React.

Так что там с ветклиникой?

Petclinic – приложение-пример, написанное на Spring Framework. Все началось с версии на Spring, а сейчас ветклинику делают все, кому не лень и в разных вариантах: на классическом Spring [9], на Spring Boot [10], пишут код на Kotlin [11], добавляют GraphQL API [12], в общем, развлекаются как могут [13]. Ветклиника — это такой полигон, на котором можно обкатывать новые фреймворки. Было решено сделать ветклинику в "классическом" варианте, без использования микросервисов: написать серверную часть с REST API на Quarkus, а клиентскую часть использовать готовую, на ReactJS.

Front-end берем готовый [14], структуру классов также было решено частично взять из приложения, написанного на чистом Spring. Что ещё нам нужно от фреймворка для создания приложения? Удобный API для написания REST сервисов и работы с БД. Хорошо, если будет dependency injection. В Spring приложениях вы можете использовать весь арсенал библиотек, доступных для JVM. А что с этим у Quarkus?

Здесь есть некоторые тонкости. Quarkus приложения могут работать как "обычные" JVM приложения, в таком случае вы можете использовать практически любые библиотеки, как и в случае использования Spring. Но если вы хотите поддерживать Graal VM Native Image, который работает, исходя из предположения, что программа должна сразу знать о себе все (Closed World Assumption), то список возможных библиотек сильно ограничивается, поскольку многие из них интенсивно используют динамические классы и прочие рефлексивные штуки. И не со всем из этого GraalVM хорошо дружит.

Сделанные специально для Quarkus библиотеки называются расширениями и при сборке приложения они обрабатываются дополнительно для совместимости с AOT. Для этого введена специальная фаза сборки – build augmentation. На текущий момент список расширений [15] для Quarkus довольно обширен, но многим он все ещё может показаться недостаточным. Что хорошо — для разработки Petclinic всё нужное есть: Hibernate [16], RESTEasy [17], а также ArC [18] для dependency injection.

Несмотря на то, что в Quarkus есть два вида библиотек, на управление зависимостями это никак не влияет – вы просто прописываете координаты расширения в дескриптор проекта.

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-hibernate-orm-panache</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
            <scope>test</scope>
        </dependency>

Давайте посмотрим на код уже!

Модель данных

Здесь нет никаких новостей, используем Hibernate. Мы взяли готовую структуру классов [19] из проекта petclinic для Spring, там используется Hibernate в качестве ORM. Нелишним будет напомнить, что RedHat является одним из основных спонсоров Hibernate, так что было бы странно не видеть этот ORM в составе Quarkus.

С данными в Quarkus можно работать несколькими способами.

Можно заинжектить EntityManager и использовать его для работы с сущностями:

public class ClinicService {

    @Inject
    EntityManager em;

    public List<PetType> findAllPetTypes() {
        return em.createNamedQuery("PetTypes.findAll", PetType.class).getResultList();
    }

    public PetType findPetTypeById(Integer petTypeId) {
        return em.find(PetType.class, petTypeId);
    }
//и т.д.
}

А можно использовать расширение Panache [20]. Если унаследовать свои сущности от PanacheEntity (мы это сделали для BaseEntity), то в каждом классе сущности появятся методы для работы с базой данных (find, list, delete, persist и т.д.). Предыдущий код работы с данными, но с использованием Panache:

public class ClinicService {

    public List<PetType> findAllPetTypes() {
        return PetType.listAll();
    }

    public PetType findPetTypeById(Long petTypeId) {
        return PetType.findById(petTypeId);
    }
//и т.д.
}

Кроме того, Panache поддерживает репозитории [21], можно использовать их, такой подход может быть более привычным.

Я не могу выделить явные преимущества или недостатки того или иного подхода, но лично мне кажется, что работа с Panache чуть проще.

Сервисы

Пришлось вспомнить одну аннотацию @ApplicationScoped, которой я и пометил сервис ClinicService. Для транзакций используется всем известная аннотация @Transactional. Все. Дальше пишем обычный код сервиса, как все мы привыкли. Выбираем данные из базы, обрабатываем, укладываем обратно.

@ApplicationScoped
@Transactional
public class ClinicService {

    public List<PetType> findAllPetTypes() {
        return PetType.listAll();
    }

    public Collection<Vet> findAllVets() {
        return Vet.findAll(Sort.ascending("firstName")).list();
    }

    public Pet updatePet(long petId, Pet pet) {
        Pet currentPet = findPetById(petId);
        if (currentPet == null) {
            return null;
        }
        currentPet.setBirthDate(pet.getBirthDate());
        currentPet.setName(pet.getName());
        currentPet.setType(pet.getType());
        return currentPet;
    }
//и т.д.
}

CDI в Quarkus имеет некоторые ограничения [22]. Так что, если вам важно использование бинов @ConversationScoped, то, возможно, Quarkus не для вас. А в остальном — все довольно привычно. Используйте @Inject для инъекции других сервисов, @Singleton — для создания синглтон бинов, @RequestScoped @SessionScoped — для обозначения соответствующего жизненного цикла. Все эти аннотации — часть спецификации CDI, с которой почти все разработчики имели дело. Если не имели — то выучить их не сложно.

Контроллеры

Для создания REST API контроллеров пришлось немного почитать документацию по JAX-RS, чтобы провести аналогию с классами и аннотациями Spring. Взять готовые Spring контроллеры не получилось, пришлось переписать код на JAX-RS, но, в целом – совсем ничего сложного, если вы умеете делать REST контроллеры.
Получилось как-то так. Код простой и ничего Quarkus-специфичного в нем нет.

@Path("/api")
@Produces(MediaTypes.APPLICATION_JSON_UTF8)
@Consumes(MediaTypes.APPLICATION_JSON_UTF8)
public class OwnersResource {

    @Inject
    ClinicService clinicService;

    @POST
    @Path("/owner")
    public Response addOwner(@Valid Owner owner) {
        owner = clinicService.saveOwner(owner);
        URI uri = URI.create(String.format("/api/owner/%s", owner.getId()));
        return Response.ok(owner).location(uri).build();
    }

    @GET
    @Path("/owner/{ownerId}")
    public Response getOwner(@PathParam("ownerId") long ownerId) {
        Owner owner = clinicService.findOwnerById(ownerId);
        if (owner == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
        return Response.ok(owner).build();
    }
//и т.д.
}

А как тестировать код?

Для тестирования используются RESTAssured и JUnit, есть опция тестирования кода, скомпилированного в нативный исполняемый файл. Здесь тоже никаких новостей.

@QuarkusTest
public class PetTypesResourceTest {

    @Test
    public void testListAllPetTypes() {
        given()
                .when()
                .get("api/pettypes")
                .then()
                .statusCode(200)
                .body(
                        containsString("cat"),
                        containsString("dog"),
                        containsString("lizard"),
                        containsString("snake"),
                        containsString("bird"),
                        containsString("hamster")
                );
    }
//и т.д.

Запускаем!

У нас есть контроллеры, сервисы и сущности – все нужное для запуска приложения. Что характерно – специальный файл-запускатель Aplication.java в Quarkus не нужен, при сборке фреймворк его сам сделает.

При разработке приложение обычно запускается в development mode, когда можно редактировать код и видеть изменения без перезапуска приложения:

./mvnw quarkus:dev

Если нужно запускать Uber Jar, то следует указать свойство quarkus.package.uber-jar=true в файле application.properties, а потом собрать проект командой

./mvnw package

После чего можно запустить файл с суффиксом -runner в каталоге target. В моем случае это был quarkus-petclinic-rest-1.0-SNAPSHOT-runner.jar. Здесь в примерах используется maven, но Quarkus поддерживает и Gradle для сборки проекта.

Естественно, можно упаковать приложение в Docker контейнер, есть подробная и не очень длинная инструкция [23], как это делать.

И что там со быстродействием?

Petclinic на Quarkus стартует быстро, примерно в 2 раза быстрее, чем UberJar «эталонного» приложения Petclinic [10], написанного на Spring Boot. Для тестов использовалась база H2, in-memory. Hibernate не проверял и не обновлял схему БД при старте приложения. Итоги ниже.

Spring Boot Uber Jar:

17:39:13 INFO 26780 --- [main] o.s.s.petclinic.PetClinicApplication     : Started PetClinicApplication in 14.09 seconds (JVM running for 14.917)

Quarkus Uber Jar:

17:37:26 INFO [io.quarkus]] (main) quarkus-petclinic-rest 1.0-SNAPSHOT (running on Quarkus 1.2.1.Final) started in 6.377s. Listening on: http://0.0.0.0:8080

Двукратное увеличение скорости запуска приложения — это неплохо, но давайте рассмотрим ещё одну вещь: AOT компиляцию [24] при помощи GraalVM Native Image. Это то, что делает Micronaut и то, что сейчас делают в Spring [25]. В Quarkus компиляция в нативный код – это не набор заклинаний, а просто название профиля при запуске в maven.

./mvnw package -Pnative.

Нативный код стартует ультрабыстро — это занимает десятые доли секунды!

15:53:18 INFO [io.quarkus]] (main) quarkus-petclinic-rest 1.0-SNAPSHOT (running on Quarkus 1.2.1.Final) started in 0.045s. Listening on: http://0.0.0.0:8080

Расход памяти я мерил в Linux утилитой pmap, методика [26] есть на сайте Quarkus. Обратите внимание на цифру RSS – resident set size, это актуальная цифра расхода памяти.
Spring Boot Uber Jar:

7910:   java -jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000       4       4       0 r-x-- java
...
ffffffffff600000       4       0       0 r-x--   [ anon ]
---------------- ------- -------- ------- 
total kB         5071540  971,740  934364

Quarkus Uber Jar:

7795:   java -jar quarkus-petclinic-rest-1.0-SNAPSHOT-runner.jar
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000       4       4       0 r-x-- java
...
ffffffffff600000       4       0       0 r-x--   [ anon ]
---------------- ------- -------- ------- 
total kB         5006732  523,648  483288

А это – расход памяти, если мы скомпилируем приложение в исполняемый файл при помощи GraalVM. Потребление памяти меньше примерно в 20 раз в сравнении со Spring Boot приложением.

3737:   ./quarkus-petclinic-rest-1.0-SNAPSHOT-runner
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000      12      12       0 r---- quarkus-petclinic-runner
...
ffffffffff600000       4       0       0 r-x--   [ anon ]
---------------- ------- -------- ------- 
total kB          675500   50,272  11668

Промежуточные итоги

Стандартная ветклиника получилась без проблем. Писать на Quarkus можно без головной боли, ничего раздражающего для себя я не увидел, обычный фреймворк с приятными плюшками вроде dev mode. Все как-то сразу получилось, даже неинтересно. Никаких плагинов для IDEA ставить не пришлось, тесты запускаются, приложение запускается. Отладка работает через remote port.

Developer Mode – хорошо сделан. Просто запускаете приложение и начинаете править код. Quarkus сам перекомпилирует и перезапустит приложение, если были сделаны изменения. Получается цикл "код — обновление страницы — анализ вывода — код". На больших и сложных приложениях это, очевидно, будет работать медленно, но Quarkus изначально сделан для разработки микросервисов, поэтому разработчики выбрали полный перезапуск приложения вместо перегрузки отдельных классов. Небольшой минус — необходимо переподключаться к debug порту, если приложение стартует заново.

Добавляем безопасности

Чтобы добавить веселья в стандартную ветклинику, было решено дополнительно включить простейший ролевой доступ к API, с использованием библиотеки Elytron [27], которая умеет брать пользователей и роли из текстовых файлов. Все прошло довольно гладко, документация у Quarkus неплохая. Подключаем extension, делаем два файла: с пользователями и с ролями.
Файл users.properties. Ключ — имя пользователя, значение — пароль. Все записано в plain text, но можно вставить шифрование, если необходимо.

admin=admin
vet=vet

Файл roles.properties. Ключ — пользователь, значение — роли.

admin=ROLE_OWNER_ADMIN, ROLE_VET_ADMIN, ROLE_ADMIN
vet=ROLE_VET_ADMIN

Делаем строковые константы, чтобы проще рефакторить код.

public class Roles {
    public static final String OWNER_ADMIN = "ROLE_OWNER_ADMIN";
    public static final String VET_ADMIN = "ROLE_VET_ADMIN";
    public static final String ADMIN = "ROLE_ADMIN";
}

Добавляем аннотацию к методу, прописываем роли. Собственно, все.

public class VetsResource {

    @GET
    @Path("/vets")
    @RolesAllowed(Roles.VET_ADMIN)
    public Response getAllSpecialties() {
    //вызов сервиса и формирование ответа
    }
}

Для React клиента был сделан отдельный API вызов, который проверяет правильность имени и пароля. При ответе ACCEPTED base64 строка аутентификации кэшируется на клиенте в браузере и передается при каждом вызове API.

@Path("api/auth")
@PermitAll
@Produces(MediaTypes.APPLICATION_JSON_UTF8)
@Consumes(MediaTypes.APPLICATION_JSON_UTF8)
public class AuthResource {

    @POST
    public Response authorize(@Context SecurityContext sc) {
        Principal userPrincipal = sc.getUserPrincipal();
        if (userPrincipal != null) {
            return Response.status(Response.Status.ACCEPTED).entity(userPrincipal).build();
        } else {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }
    }
}

Тут я нашел свой первый баг [28] в Quarkus. CORS в сочетании с Security давал интересный эффект. Неправильная пара пользователь-пароль отбрасывалась сразу и до CORS фильтра дело не доходило. В результате, нужные заголовки в ответ не добавлялись. А, если нет CORS заголовков, то браузер даже не допускал ответ сервера до React клиента. После долгой переписки баг все-таки исправили и React клиент ожил.

Имитация Spring

Для Quarkus существуют расширения, которые добавляют поддержку Spring аннотаций и некоторых API. В принципе, это логично. Компилятору должно быть все равно, написан ли там Inject [29] или Autowired [30]. С API посложнее, конечно, но, в принципе, тоже реализуемо. Конечно, не все поддерживается, но можно сделать код немного привычнее, если вы переходите на Spring. Вот что получилось.

JPA репозитории ровно такие же, как в Spring.

@Repository
public interface OwnerRepository extends CrudRepository<Owner, Integer> {
    Collection<Owner> findOwnersByLastNameIsLike(String lastNameString);
}

Пока добавлял репозитории, пришлось временно убрать наследование сущностей, потому что проявился ещё один баг [31]. Но его починили без долгой переписки, как часть большого исправления.

Сервис в Quarkus может быть вообще неотличим от спрингового.

@Service
@Transactional
public class ClinicService {

    @Autowired
    OwnerRepository ownerRepository;

    public Collection<Owner> findOwnerByLastName(String ownerLastName) {
        return ownerRepository.findOwnersByLastNameIsLike(ownerLastName+"%");
    }
//…
}

Да и контроллер тоже.

@RestController
@RequestMapping(path = "/api", 
        consumes = MediaTypes.APPLICATION_JSON_UTF8, 
        produces = MediaTypes.APPLICATION_JSON_UTF8)
public class OwnersResource {

    @Autowired
    ClinicService clinicService;

    @GetMapping("/owner/{ownerId}")
    @RolesAllowed({Roles.OWNER_ADMIN, Roles.VET_ADMIN})
    public ResponseEntity<Owner> getOwner(@PathVariable("ownerId") int ownerId) {
        Owner owner = null;
        owner = clinicService.findOwnerById(ownerId);
        if (owner == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        return ResponseEntity.ok(owner);
    }
//...
}

Заметьте, что можно смешивать аннотации Quarkus и Spring. В итоге, все заработало нормально, как и должно было. Разработчики из RedHat делают все, чтобы стать заметным игроком на рынке Java фреймворков.

Заключение

С Quarkus приятно работать, RedHat в него сейчас активно вкладывается, появилось огромное количество материалов и докладов на конференциях. Можно смело начинать эксперименты с фреймворком, стабильные версии уже вышли. Один из последних примеров использования Quarkus на боевом проекте — Lufthansa [32].

Важное замечание: даже сами разработчики из RedHat не рекомендуют переводить на Quarkus то, что у вас сейчас отлично работает на Spring или другом фреймворке. Начинайте на Quarkus новую разработку, не трогайте существующие сервисы.

Так что, если вы планируете приложение с сервисной архитектурой и вам реально важно быстро поднимать новые экземпляры backend сервисов – потестируйте Quarkus. Он и так быстр, а в сочетании с AOT компиляцией – вообще круто получается.

Ещё, есть смысл рассмотреть Quarkus, если расходы на Spring приложение в облаке становятся ощутимыми из-за потребления памяти (и вы уверены, что это из-за Spring). Тогда, возможно, затраты на переписывание приложения со Spring на Quarkus будут скомпенсированы меньшими расходами на облачную инфраструктуру в дальнейшем.

А вот для внутренних корпоративных приложений, где не всегда важен быстрый старт сервисов, можно обойтись и более традиционными фреймворками типа Spring Boot или CUBA Platform.

Если интересно посмотреть на субатомную ветклинику – она на GitHub [33]. Замечания и дополнения приветствуются.

Автор: Андрей Беляев

Источник [34]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/347814

Ссылки в тексте:

[1] CUBA Platform: https://www.cuba-platform.com/

[2] cloud-native: https://www.redhat.com/en/topics/cloud-native-apps

[3] Micronaut: https://micronaut.io

[4] Quarkus: https://quarkus.io/

[5] Petclinic: https://github.com/spring-petclinic

[6] этой статье: https://habr.com/ru/company/haulmont/blog/443242/

[7] спецификации: https://quarkus.io/blog/quarkus-eclipse-microprofile-3-2-compatible/

[8] Qute: https://quarkus.io/guides/qute-reference

[9] на классическом Spring: https://github.com/spring-petclinic/spring-framework-petclinic

[10] на Spring Boot: https://github.com/spring-projects/spring-petclinic

[11] пишут код на Kotlin: https://github.com/spring-petclinic/spring-petclinic-kotlin

[12] добавляют GraphQL API: https://github.com/spring-petclinic/spring-petclinic-graphql

[13] развлекаются как могут: https://github.com/spring-petclinic/spring-petclinic-microservices

[14] готовый: https://github.com/spring-petclinic/spring-petclinic-reactjs

[15] список расширений: https://quarkus.io/extensions/

[16] Hibernate: https://quarkus.io/extensions/#data

[17] RESTEasy: https://quarkus.io/extensions/#web

[18] ArC: https://quarkus.io/guides/cdi-reference

[19] готовую структуру классов: https://github.com/spring-petclinic/spring-framework-petclinic/tree/master/src/main/java/org/springframework/samples/petclinic/model

[20] Panache: https://quarkus.io/guides/hibernate-orm-panache-guide

[21] репозитории: https://quarkus.io/guides/hibernate-orm-panache#the-daorepository-option

[22] некоторые ограничения: https://quarkus.io/guides/cdi-reference#limitations

[23] инструкция: https://quarkus.io/guides/building-native-image

[24] AOT компиляцию: https://quarkus.io/guides/maven-tooling#building-a-native-executable

[25] делают в Spring: https://github.com/spring-projects/spring-framework/wiki/GraalVM-native-image-support

[26] методика: https://quarkus.io/guides/performance-measure

[27] библиотеки Elytron: https://quarkus.io/guides/security-properties

[28] баг: https://github.com/quarkusio/quarkus/issues/3685

[29] Inject: https://habr.com/ru/users/inject/

[30] Autowired: https://habr.com/ru/users/autowired/

[31] баг: https://github.com/quarkusio/quarkus/issues/5261

[32] Lufthansa: https://quarkus.io/blog/aviatar-experiences-significant-savings/

[33] GitHub: https://github.com/belyaev-andrey/quarkus-petclinic-reactjs

[34] Источник: https://habr.com/ru/post/487588/?utm_source=habrahabr&utm_medium=rss&utm_campaign=487588