Twoja pierwsza aplikacja Quarkus
Pisanie pierwszej aplikacji w Quarkusie może być wyzwaniem, ponieważ zapoznanie się z nowymi bibliotekami, a tym samym zmiana nawyków, może sprawić, że poczujesz się zagubiony i bezradny. Przejdźmy przez to razem i uczyńmy to doświadczenie tak przyjemnym i ekscytującym, jak powinno być!
Pierwsze kroki
Podobnie jak Spring Initilzr, Quarkus oferuje stronę, na której można przygotować i pobrać swój projekt: https://code.quarkus.io/. Na potrzeby tego artykułu spróbujmy przygotować część backendową aplikacji do rezerwacji parkingu o nazwie Valet:
Po wybraniu bibliotek aplikacji można wygenerować plik zip projektu, pobrać go i otworzyć za pomocą ulubionego IDE. Pobrana aplikacja jest gotowa do zbudowania i uruchomienia w trybie deweloperskim.
Tworzenie zasobów
Nasza aplikacja będzie udostępniać miejsce parkingowe z możliwością rezerwacji, a następnie zwolnienia. Przygotujmy taki zasób z jego funkcjonalnościami CRUD:
package pl.jlabs.blog.valet;
import {...}
@Path("/spots")
@Consumes("application/json")
@Produces("application/json")
public class ParkingSpotResource {
@Inject
ParkingSpotService parkingSpotService;
@POST
public Response create(@Context UriInfo uriInfo, ParkingSpotDto spot) {
parkingSpotService.create(spot);
return Response.created(URI.create(uriInfo.getPath() + "/" + spot.getId())).build();
}
@GET
public ParkingSpotsDto getAll() {
return parkingSpotService.getAll();
}
@GET
@Path("{id}")
public ParkingSpotDto get(@PathParam("id") Long id) {
return parkingSpotService.get(id);
}
@PUT
@Path("{id}")
public Response update(@PathParam("id") Long id, ParkingSpotDto spot) {
parkingSpotService.update(id, spot);
return Response.accepted(spot).build();
}
@DELETE
@Path("{id}")
public Response delete(@PathParam("id") Long id) {
parkingSpotService.delete(id);
return Response.noContent().build();
}
}
Adnotacja @Path definiuje ścieżkę do zasobu i znajdujących się w nim endpoint’ów. @Consumes & @Produces definiuje typ zawartości konsumowanej i produkowanej przez endpointy. @POST, @GET, @PUT i @DELETE definiują metody dostępne wewnątrz zasobu. Wszystko to standardowe adnotacje JAX-RS używane do definiowania sposobu dostępu do usługi, a Quarkus jest w stanie odpowiednio je obsługiwać. Uruchomienie aplikacji z tą klasą wewnątrz eksponuje nowy endpoint REST „/spots” z dołączonymi do niego metodami CRUD HTTP. Podane endpointy są również automatycznie dodawane do listy zasobów Quarkus dostępnej na stronie docelowej serwera:
Testowanie REST endpoints
Aby testowanie punktów końcowych przebiegało tak sprawnie jak każda inna część narzędzia Quarkus, dostępna jest adnotacja, która kontroluje framework testowy. Jest to rozszerzenie JUnit, które uruchamia aplikację pod spodem i obsługuje jej cykl życia podczas testowania. Szkielet aplikacji zawiera już przykładowe testy, które wymagają jedynie dodania logiki biznesowej:
package pl.jlabs.blog.valet;
import {...}
@QuarkusTest
@TestMethodOrder(OrderAnnotation.class)
public class ParkingSpotResourceTest {
@Test
@Order(1)
public void create_shouldReturn201AndLocation() {
RestAssured
.given()
.contentType("application/json")
.body(spot())
.when()
.post("/spots")
.then()
.statusCode(HttpStatus.SC_CREATED)
.header("Location", endsWith("/spots/1"));
}
@Test
@Order(2)
public void get_shouldReturn200AndSomeBody() {
RestAssured
.when()
.get("/spots/1")
.then()
.statusCode(200)
.body(notNullValue());
}
@Test
@Order(3)
public void update_shouldReturn202AndSomeBody() {
RestAssured
.given()
.contentType("application/json")
.body(unavailableSpot())
.when()
.put("/spots/1")
.then()
.statusCode(HttpStatus.SC_ACCEPTED)
.body(notNullValue());
}
@Test
@Order(4)
public void getAll_shouldReturn200AndSomeBody() {
RestAssured
.when()
.get("/spots")
.then()
.statusCode(200)
.body(notNullValue());
}
@Test
@Order(5)
public void delete_shouldReturn204() {
RestAssured
.when()
.delete("/spots/1")
.then()
.statusCode(HttpStatus.SC_NO_CONTENT)
.body(notNullValue());
}
String spot() {
var spot = new JsonObject();
spot.put("id", "1");
spot.put("isAvailable", "true");
return spot.encode();
}
}
Zespół Quarkus zdecydowanie sugeruje korzystanie z REST Assured jako najwygodniejszego sposobu testowania punktów końcowych HTTP.
Przechowywanie danych za pomocą frameworka Panache
Bardzo ważnym aspektem pracy z Quarkus jest uproszczony dostęp do przechowywania danych. Z biegiem lat Hibernate ORM obrósł szeregiem irytujących rzeczy, z którymi użytkownicy muszą sobie radzić. Przykładami są między innymi rozdzielenie definicji danych (modelu) od ich operacji (co jest antytezą architektury zorientowanej obiektowo) lub nadmierna rozdęcie w przypadku typowych operacji i nie nadawanie się do prostego dostępu do danych (podczas gdy korzystanie ze zwykłego JDBC może wydawać się prymitywne).
Problemy te zostały rozwiązane dzięki Panache. Encja może rozszerzyć klasę PanacheEntity i zostać przekształcona w aktywny rekord. Model obiektowy i jego zachowanie mogą być teraz przechowywane razem, tak jak wszyscy jesteśmy przyzwyczajeni i nie ma potrzeby tworzenia DAO lub repozytoriów.
Aby rozpocząć pracę z Panache, wystarczy dodać zależność do pliku pom/gradle wraz z wybranym sterownikiem jdbc:
<!-- Hibernate ORM specific dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<!-- JDBC driver dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
# skonfiguruj źródło danych
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = valet_user
quarkus.datasource.password = valet_password
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/valet
# usuń i utwórz bazę danych podczas uruchamiania (użyj `update`, aby tylko zaktualizować schemat)
quarkus.hibernate-orm.database.generation = drop-and-create
Oto przykład tego, jak może wyglądać jednostka Parking Spot Entity z poprzedniego przykładu:
package pl.jlabs.blog.valet.data;
import {...}
@Entity
public class ParkingSpotEntity extends PanacheEntity {
@Nonnull
public Boolean available;
public static List<ParkingSpotEntity> findAllAvailable() {
return list("available", true);
}
}
Mając tak zdefiniowaną encję możemy już wykonywać wiele operacji na zbiorze danych Parking Spots: persist(), listAll(), count(), findById(id), deleteById(id) i wiele innych. Dodatkowo, jak widać, tworzenie prostych zapytań opartych na jednym lub wielu polach encji jest również bardzo proste. Identyfikator encji jest ukryty wewnątrz klasy PanacheEntity.
Testowanie takiego aktywnego rekordu może wydawać się problematyczne na początku, ponieważ rekord używa metod statycznych. Na szczęście Quarkus dostarcza panache-mock, który pozwala nam użyć Mockito do mock’owania wszystkich dostarczonych metod statycznych, w tym tych wprowadzonych przez nas samych:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
@Test
public void createShoulWriteToDb() {
//given
PanacheMock.mock(ParkingSpotEntity.class);
Mockito.when(ParkingSpotEntity.findAll()).thenReturn(Collections.emptyList());
//when
parkingSpotService.create(parkingSpotDto);
//then
PanacheMock.verify(ParkingSpotEntity.class, Mockito.atLeast(1)).persist();
}
Podsumowanie
W tym artykule udało nam się stworzyć naszą pierwszą aplikację Quarkus eksponującą enpoint’y CRUD HTTP wraz z odpowiednimi testami i warstwą persystencji db. Wszystko to w kilku linijkach kodu i konfiguracji. Mam nadzieję, że udało mi się zainteresować Cię tym ekscytującym frameworkiem, i będziesz się dobrze bawić, odkrywając go dalej na własną rękę.