Může Micronaut nahradit Spring Boot? Podívejme se na příklad.

Ahoj, jmenuji se Ivan Kozikov, jsem fullstack Java developer v NIX United. Mám certifikace Oracle a Kubernetes a rád zkoumám nové technologie a učím se nová témata v oblasti Javy.

Zdroj JRebel každý rok provádí průzkum mezi vývojáři Java o tom, jaké frameworky používají. V roce 2020 vyhrál Spring Boot s 83 %. V roce 2021 však jeho podíl klesl na 62 %. Jedním z těch, kteří více než zdvojnásobili svou přítomnost na trhu, byl Micronaut. Rychlý růst popularity tohoto rámce vyvolává logickou otázku:co je na něm zajímavého? Rozhodl jsem se zjistit, jaké problémy Micronaut překonává, a pochopit, zda se může stát alternativou k Spring Boot.

V tomto článku projdu historií softwarové architektury, která pomůže pochopit, proč takové frameworky vznikly a jaké problémy řeší. Zdůrazním hlavní rysy Micronautu a porovnám dvě aplikace s identickými technologiemi:jednu na tomto frameworku a druhou na Spring Boot.

Od monolitů k mikroslužbám a dále…

Moderní vývoj softwaru začal monolitickou architekturou. V něm je aplikace obsluhována prostřednictvím jediného nasaditelného souboru. Pokud se bavíme o Javě, jedná se o jeden JAR soubor, který skrývá veškerou logiku a obchodní procesy aplikace. Tento soubor JAR pak přenesete kamkoli jej potřebujete.

Tato architektura má své výhody. Za prvé, je velmi snadné začít s vývojem produktu. Vytvoříte jeden projekt a naplníte jej obchodní logikou, aniž byste přemýšleli o komunikaci mezi různými moduly. Na začátku také potřebujete velmi málo zdrojů a je snazší provádět integrační testování pro celou aplikaci.

Tato architektura má však i nevýhody. Aplikace na monolitické architektuře téměř vždy přerostly takzvanou „velkou vrstvu bahna“. Komponenty aplikace se propojily natolik, že bylo obtížné je udržovat, a čím větší je produkt, tím více zdrojů a úsilí by bylo zapotřebí ke změně čehokoli v projektu.

Proto ji nahradila architektura mikroslužeb. Rozděluje aplikaci na malé služby a vytváří samostatné soubory nasazení v závislosti na obchodních procesech. Nenechte se však svést slovem „mikro“ – odkazuje na obchodní možnosti služby, nikoli na její velikost.

Obvykle jsou mikroslužby zaměřeny na jednotlivé procesy a jejich podporu. To poskytuje několik výhod. Za prvé, protože se jedná o samostatné nezávislé aplikace, můžete potřebnou technologii přizpůsobit konkrétnímu obchodnímu procesu. Za druhé, je mnohem snazší sestavit a vypořádat se s projektem.

Existují však i nevýhody. Nejprve se musíte zamyslet nad vztahem mezi službami a jejich kanály. Mikroslužby také vyžadují více zdrojů pro údržbu své infrastruktury než v případě monolitu. A když přejdete do cloudu, je tento problém ještě kritičtější, protože musíte platit za spotřebu zdrojů cloudové infrastruktury ze svých aplikací.

Jaký je rozdíl mezi frameworky a mikrorámci?
Pro urychlení vývoje softwaru se začaly vytvářet frameworky. Historicky byl modelem pro mnoho vývojářů Java Spring Boot. Postupem času však jeho obliba klesala a to lze vysvětlit. Spring Boot v průběhu let získal poměrně velkou „váhu“, což mu brání pracovat rychle a využívat méně zdrojů, jak to vyžaduje moderní vývoj softwaru v cloudovém prostředí. Proto jej začaly nahrazovat mikrorámce.

Microframeworks jsou poměrně novým typem rámce, jehož cílem je maximalizovat rychlost vývoje webových služeb. Obvykle mají většinu funkcí omezenou – na rozdíl od řešení s plným zásobníkem, jako je Spring Boot. Velmi často jim například chybí autentizace a autorizace, abstrakce pro přístup k databázi, webové šablony pro mapování na komponenty uživatelského rozhraní atd. Micronaut začínal stejným způsobem, ale tuto fázi již přerostl. Dnes má vše, co z něj dělá full stack framework.

Hlavní výhody Micronautu

Autoři tohoto frameworku se inspirovali Spring Bootem, ale kladli důraz na minimální použití reflexních a proxy tříd, což urychluje jeho práci. Micronaut je vícejazyčný a podporuje Java, Groovy a Kotlin.

