Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Spring, Micronaut czy Quarkus?

Jarosław Słaby Software Engineer / EPAM Systems Poland
Uzupełnij swoją wiedzę dotyczącą webowych frameworków Javy, czyli Springa, Micronauta i Quarkusa.
Spring, Micronaut czy Quarkus?

Java jest popularna, a co za tym idzie, szeroko stosowana, jednak nie można jej stosować w każdej sytuacji. Całkiem nieźle radzi sobie jednak w świecie aplikacji webowych. Posiada również sporą ilość frameworków do realizowania różnych operacji. Jednym z takich frameworków jest Spring firmy Pivotal – służący m.in. do szybkiego tworzenia aplikacji webowych. Dostarczając mechanizmów obsługi servletów, czy wstrzykiwania zależności do komponentów aplikacji, przyspiesza przygotowanie środowiska dla aplikacji, pozwalając skupić się na logice biznesowej.

Pomimo swoich zalet Spring nie jest najlepszym wyborem, jeśli chodzi o tworzenie architektury opartej o mikroserwisy. Pomimo faktu, iż istnieje mnóstwo tutoriali poświęconych temu zagadnieniu, istnieją inne frameworki, które w przypadku mikroserwisów sprawdzą się dużo lepiej. Są to m.in. Micronaut oraz Quarkus.


Micronaut

Za powstanie frameworka Micronaut odpowiedzialny jest zespół twórców Gralis (frameworka webowego dla języka Groovy). Opublikowany został w 2018 roku jako lepsza, lżejsza alternatywa dla Springa do tworzenia aplikacji serverless (dedykowanych środowiskom uruchomieniowym takim, jak np. AWS lambda). Wspiera język Java, Kotlin oraz Groovy. Micronaut zmniejsza zapotrzebowanie aplikacji na pamięć oraz inne zasoby systemowe. Dodatkowo posiada którszy czas uruchomienia niż np. Spring, ponieważ korzysta z mechanizmu wstrzykiwania zależności w czasie kompilacji. 

Instalacja

Aby zainstalować Micronauta, wchodzimy na stronę, a następnie, w zależności od posiadanego przez nas systemu operacyjnego, wybieramy odpowiedni sposób instalacji. Dla systemu Windows należy pobrać archiwum z plikami binarnymi frameworka (Binary):

Następnie pobrane archiwum wypakowujemy do dowolnej, wybranej przez siebie lokalizacji. Kolejnym krokiem jest dodanie folderu do ścieżki PATH systemu operacyjnego. Ustawiamy następujące zmienne:

  1. MICRONAUT_HOME=ścieżka/do/katalogu/instalacyjnego/frameworka
  2. PATH=%PATH%;%MICRONAUT_HOME%/bin


Ustawienie takie powoduje dostęp z każdego miejsca systemu do głównego polecenia Micronauta: mn. Polecenie mn służy m.in. do stworzenia nowego projektu. Posiada ono tryb interaktywny umożliwiający tworzenie aplikacji, profili, sprawdzanie wersji frameworka itp.

Aby stworzyć podstawową aplikację Micronauta, wpisujemy polecenie:

mn create-app <app-name>


Należy pamiętać o tym, aby, przed jego wykonaniem, przejść do odpowiedniego katalogu, ponieważ projekt zostanie wygenerowany w bieżącym folderze. Domyślnie generowany jest projekt w języku Java, który jako narzędzie do budowy wykorzystuje Gradle. Jeśli chcemy wygenerować projekt Mavena, dodatkowo musimy podać parametr -b (lub --build) maven. Podobnie jest w przypadku zmiany domyślnego języka z Javy np. na Kotlin. Wtedy do naszego polecenia dodajemy parametr -l (--lang) kotlin. Na potrzeby tego artykułu pozostaniemy jednak przy domyślnych ustawieniach, tj. języku Java i Gradle.


Quarkus

Quarkus został stworzony w 2018 roku przez firmę RedHat. Powstał z myślą o wykrozystaniu go do natywnej kompilacji na potrzeby GraalVM, ale może również współpracować z klasycznym JVM. Dedykowany jest, podobnie jak Micronaut, do tworzenia mikroserwisów cechujacych się krótkim czasem uruchamiania i niskim zużyciem pamięci (również aplikacji serverless). Przy pomocy Quarkusa możemy tworzyć aplikacje w Javie oraz w Kotlinie.

Instalacja

Podobnie, jak w przypadku Springa, Quarkus udostępnia możliwość wygenerowania gotowego projektu. Nie ma potrzeby instalowania żadnego dodatkowego oprogramowania ani narzędzi konsoli. Wchodzimy na stronę, a następnie dobieramy odpowiednie parametry, tj. narzędzie do budowy projektu, nazwę grupy i artefaktu projektu oraz jego wersję. Dodatkowo z listy na dole strony możemy wybrać zależności potrzebne nam w projekcie. Po wygenerowaniu projektu rozpakowujemy go i importujemy do naszego IDE. Aby uruchomić nasz projekt, wpisujemy polecenie mvn compile quarkus:dev, które uruchamia naszą aplikację w trybie developerskim. Tryb ten umożliwia przeładowywanie zmian w kodzie bez restartu aplikacji.

