Spring Web – obsługa wyjątków

W rest API stworzonym według wpisu o HATEOS Rest api nie poruszyłem ważnej kwestii obsługi wyjątków. Spring domyślenie zwróci nam status HTTP 500 jeśli wyjątek nie zostanie obsłużony lub HTTP 400 jeśli wyślemy niepoprawnie sformatowany obiekt JSON. Gdybyśmy jednak chcieli obsłużyć wyjątek w naszej logice i zwrócić status inny niż 500 to musimy skorzystać z dodatkowych opcji dostarczanych przez Spring framework.

 



Wyjątek z adnotacją @ResponseStatus

Korzystając z naszego HATEOS Rest api, gdy wywołamy metodę GET /car-rental-service/cars/5 dla obiektu z id=5 dostaniemy błąd podobny do tego:

Krótko mówiąc jeśli aplikacja nie znajdzie szukanego elementu zwraca Internal Server Error z niezbyt jasną informacją o błędzie Content must not be null!

Prosty sposób aby temu zaradzić jest stworzenie własnej klasy wyjątku, który dodatkowe oznaczymy adnotacją definiującą status http jaki ma zostać zwrócony.

1
2
3
4
5
6
7
8
9
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(HttpStatus.NOT_FOUND)
class ResourceNotFound extends RuntimeException {
    ResourceNotFound(String message) {
        super(message)
    }
}

Tak przygotowany wyjątek możemy użyć w naszym serwisie w metodzie wyszukiwania obiektu Car:

1
2
3
4
5
Car getCar(long id) {
    Car car = carRepository.findById(id)
    if (!car) throw new ResourceNotFound("Car with id ${id} not found")
    car
}

Po ponowym zapytaniu do serwera o nieistniejący obiekt otrzymamy bardziej czytelny błąd:

 

Global Exception Handler

Powyższa metoda rozwiązywania wyjątków jest dobra jeśli obsługujemy własny wyjątek. Co jeśli jednak do wyjątku, który jest np. wyrzucany przez zewnętrzną bibliotekę, nie mamy dostępu. Oczywiście można by go łapać w try/catch i wyrzucać własny typ, jednakże mocno by to zanieczyściło nasz kod, poza tym byłoby bardzo niewygodne.
W tym wypadku powinniśmy użyć adnotacji @ExceptionHandler w @ControllerAdvice.
 
Załóżmy, że nasza aplikacja wyrzuca błąd przy tworzeniu nowego obiektu Car z powodu duplikacji unikalnego id na bazie danych. Wyjątek ten będzie zwracany przez API domyślnie w ten sposób:

Dużo lepszym statusem dla Unique constraint violation byłoby HTTP 409 Conflict oraz tekst informujący co dokładnie poszło nie tak. Należy w tym wypadku dodać nową klasę do obsługi globalnych wyjątków i odpowiednio zmapować wyjątek:

1
2
3
4
5
6
7
8
@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(value = SQLIntegrityConstraintViolationException)
    void handleSqlConstraintException(HttpServletResponse response, SQLIntegrityConstraintViolationException e) {
        response.sendError(HttpStatus.CONFLICT.value(), "Object unique constraint error.")
    }
}

Powyżej wydelegowałem konwersję błędu do formatu json do domyślnego konwertera ze spring używając do tego metody response.sendError. Przekazałem tylko odpowiedni status HTTP oraz własną informację o błędzie. Dzięki temu otrzymałem standardowy format błędu ze zmienionymi detalami:

Gdybym jednak chciał zwrócić inny obiekt json mógłbym do tego użyć adnotacji @RestControllerAdvice i w metodzie obsługującej wyjątek zwrócić cały obiekt ResponseEntity<MyError>.

***

Jest jeszcze kilka sposobów na obsługę wyjątków w REST API, ja jednak przybliżyłem Ci te najbardziej praktyczne. Polecam Ci oczywiście zgłębianie wiedzy z oficjalnej dokumentacji Spring.

Posted on: Kwiecień 8, 2018

Dodaj komentarz

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