Mezi hlavní výhody Micronautu vyzdvihuji následující:

  • Abstrakce pro přístup ke všem oblíbeným databázím. Micronaut má hotová řešení pro práci s databázemi. Poskytují také API pro vytváření vlastních tříd a metod pro přístup k databázím. Navíc podporují obě varianty:normální blokování přístupu a reaktivní přístup.

  • Aspekt-orientované API. Ve Spring Boot můžete rychle vyvíjet software díky anotacím. Ale tyto instrukce jsou postaveny na reflexi a vytváření proxy tříd při provádění programu. Micronaut poskytuje sadu instrukcí připravených k použití. Jeho nástroje můžete použít k psaní vlastních anotací, které využívají reflexi pouze v době kompilace, nikoli za běhu. To urychluje spouštění aplikace a zlepšuje její výkon.

  • Nativně integrovaná práce s cloudovými prostředími. Budeme o tom mluvit podrobně dále a důležité body prozradím samostatně.

  • Vestavěná sada testovacích nástrojů. Ty vám umožňují rychle vyvolat klienty a servery, které potřebujete pro testování integrace. Můžete také použít známé knihovny JUnit a Mockito.

Co nám dává kompilace na plný úvazek?

Již jsem poukázal na to, že Micronaut nepoužívá reflexní a proxy třídy – to je možné prostřednictvím kompilace předem. Před spuštěním aplikace v době vytváření balíčku se Micronaut snaží komplexně vyřešit všechny injekce závislostí a kompilovat třídy tak, aby to nebylo nutné, když je samotná aplikace spuštěna.

Dnes existují dva hlavní přístupy ke kompilaci:právě včas (JOT) a předem (AOT). Kompilace JIT má několik hlavních výhod. První je velká rychlost budování artefaktu, souboru JAR. Nepotřebuje kompilovat další třídy – dělá to pouze za běhu. Je také jednodušší načíst třídy za běhu; s AOT-kompilací to musí být provedeno ručně.

Při kompilaci AOT je však doba spouštění kratší, protože vše, co aplikace ke svému běhu potřebuje, se zkompiluje ještě před jejím spuštěním. S tímto přístupem bude velikost artefaktu menší, protože neexistují žádné proxy třídy, přes které by se pak spouštěly kompilace. Na druhou stranu, tato kompilace vyžaduje méně zdrojů.

Je důležité zdůraznit, že Micronaut má vestavěnou podporu pro GraalVM. Toto je téma na samostatný článek, takže se zde nebudu hlouběji zabývat. Dovolte mi říci jednu věc:GraalVM je virtuální stroj pro různé programovací jazyky. Umožňuje vytváření spustitelných obrazových souborů, které lze spouštět v kontejnerech. Tam je rychlost spuštění a běhu aplikace maximální.

Když jsem se to však pokusil použít v Micronautu, i když jsem se řídil připomínkami tvůrce frameworku, při vytváření nativního obrazu jsem musel určit klíčové třídy aplikace, protože budou předkompilovány za běhu. Proto by tato otázka měla být pečlivě prozkoumána ve srovnání s inzerovanými sliby.

Jak Micronaut pracuje s cloudovou technologií

Samostatně by měla být zveřejněna nativní podpora cloudových technologií. Zdůrazním čtyři hlavní body:

  • Micronaut zásadně podporuje kordónování. Když pracujeme s cloudovými prostředími, zvláště když existuje více dodavatelů, musíme vytvořit komponenty speciálně pro infrastrukturu, ve které budeme aplikaci používat. K tomu nám Micronaut umožňuje vytvářet podmíněné komponenty, které závisí na určitých podmínkách. To poskytuje sadu konfigurací pro různá prostředí a snaží se maximalizovat definici prostředí, ve kterém běží. To značně zjednodušuje práci vývojáře.

  • Micronaut má vnořené nástroje k určení služeb potřebných ke spuštění aplikace. I když nezná skutečnou adresu služby, pokusí se ji najít. Proto existuje několik možností:můžete použít vestavěné nebo přídavné moduly (např. Consul, Eureka nebo Zookeeper).

  • Micronaut má schopnost vytvořit nástroj pro vyrovnávání zátěže na straně klienta. Je možné regulovat zatížení replik aplikací na straně klienta, což vývojářům usnadňuje život.

  • Micronaut podporuje architekturu bez serveru. Opakovaně jsem se setkal s tím, že vývojáři říkali:"Nikdy nebudu psát funkce lambda v Javě." V Micronautu máme dvě možnosti zápisu lambda-funkcí. První je využití API, které je přímo dané infrastrukturou. Druhým je definovat řadiče jako v běžném REST API a následně je používat v rámci této infrastruktury. Micronaut podporuje AWS, Azure a Google Cloud Platform.