Szybki start

Aby porównać w/w frameworki stworzymy prostą aplikację – obsługującą tworzenie i pobieranie pracowników firmy ABC Inc. Dla uproszczenia kontenerem do przechowywania danych będą struktury danych Javy w pamięci, zrezygnujemy z połączenia z bazą danych (o tym w następnym artykule). Zbudujemy ją przy pomocy wszystkich trzech technologii i porównamy wygląd kodu.

Spring

Po zaimportowaniu projektu Springa do IDE (w przypadku tego tutorialu będzie to IntelliJ IDEA) naszym oczom ukazuje się następująca struktura projektu (projekt Maven):

Przystąpmy więc do tworzenia naszej prostej aplikacji. Stwórzmy zatem kontroler służący do dodawania pracownika oraz pobierania listy pracowników.

Klasa EmployeeController.java:

package pl.js.springtest.employees;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Set;

@RestController
public class EmployeeController {

  private final EmployeeService employeeService;

  EmployeeController(EmployeeService employeeService) {
    this.employeeService = employeeService;
  }

  @GetMapping(value = "/employees", produces = MediaType.APPLICATION_JSON_VALUE)
  ResponseEntity<Set<Employee>> getEmployees() {
    return new ResponseEntity<>(employeeService.getAllEmployees(), HttpStatus.OK);
  }

  @PostMapping(value = "/employees", produces = MediaType.APPLICATION_JSON_VALUE)
  ResponseEntity<Employee> addEmployee(@RequestBody Employee employee) {
    return new ResponseEntity<>(employeeService.addEmployee(employee), HttpStatus.OK);
  }
}


Widać tutaj dwie metody: getEmployees() służącą do pobrania z serwisu listy pracowników i zwrócenia odpowiedniej odpowiedzi HTTP oraz addEmployee() – dodającą pracownika do listy. Serwis wygląda następująco:

Klasa EmployeeService.java:

package pl.js.springtest.employees;

import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Service
class EmployeeService {

  private Set<Employee> employees = new HashSet<>();

  Set<Employee> getAllEmployees() {
    return Collections.unmodifiableSet(employees);
  }

  Employee addEmployee(Employee employee) {
    employees.add(employee);
    return employee;
  }
}


Jako kontener danych wykorzystujemy tutaj zwykły HashSet ze standardowej biblioteki Javy. Model, który jest wykorzystywany w naszej aplikacji, wygląda następująco (taki sam model zostanie wykorzystany w przypadku wszystkich trzech frameworków):

Klasa Employee.java:

package pl.js.springtest.employees;

import java.math.BigDecimal;
import java.util.Objects;

class Employee {
  private String firstName;
  private String lastName;
  private BigDecimal salary;

  public Employee(String firstName, String lastName, BigDecimal salary) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.salary = salary;
  }

  public String getFirstName() {
    return firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public BigDecimal getSalary() {
    return salary;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Employee employee = (Employee) o;
    return Objects.equals(firstName, employee.firstName) &&
        Objects.equals(lastName, employee.lastName) &&
        Objects.equals(salary, employee.salary);
  }

  @Override
  public int hashCode() {
    return Objects.hash(firstName, lastName, salary);
  }
}


Ze względu na fakt wykorzystania struktury danych, jaką jest HashSet, koniecznym było odpowiednie przeciążenie metod equals() i hashCode().

Micronaut

Po wygenerowaniu projektu i zaimportowaniu do IDE widzimy następującą strukturę:

Na pierwszy rzut oka widać kilka różnic (oprócz oczywistej – w przypadku projektu Micronauta wykorzystamy Gradle). Jedną z nich jest obecność pliku Dockerfile. Domyślna zawartość tego pliku umożliwia skopiowanie wszystkich artefaktów aplikacji do kontenera, ekspozycję portu 8080 jako domyślnego dla aplikacji oraz uruchomienie pliku .jar. Kolejną różnicą jest to, że Micronaut domyślnie przechowuje swoje właściwości (properties) w pliku application.yml (Spring również ma taką możliwość, ale domyślnie wykorzystuje plik application.properties).

Stwórzmy więc analogiczną aplikację w frameworku Micronaut. Zacznijmy więc od kontrolera:

Klasa EmployeeController.java:

package micronaut.test.employees;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;

import java.util.Set;

@Controller
class EmployeeController {

  private final EmployeeService employeeService;

  EmployeeController(EmployeeService employeeService) {
    this.employeeService = employeeService;
  }

  @Get(value = "/employees", produces = MediaType.APPLICATION_JSON)
  HttpResponse<Set<Employee>> getEmployees() {
    return HttpResponse.ok(employeeService.getAllEmployees());
  }

  @Post(value = "/employees", produces = MediaType.APPLICATION_JSON)
  HttpResponse<Employee> addEmployee(@Body Employee employee) {
    return HttpResponse.ok(employeeService.addEmployee(employee));
  }
}


