Spring Boot
na GraalVM
ie? Brzmi ciekawie, ale czy to jest możliwe?
Według wpisu sprzed 2 miesięcy prosty projekt można postawić na GraalVM Native Image
. Jednak prosty CRUD, to nie system produkcyjny, który wymaga różnych bibliotek. Pytanie, jak to wygląda z nieco bardziej skomplikowanym stackiem technologicznym?
JHipster
Nie będę oczywiście pisał wszystkiego od zera. W zeszłym roku napisałem szybką aplikację (i wpis o niej). W niej wykorzystałem generator kodu JHipster. Taki generator to jest dobry dla szybkich, krótkich projektów, gdyż oprócz wygenerowanego modelu dostarcza wielu standardowych wymagań takich jak uwierzytelnienie, autoryzacja, metryki, obsługa logowania, a nawet stworzenie obrazu Docker
owego. Warto spojrzeć na ten projekt oraz co nim się da wygenerować również po to, żeby poznać technologie, zaczerpnąć ewentualnie patternów zaimplementowanych w takim projekcie.
Posiadając już apkę z Spring Boot
em w wersji 2.2.7
postanowiłem zastosować w nim Spring Boot Native
. Pierwszym wymogiem jest podbicie wersji Spring Boot
a do 2.4.5
. Oczywiście podbicie wersji Spring Boota
pociąga za sobą podbicie innych wersji bibliotek używanych przez Spring
a. Wchodząc do tego piekła ciężko z niego wyjść. Zatem od razu sobie odpuściłem manualne podbijanie, gdy w kolejnej wersji JHipstera (w wersji 7.0) podbicie Spring Boot
a do wersji 2.4.x jest zrobione out-of-the-box.
Zatem przeszedłem do ponownego wygenerowania na podstawie metadanych JHipstera
. Rzecz jasna po tym należało zmerge’ować wygenerowany kod z moimi zmianami. Rzecz jasna to też nie było przyjemnie, ale wiązało się z mniejszym ryzykiem, niż manualne podbicie wersji.
Spring Boot + Spring Boot Native
Po podbiciu Spring Boota można było wprowadzić zmiany wymagane przez Spring Boot Native. Po uaktualnieniu pom.xml
można było spróbować pierwszy raz uruchomić proces budowania. Pierwszy raz budowanie zakończyło się porażką na etapie budowania Maven
a z komunikatem podobnym do Nie udało się, bo się nie wysypało. Pierwszym rozwiązaniem okazało się dodanie do parametrów kompliacji Maven
a paramteru <forceJavacCompilerUse>true</forceJavacCompilerUse>
. Po drugiej próbie kompilacji komunikat już był precyzyjny. Tym razem problem był po mojej stronie – źle zmerge’owałem źródła.
Trzecia kompilacja zakończyła się wielkim sukcesem – po 15 minutach mielenia na moim starym lapku projekt skompilował się! Uruchomienie przyniosło jednak szybkie rozczarowanie – logback
nie może dostać się do pliku z konfiguracją. Po kilku próbach (czyt. kilkukrotnym czekaniu po 15 minut) i przeczesaniu internetu na temat tego błędu, zmigrowałem konfigurację na taką, która bazuje na przekazanych propertiesach.
Czwarta (15 minut) kompilacja zakończyła się porażką – uruchomienie aplikacji natywnej zakończyła się problemem z Audytowaniem uwierzytelniania. Nie jest to kluczowa funkcjonalność tego projektu, zatem szybkie zakomentowanie @EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
rozwiązało problem.
Piąta (15 minut) kompilacja zakończyła się kolejną porażkę. Tym razem okazało się, że Swagger
to zbyt duże wyzwanie dla Spring Native
. Rozwiązanie – zakomentowanie uruchamiania profilu słagerowego.
Szósta (15 minut) kompilacja zakończyła się znów porażką. Tym razem kontekst Springa nie wstał. Po prostu. Bez informacji, co nie działa, gdzie i o co chodzi. Tutaj się poddałem.
Teoretycznie jest jakiś ticket na dostosowanie JHipstera
do Spring Native
, a nawet można dostać 500$ za dostarczenie tego feature’u. Można podejrzewać, że dostosowanie do Spring Native
ostatecznie zostanie wykonane. Ale to póki co, to pieśń przyszłości.
Inne rozwiązania
JHipster
jest już dość dojrzałym projektem – na frontendzie oferuje nie tylko Angular
a, ale również React
a i Vue.js
, za to na backednzie wsparcie obejmuje Spring Boot
a, a także Micronauta i Quarkusa. Co ciekawe język programowania wymienić na Kotlin
a (ze Spring Boot
em), C#
(.Net
), czy Javascript
(NestJS
).
W przypadku Micronauta niestety wsparcie nie obejmuje kompliacji do Native Image
. Szkoda, bo Micronaut
ma API bardzo podobne do Spring Boot
a.
Został JHipster z Quarkusem. Tutorial tłumaczący, jak stworzyć wygenerować taką aplikację jest dosadnie prosty. Wsparcie obejmuje również Native Image
. Zatem do dzieła.
JHipster + Quarkus
Ponownie musiałem przegenerować na podstawie metadanych JHipster
a. Niestety API Quarkus
a różni się zdecydowanie od Spring Boot
a.
Począwszy od bazy danych i JPA
różni się podejście do „owrapowania JPA
„. W Spring Boot
mamy bardzo przyjazne Spring Data
– Quarkus
oferuje nam za to Panache, który jest frameworkiem dedykowanym dla Quarkus
em. W nim dominującym podejściem do pobierania i zapisywania krotek jest ActiveRecord, choć da się również stworzyć klasy Repository. Jest wsparcie dla paginacji, customowych projekcji, zapytań w JPQL
. Jedyne, czego brakuje, to odpowiednik uproszczenia JPA
Criteria Query.
Jak mówiłem, podejściem dominującym jest Active Record, to w modelowaniu klas – zamiast klas POJO z getterami i setterami – dominuje klasa z publicznymi polami bez getterów i setterów dziedzicząca po PanacheEntity
. Tutaj również musiałem dostosować swoje customowe zmiany.
Wsparcie dla cache’ów jest, ale w moim przypadku nie było potrzebne (i nie działało ;)), zatem je wyłączyłem.
Wsparcie dla security contextu jest, jednak dostęp nie jest przez ThreadLocal
a zwanego SecurityContextHolder
em, a przez dodatkowy parametr wywołania endpointa.
Dodatkowo Quarkus
wymusza poznawanie innych frameworków (czyt. RedHatowych, Eclipse’owych). I tak zamiast Jackson
a mamy JSONB
, zamiast Springa MVC
mamy Jax-Rs
. Wstrzykiwanie zależności wykonywane jest przez @Inject
, choć Spring
również wspiera tę adnotację.
Ostatnie, o czym trzeba pamiętać, to żeby do DTOsów i innych obiektów obrabianych refleksją (encje bazodanowe, projekcje) dodać adnotację @RegisterForReflection
.
Wynik
Quarkus
jest stworzony dla GraalVM Native Image
, więc w tym przypadku nie było żadnego problemu z kompilacją do kodu natywnego. Sam czas kompilacji na mojej maszynie, to około 10 minut, czyli o 1/3 szybciej, niż kompilacja Spring Boot Native
.
Normalnie na OpenJDK
aplikacja wstaje lokalnie w około 5 sekund. Po skompilowaniu do kodu natywnego, uruchomienie aplikacji trwa do 100 milisekund.
Drugą kwestią jest, że taką aplikację można postawić niewielkim Heapie – dałem -Dxmx=64M
, co łącznie z OffHeapem łącznie daje 200 MB (tyle przynajmniej pokazuje htop). Obstawiam, że gdyby zrezygnować z wielu JHipster
owych feature’ów, to dało by się zejść z zużyciem pamięci jeszcze niżej.Spring Boot
a to z mniejszym Heapem niż 128 to bym nawet nie próbował startować, a jak wiadomo OffHeap też swoje waży. Jakkolwiek istnieją ludzie, którzy ze Spring Boot
em potrafią zejść poniżej 80MB mierząc wszystko…
Podsumowanie
Należy pamiętać, że Spring Native
jest jeszcze w fazie beta, zatem nie wszystko może tam działać. Możliwe, że tworzenie serwera od początku do końca było by lepszą strategią pozwalającą na znalezienie wszystkich przyczyn błędów.
Jednak każdorazowa rekompilacja trwająca 15 minut skutecznie odrzuca. Jako programista Java jestem przyzwyczajony do kompilacji klasy do bytecodu mierzonej w maksymalnie sekundach. Nawet kompilacja z użyciem Maven
a takiego projektu trwa maksymalnie minutę.
Trzeba poczekać na pierwszy release, wtedy będzie można cokolwiek powiedzieć. Póki co jest zbyt wcześnie…