Spring HATEOAS – REST API

Spring boot web umożliwia łatwy sposób na wystawienie restowego API. Chociaż nie ma oficjalnego dokumentu o tym jak powinniśmy projektować API tzn jakich metod HTTP używać, jaki status zwracać itp to zazwyczaj staramy trzymać się w miarę intuicyjnych schematów opisanych poniżej.
Do tego dodamy Spring HATEOS – jest to jedna z definicji architektury REST, której celem jest samoopisujące się API tzn. odpowiedź serwera zawiera w sobie aktualny stan zasobu jak również relacje i akcje możliwe do wywołania na obiekcie – klient nie musi posiadać żadnej wcześniejszej wiedzy o API wystawianym przez serwer. Może to trochę przypominać WebService przy użyciu protokołu HTTP.

 

Konfiguracja Spring Web

W zasadzie cała konfiguracja zawiera się w utworzeniu nowego projektu z zależnością do Spring Boot Web tak jak to opisałem w tworzeniu nowego projektu w spring. Dalej będziemy rozszerzać konfigurację dodając nowe funkcje do naszego API. Na początek zmieńmy kontekst naszej aplikacji czyli prefix url dla API np. zamiast wywoływać localhost:8080/doSomething możemy zmienić na localhost:8080/my-api-server/doSomething. Dzięki temu łatwo możemy wyróżnić serwisy w architekturze microservices. Załóżmy, że robimy API dla aplikacji do wypożyczania samochodów, wtedy dodamy :

1
2
// application.properties
server.context-path=/car-rental-service

 

Kontroler

Podstawowym komponentem do wystawiania endpointów w aplikacji webowej Spring jest klasa z adnotacją @RestController. Adnotacja ta oprócz tego, że stworzy nam obiekt bean, to również sprawia, że rezultat zwracany przez metody domyślnie jest serializowany jako body odpowiedzi. Wcześniej służyła do tego adnotacja @ResponseBody.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
@RequestMapping('/cars')
class CarController {

    @Autowired
    private CarService carService

    @GetMapping
    String getAllCars() {
        carService.fetchCarsList()
    }

    @GetMapping(path = '/{id}')
    Car getCar(@PathVariable Long id) {
        carService.getCar(id)
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    Car createCar(@RequestBody Car car) {
        carService.saveCar(car)
    }

    @PutMapping(path = '/{id}')
    Car updateCar(@PathVariable Long id, @RequestBody Car car) {
        carService.saveCar(car, id)
    }

    @DeleteMapping(path = '/{id}')
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void deleteCar(@PathVariable Long id) {
        carService.deleteCar(id)
    }
}

Powyższe mapowanie metod jest dosyć oczywiste. Dla akcji typu CRUD (Create, Read, Update, Delete) używam różnych metod HTTP. Przy pomocy @ResponseStatus zmieniam domyślny status 200 na bardziej odpowiadający wykonywanej akcji np. utworzenie nowego obiektu zwraca 201 Created. Standardowym zaś zachowaniem Spring MVC jest zwracanie statusu 405 Method Not Allowed dla zapytań, które nie zostały zmapowane w kontrolerze.

Spring Boot Web automatycznie zwraca nam odpowiedź w formacie JSON:

Jest tylko jeden problem… Pole firstRegistration jest typu LocalDate, które zostało zserializowane jak zwykły obiekt. Dużo lepiej byłoby mieć to w formacie ISO np 2018-03-28.

 

Java Time Module

Aby zmienić format daty dodamy moduł Jackson Datatype. Najpierw trzeba dodać zależność w build.gradle:

1
2
3
4
dependencies {
    compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.4'
    ...
}

Następnie wystarczy dodać jedną linię w application.properties:

1
spring.jackson.serialization.write_dates_as_timestamps=false

Domyślnie JavaTimeModule serializuje datę do formatu timestamp. Aby to zmienić wystarczy przełączyć tę cechę na false i uzyskamy format ISO:

 

Hateoas

Jak już mamy poprawną odpowiedź z serwera w formacie JSON można owrapować to w informacje hateoas o naszym API. W Spring służy do tego moduł Spring Hateoas:

1
  compile "org.springframework.boot:spring-boot-starter-hateoas"

Jeśli trzymamy się konwencji projektowanie API na zasadzie /nazwa_obiektu/id/nazwa_obiektu/id itd to możemy skorzystać z interfejsu EntityLinks do tworzenia linków do zasobów. Najpierw włączymy tę funkcję za pomocą adnotacji w klasie konfiguracyjnej:

1
2
3
4
5
@Configuration
@EnableEntityLinks
class AppConfig {

}

Następnie musimy zmodyfikować kontroler aby wystawiał zasób dla EntityLinks przy pomocy @ExposesResourceFor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RestController
@RequestMapping('/cars')
@ExposesResourceFor(Car)
class CarController {

    @Autowired
    private EntityLinks entityLinks
    @Autowired
    private CarService carService

    @GetMapping(produces = "application/hal+json")
    Resources<Resource<Car>> getAllCars() {
        new Resources(carService.fetchCarsList().collect({
            new Resource<Car>(it, entityLinks.linkToSingleResource(Car, it.carId))
        }), entityLinks.linkToCollectionResource(Car))
    }

    @GetMapping(path = '/{id}', produces = "application/hal+json")
    Resource<Car> getCar(@PathVariable Long id) {
        new Resource(carService.getCar(id), entityLinks.linkToSingleResource(Car, id))
    }

    @PostMapping(produces = "application/hal+json")
    @ResponseStatus(HttpStatus.CREATED)
    Resource<Car> createCar(@RequestBody Car car) {
        Car newCar = carService.saveCar(car)
        new Resource(car, entityLinks.linkToSingleResource(Car, newCar.carId))
    }

    @PutMapping(path = '/{id}', produces = "application/hal+json")
    Resource<Car> updateCar(@PathVariable Long id, @RequestBody Car car) {
        new Resource(carService.saveCar(car, id), entityLinks.linkToSingleResource(Car, id))
    }

    @DeleteMapping(path = '/{id}')
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void deleteCar(@PathVariable Long id) {
        carService.deleteCar(id)
    }
}

Dwie rzeczy na które trzeba zwrócić uwagę:

  • Do mapowania metod zwracających obiekt musimy jednoznaczenie dodać typ mime zwracany przez endpoint. Spring Hateoas na tą chwilę daje nam tylko jedną możliwość HAL. Jest to sposób reprezentacji obiektów Json implementujący Hateoas. Inną popularną implementacją jest Json API
  • Zwracane obiekty są teraz zagnieżdżone w obiekcie Resource, który umożliwia dodawanie linków self i relacji zasobu. Do tworzenia tych linków zastosowałem wcześniej wspomniany interfejs EntityLinks

Aby model Car mógł być zwracany jako Resource musi rozszerzać org.springframework.hateoas.ResourceSupport. Przykładowy model Car może wyglądać następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Canonical
class Car extends ResourceSupport {

    @JsonProperty(value = 'id', access = JsonProperty.Access.READ_ONLY)
    Long carId
    Brand brand
    String model
    Integer year
    Long milleage
    LocalDate firstRegistration = LocalDate.now()

    enum Brand {
        BMW, AUDI, MERCEDES
    }
}

Wynik zapytania GET cars wygląda teraz tak

zaś pojedynczy element:

 
W kolejnych postach będę rozszerzał powyższe api o walidację, autoryzację oraz testy.

Posted on: Kwiecień 1, 2018

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *