Quarkus: Сверхзвуковая субатомная ветклиника
Это вольный пересказ моего Lightning Talk с конференции Joker 2019. С тех пор вышло несколько новых версий Quarkus, доклад приведен в соответствие с текущим положением вещей.
В рамках разработки нашего фреймворка CUBA Platform, мы уделяем много внимания тому, что происходит в индустрии. Хотя фреймворк CUBA выстроен вокруг Spring, нельзя игнорировать то, что происходит вокруг. И, наверняка, все заметили, что в последнее время очень много шума вокруг cloud-native фреймворков. Два новичка — Micronaut и Quarkus достаточно уверенно начинают вступать на территорию Spring Boot. В один прекрасный день было решено сделать небольшое RnD, по результатам которого я расскажу об опыте работы с фреймворком Quarkus на примере хорошо известного приложения — Petclinic.
Quarkus?
Короткий обзор фреймворка Quarkus был в этой статье. Вкратце: вместо того, чтобы писать свой фреймворк с нуля, разработчики RedHat взяли готовые библиотеки (Hibernate ORM, Eclipse Vert.x, Netty, RESTEasy) и собрали их в единый пакет, а поверх этой сборки сделали совместимый с Java стандартами API. Quarkus соответствует спецификации Eclipse Microprofile 3.2. Кроме того, фреймворк построен так, чтобы минимизировать использование рефлексии при старте приложения, что дает выигрыш по времени старта, использованию памяти и дает возможность использовать AOT компиляцию с GraalVM.
Также в последних версиях Quarkus добавили движок-шаблонизатор для создания UI — Qute. На момент RnD он не был доступен (и, если честно, я плохо понимаю, зачем он нужен), а в нашем проекте использовался React.
Так что там с ветклиникой?
Petclinic — приложение-пример, написанное на Spring Framework. Все началось с версии на Spring, а сейчас ветклинику делают все, кому не лень и в разных вариантах: на классическом Spring, на Spring Boot, пишут код на Kotlin, добавляют GraphQL API, в общем, развлекаются как могут. Ветклиника — это такой полигон, на котором можно обкатывать новые фреймворки. Было решено сделать ветклинику в «классическом» варианте, без использования микросервисов: написать серверную часть с REST API на Quarkus, а клиентскую часть использовать готовую, на ReactJS.
Front-end берем готовый, структуру классов также было решено частично взять из приложения, написанного на чистом Spring. Что ещё нам нужно от фреймворка для создания приложения? Удобный API для написания REST сервисов и работы с БД. Хорошо, если будет dependency injection. В Spring приложениях вы можете использовать весь арсенал библиотек, доступных для JVM. А что с этим у Quarkus?
Здесь есть некоторые тонкости. Quarkus приложения могут работать как «обычные» JVM приложения, в таком случае вы можете использовать практически любые библиотеки, как и в случае использования Spring. Но если вы хотите поддерживать Graal VM Native Image, который работает, исходя из предположения, что программа должна сразу знать о себе все (Closed World Assumption), то список возможных библиотек сильно ограничивается, поскольку многие из них интенсивно используют динамические классы и прочие рефлексивные штуки. И не со всем из этого GraalVM хорошо дружит.
Сделанные специально для Quarkus библиотеки называются расширениями и при сборке приложения они обрабатываются дополнительно для совместимости с AOT. Для этого введена специальная фаза сборки — build augmentation. На текущий момент список расширений для Quarkus довольно обширен, но многим он все ещё может показаться недостаточным. Что хорошо — для разработки Petclinic всё нужное есть: Hibernate, RESTEasy, а также ArC для dependency injection.
Несмотря на то, что в Quarkus есть два вида библиотек, на управление зависимостями это никак не влияет — вы просто прописываете координаты расширения в дескриптор проекта.
io.quarkus
quarkus-hibernate-orm-panache
io.quarkus
quarkus-junit5
test
com.google.code.gson
gson
2.8.5
test
Давайте посмотрим на код уже!
Модель данных
Здесь нет никаких новостей, используем Hibernate. Мы взяли готовую структуру классов из проекта petclinic для Spring, там используется Hibernate в качестве ORM. Нелишним будет напомнить, что RedHat является одним из основных спонсоров Hibernate, так что было бы странно не видеть этот ORM в составе Quarkus.
С данными в Quarkus можно работать несколькими способами.
Можно заинжектить EntityManager и использовать его для работы с сущностями:
public class ClinicService {
@Inject
EntityManager em;
public List findAllPetTypes() {
return em.createNamedQuery("PetTypes.findAll", PetType.class).getResultList();
}
public PetType findPetTypeById(Integer petTypeId) {
return em.find(PetType.class, petTypeId);
}
//и т.д.
}
А можно использовать расширение Panache. Если унаследовать свои сущности от PanacheEntity
(мы это сделали для BaseEntity
), то в каждом классе сущности появятся методы для работы с базой данных (find, list, delete, persist и т.д.). Предыдущий код работы с данными, но с использованием Panache:
public class ClinicService {
public List findAllPetTypes() {
return PetType.listAll();
}
public PetType findPetTypeById(Long petTypeId) {
return PetType.findById(petTypeId);
}
//и т.д.
}
Кроме того, Panache поддерживает репозитории, можно использовать их, такой подход может быть более привычным.
Я не могу выделить явные преимущества или недостатки того или иного подхода, но лично мне кажется, что работа с Panache чуть проще.
Сервисы
Пришлось вспомнить одну аннотацию @ApplicationScoped
, которой я и пометил сервис ClinicService
. Для транзакций используется всем известная аннотация @Transactional
. Все. Дальше пишем обычный код сервиса, как все мы привыкли. Выбираем данные из базы, обрабатываем, укладываем обратно.
@ApplicationScoped
@Transactional
public class ClinicService {
public List findAllPetTypes() {
return PetType.listAll();
}
public Collection 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 имеет некоторые ограничения. Так что, если вам важно использование бинов @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 контейнер, есть подробная и не очень длинная инструкция, как это делать.
И что там со быстродействием?
Petclinic на Quarkus стартует быстро, примерно в 2 раза быстрее, чем UberJar «эталонного» приложения Petclinic, написанного на 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 компиляцию при помощи GraalVM Native Image. Это то, что делает Micronaut и то, что сейчас делают в Spring. В 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, методика есть на сайте 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, которая умеет брать пользователей и роли из текстовых файлов. Все прошло довольно гладко, документация у 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
имя пользователя и пароль кэшируются на клиенте в браузере и передаются при каждом вызове 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();
}
}
}
Тут я нашел свой первый баг в Quarkus. CORS в сочетании с Security давал интересный эффект. Неправильная пара пользователь-пароль отбрасывалась сразу и до CORS фильтра дело не доходило. В результате, нужные заголовки в ответ не добавлялись. А, если нет CORS заголовков, то браузер даже не допускал ответ сервера до React клиента. После долгой переписки баг все-таки исправили и React клиент ожил.
Имитация Spring
Для Quarkus существуют расширения, которые добавляют поддержку Spring аннотаций и некоторых API. В принципе, это логично. Компилятору должно быть все равно, написан ли там Inject или Autowired. С API посложнее, конечно, но, в принципе, тоже реализуемо. Конечно, не все поддерживается, но можно сделать код немного привычнее, если вы переходите на Spring. Вот что получилось.
JPA репозитории ровно такие же, как в Spring.
@Repository
public interface OwnerRepository extends CrudRepository {
Collection findOwnersByLastNameIsLike(String lastNameString);
}
Пока добавлял репозитории, пришлось временно убрать наследование сущностей, потому что проявился ещё один баг. Но его починили без долгой переписки, как часть большого исправления.
Сервис в Quarkus может быть вообще неотличим от спрингового.
@Service
@Transactional
public class ClinicService {
@Autowired
OwnerRepository ownerRepository;
public Collection 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 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.
Важное замечание: даже сами разработчики из RedHat не рекомендуют переводить на Quarkus то, что у вас сейчас отлично работает на Spring или другом фреймворке. Начинайте на Quarkus новую разработку, не трогайте существующие сервисы.
Так что, если вы планируете приложение с сервисной архитектурой и вам реально важно быстро поднимать новые экземпляры backend сервисов — потестируйте Quarkus. Он и так быстр, а в сочетании с AOT компиляцией — вообще круто получается.
Ещё, есть смысл рассмотреть Quarkus, если расходы на Spring приложение в облаке становятся ощутимыми из-за потребления памяти (и вы уверены, что это из-за Spring). Тогда, возможно, затраты на переписывание приложения со Spring на Quarkus будут скомпенсированы меньшими расходами на облачную инфраструктуру в дальнейшем.
А вот для внутренних корпоративных приложений, где не всегда важен быстрый старт сервисов, можно обойтись и более традиционными фреймворками типа Spring Boot или CUBA Platform.
Если интересно посмотреть на субатомную ветклинику — она на GitHub. Замечания и дополнения приветствуются.