Choć zasada działania kontrolera jest taka sama, widać tu kilka zasadniczych różnic. Po pierwsze cała klasa jest adnotowana za pomocą @Controller zamiast @RestController jak w przypadku Springa. Zamiast adnotacji @GetMapping mamy @Get, zamiast @PostMapping - @Post. Różnica występuje również w tworzeniu odpowiedzi. Odpowiednikiem klasy ResponseEntity ze Springa jest HttpResponse – wyposażona w zestaw statycznych metod fabrycznych do tworzenia odpowiedzi o określonych kodach. Różnicę możemy również zauważyć w warstwie serwisu.

Klasa EmployeeService.java:

package micronaut.test.employees;

import javax.inject.Singleton;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Singleton
class EmployeeService {

  private Set<Employee> employees = new HashSet<>();

  Set<Employee> getAllEmployees() {
    return Collections.unmodifiableSet(employees);
  }

  Employee addEmployee(Employee employee) {
    employees.add(employee);
    return employee;
  }
}


Spring posiada adnotację @Service umożliwiającą ustawienie klasy jako serwis. Micronaut nie posiada analogicznej adnotacji, dlatego w przykładzie wykorzystano adnotację @Singleton. Adnotacja
ta umożliwia kontekstowi Micronauta stworzenie obiektu klasy i wstrzyknięcie go do kontrolera. Jednocześnie określa ona zakres beana (scope) – w przypadku Springa posiadamy adnotację @Proxy. Tutaj zakres definiujemy niejako od razu. Warto również wspomnieć o tym, że w przypadku Springa domyślnym zakresem beana jest singleton. W Micronaucie jest to proxy, oznaczające konieczność stworzenia nowej instancji przy każdym zapotrzebowaniu na obiekt danej klasy.

Quarkus

Zaimportujmy wygenerowany wcześniej projekt Quarkusa to IntelliJ. Struktura takiego projektu wygląda następująco:

Podobnie jak w przypadku Micronauta mamy tutaj dostęp do predefiniowanych plików Dockerfile, z których jeden służy do stworzenia klasycznej aplikacji Javy i uruchomienie jej wewnątrz kontenera
w klasycznej maszynie wirtualnej JVM. Drugi natomiast (Dockerfile.native) umożliwia stworzenie kontenera bez JVM-a i uruchomienie tzw. „native image” – pliku wykonywalnego aplikacji skompilowanego do natywnego kodu systemowego (zainteresowanych odsyłam do dokumentacji GraalVM). Dodatkowo w projekcie istnieje już klasa ExampleResource zawierająca przykładowy kontroler. 

Stwórzmy zatem analogiczną aplikację, jak w przypadku Micronauta i Springa. 

Klasa EmployeeController.java:

package pl.js.employees;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/employees")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class EmployeeController {

  final EmployeeService employeeService;

  public EmployeeController(EmployeeService employeeService) {
    this.employeeService = employeeService;
  }

  @GET
  public Response getEmployees() {
    return Response.ok(employeeService.getAllEmployees()).build();
  }

  @POST
  public Response addEmployee(Employee employee) {
    return Response.ok(employeeService.addEmployee(employee)).build();
  }
}


Główne różnice, jakie możemy dostrzec to brak adnotacji @Controller, niegeneryczną klasę Response, zawierającą budowniczego do tworzenia odpowiedzi HTTP, brak adnotacji @Body przy parametrze metody dodającej użytkownika oraz adnotację @ApplicationScoped tworzącą bean w kontekście aplikacji Quarkusa. Klasa serwisu wygląda następująco:

Klasa EmployeeService.java:

package pl.js.employees;

import javax.enterprise.context.ApplicationScoped;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@ApplicationScoped
class EmployeeService {

  private Set<Employee> employees = new HashSet<>();

  Set<Employee> getAllEmployees() {
    return Collections.unmodifiableSet(employees);
  }

  Employee addEmployee(Employee employee) {
    employees.add(employee);
    return employee;
  }
}


Podsumowanie

Wszystkie trzy frameworki działają w podobny sposób, rozpoczęcie pracy z każdym z nich jest proste i nie powinno sprawić trudności zarówno laikowi, jak i bardziej zaawansowanemu programiście. Istnieje jednak kilka znaczących różnic, które sprawiają, że zarówno Micronaut, jak i Quarkus wydają się lepsze w zastosowaniach mikroserwisowych. Przede wszystkim jest to wstrzykiwanie zależności na etapie kompilacji, co znacząco skraca czas uruchamiania aplikacji. Drugą ważną zaletą jest możliwość skompilowania kodu aplikacji napisanej przy pomocy Micronauta i Quarkusa to kodu natywnego systemu operacyjnego, co znacząco podnosi wydajność takiej aplikacji i zmniejsza jej zapotrzebowanie na pamięć. Jednakże są to stosunkowo nowe technologie, które nie posiadają tak prężnie rozwijającej się społeczności, jak w przypadku Springa. Warto jednak je rozważyć w przypadku, gdy rozważamy architekturę mikroserwisów.

Dokumentacja do Quarkusa
Dokumentacja do Micronauta

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 160 tysiącami naszych czytelników

Dowiedz się więcej