Někdo může namítnout, že tohle všechno je dostupné i ve Spring Boot. Ale připojení cloudové podpory je možné pouze díky dalším knihovnám nebo cizím modulům, zatímco v Micronautu je vše zabudováno nativně.

Pojďme porovnat aplikace Micronaut a Spring Boot

Pojďme k zábavnější části! Mám dvě aplikace — jednu napsanou v Spring Boot, druhou v Micronautu. Jedná se o tzv. uživatelskou službu, která má sadu operací CRUD pro práci s uživateli. Máme databázi PostgreSQL připojenou přes reaktivní ovladač, zprostředkovatele zpráv Kafka a WEB Sockets. Máme také HTTP klienta pro komunikaci se službami třetích stran, abychom získali více informací o našich uživatelích.

Proč taková aplikace? Často se v prezentacích o Micronautu předávají metriky v podobě aplikací Hello World, kde nejsou připojeny žádné knihovny a v reálném světě nic není. Jak to funguje, chci ukázat na příkladu podobném praktickému použití.

Chci poukázat na to, jak snadné je přejít z Spring Boot na Micronaut. Náš projekt je docela standardní:máme klienta třetí strany pro HTTP, REST řadič pro zpracování obchodů, služeb, úložiště atd. Když se podíváme do řadiče, vidíme, že po Spring Bootu je vše snadno pochopitelné. Anotace jsou velmi podobné. Nemělo by být těžké se to všechno naučit. Dokonce i většina instrukcí, jako je PathVariable, je k Spring Bootu jedna k jedné.

@Controller("api/v1/users")
public class UserController {
  @Inject
  private UserService userService;

  @Post
  public Mono<MutableHttpResponse<UserDto>> insertUser(@Body Mono<UserDto> userDtoMono) {
      return userService.createUser(userDtoMono)
          .map(HttpResponse::ok)
          .doOnError(error -> HttpResponse.badRequest(error.getMessage()));
  }

Totéž platí pro službu. Pokud bychom měli napsat anotaci služby v Spring Boot, zde máme anotaci Singleton, která definuje rozsah, na který se vztahuje. Existuje také podobný mechanismus pro vkládání závislostí. Stejně jako ve Spring Bootu je lze použít pomocí konstruktorů nebo vytvořit pomocí parametrů vlastností nebo metod. V mém příkladu je obchodní logika napsána tak, aby naše třída fungovala:

@Controller("api/v1/users")
public class UserController {
  @Inject
  private UserService userService;

  @Post
  public Mono<MutableHttpResponse<UserDto>> insertUser(@Body Mono<UserDto> userDtoMono) {
      return userService.createUser(userDtoMono)
          .map(HttpResponse::ok)
          .doOnError(error -> HttpResponse.badRequest(error.getMessage()));
  }

  @Get
  public Flux<UserDto> getUsers() {
    return userService.getAllUsers();
  }

  @Get("{userId}")
  public Mono<MutableHttpResponse<UserDto>> findById(@PathVariable long userId) {
    return userService.findById(userId)
        .map(HttpResponse::ok)
        .defaultIfEmpty(HttpResponse.notFound());
  }

  @Put
  public Mono<MutableHttpResponse<UserDto>> updateUser(@Body Mono<UserDto> userDto) {
    return userService.updateUser(userDto)
        .map(HttpResponse::ok)
        .switchIfEmpty(Mono.just(HttpResponse.notFound()));
  }

  @Delete("{userId}")
  public Mono<MutableHttpResponse<Long>> deleteUser(@PathVariable Long userId) {
    return userService.deleteUser(userId)
        .map(HttpResponse::ok)
        .onErrorReturn(HttpResponse.notFound());
  }

  @Get("{name}/hello")
  public Mono<String> sayHello(@PathVariable String name) {
    return userService.sayHello(name);
  }

Úložiště má také známý vzhled po Spring Boot. Jediná věc je, že v obou aplikacích používám reaktivní přístup.

@Inject
private UserRepository userRepository;

@Inject
private UserProxyClient userProxyClient;

Osobně se mi velmi líbil HTTP klient pro komunikaci s ostatními službami. Můžete to napsat deklarativně jen tím, že definujete rozhraní a specifikujete, jaké typy metod to bude, jaké hodnoty Query budou předány, jaké části adresy URL to budou a jaké tělo to bude. Vše je rychlé a navíc si můžete vytvořit vlastního klienta. Opět to lze provést pomocí knihoven třetích stran v rámci Spring Boot s reflexními a proxy třídami.

@R2dbcRepository(dialect = Dialect.POSTGRES)
public interface UserRepository extends ReactiveStreamsCrudRepository<User, Long> {
  Mono<User> findByEmail(String email);

