- PVSM.RU - https://www.pvsm.ru -
Это вольный пересказ моего Lightning Talk с конференции Joker 2019. С тех пор вышло несколько новых версий Quarkus, доклад приведен в соответствие с текущим положением вещей.
В рамках разработки нашего фреймворка CUBA Platform [1], мы уделяем много внимания тому, что происходит в индустрии. Хотя фреймворк CUBA выстроен вокруг Spring, нельзя игнорировать то, что происходит вокруг. И, наверняка, все заметили, что в последнее время очень много шума вокруг cloud-native [2] фреймворков. Два новичка — Micronaut [3] и Quarkus [4] достаточно уверенно начинают вступать на территорию Spring Boot. В один прекрасный день было решено сделать небольшое RnD, по результатам которого я расскажу об опыте работы с фреймворком Quarkus на примере хорошо известного приложения – Petclinic [5].
Короткий обзор фреймворка 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 клиент ожил.
Для 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
Нажмите здесь для печати.