  @Override
  @Executable
  Mono<User> save(@Valid @NotNull User entity);
}
@Client("${placeholder.baseUrl}/${placeholder.usersFragment}")
public interface UserProxyClient {

  @Get
  Flux<ExternalUserDto> getUserDetailsByEmail(@NotNull @QueryValue("email") String email);

  @Get("/{userId}")
  Mono<ExternalUserDto> getUserDetailsById(@PathVariable String userId);

}

Nyní přejdeme přímo k práci v terminálu. Mám otevřená dvě okna. Na levé straně na žlutém pozadí je Spring Boot a na pravé straně na šedém pozadí je Micronaut. Udělal jsem sestavení obou balíčků — ve Spring Boot to trvalo téměř 5 sekund, zatímco Micronautu to trvalo déle kvůli kompilaci AOT; v našem případě proces trval téměř dvakrát tak dlouho.

Dále jsem porovnal velikost artefaktu. Soubor JAR pro Spring Boot má 40 MB a pro Micronaut 38 MB. Ne o mnoho méně, ale stále méně.

Poté jsem provedl test rychlosti spouštění aplikace. Ve Spring Boot Netty se server spustil na portu 8081 a trval 4,74 sekund. Ale v Micronautu máme 1,5 sekundy. Podle mě docela podstatná výhoda.

Dalším krokem je velmi zajímavý test. Mám skript Node.js, jehož cesta přechází do souboru JAR jako argument. Spustí aplikaci a každou půl sekundu se snaží získat data z adresy URL, kterou jsem jí napsal — tedy našich uživatelů. Tento skript se ukončí, když obdrží první odpověď. Ve Spring Boot skončil za 6,1 sekundy a v Micronautu za 2,9 sekundy – opět dvakrát rychleji. Metriky zároveň ukazují, že Spring Boot se spustil za 4,5 sekundy a výsledek se dostavil za 1,5 sekundy. Pro Micronaut jsou tato čísla asi 1,5 a 1,3 sekundy. To znamená, že zisk je dosažen přesně díky rychlejšímu startu aplikace a prakticky by Spring Boot mohl odpovídat stejně rychle, kdyby na začátku neprováděl další kompilaci.

Další test:spusťte aplikace (spuštění trvá 4,4 sekundy a 1,3 sekundy ve prospěch Micronautu) a uvidíme, kolik paměti využívají oba frameworky. Používám jcmd — předám identifikátor procesu a získám heap_info. Metriky ukazují, že celkem aplikace Spring Boot vyžadovala ke spuštění 149 MB a ve skutečnosti spotřebovala 63 MB. Totéž opakujeme pro Micronaut, se stejným příkazem, ale se změnou ID procesu. Výsledek:aplikace požádala o 55 MB a použila 26 MB. To znamená, že rozdíl ve zdrojích je 2,5–3krát.

Zakončím další metrikou, abych ukázal, že Micronaut není stříbrná kulka a má kam růst. S ApacheBench jsem simuloval 500 požadavků na Spring server pro Spring Boot se souběžností pro 24 požadavků. To znamená, že simulujeme situaci, kdy 24 uživatelů současně odesílá požadavky do aplikace. S reaktivní databází ukazuje Spring Boot docela dobrý výsledek:může předat asi 500 požadavků za sekundu. Koneckonců, JIT kompilace funguje dobře na systémových špičkách. Postup zkopírujeme do Micronautu a párkrát zopakujeme. Výsledkem je asi 106 požadavků za sekundu. Zkontroloval jsem čísla na různých systémech a strojích a byly přibližně stejné, ať už dáš nebo vezmeš.

Závěr je jednoduchý

Micronaut není ideál, který může okamžitě nahradit Spring Boot. Stále má některé body, které jsou pohodlnější nebo funkčnější v prvním rámci. V některých oblastech je však populárnější produkt horší než méně populární, ale poměrně pokročilý konkurent. To znamená, že Spring Boot má také cestu. Například stejná kompilace AOT volitelně existuje v Javě od verze 9 v roce 2017.

Rád bych přidal ještě jednu myšlenku:vývojáři by se neměli bát zkoušet nové technologie. Mohou nám poskytnout skvělé příležitosti a umožnit nám jít nad rámec standardních rámců, se kterými obvykle pracujeme.