Obsługa błędów - dla mnie ta fraza kojarzy się z wściekłymi programistami frontendowymi oskarżającymi zespół backendowy o rzucanie w eter wyjątków bez żadnego ujednoliconego formatu odpowiedzi.
Wyobraźmy sobie, że ktoś zawiódł podczas procesu rozwoju lub planowania, a teraz musimy to naprawić. Jak ujednolicić odpowiedzi na błędy za pomocą Spring Boot? Moim ulubionym jest użycie @ControllerAdvice
. Jest to adnotacja używana do definiowania klasy, która zawiera @ExceptionHandler
metody z adnotacjami, które definiują sposób obsługi wyjątków.
Niestandardowe odpowiedzi na błędy
Aby ujednolicić logikę, musimy się przygotować:
- Klasa treści odpowiedzi - używana w każdej odpowiedzi na błąd.
- Wspólna metoda zwracająca ten sam format dla każdego obsłużonego wyjątku.
- Klasy obsługi wyjątków definiujące sposób obsługi danych wyjątków.
Przygotowanie
Zakładając, że pracujemy nad błędami dla menedżera pracowników i mamy employees/{id}
punkt końcowy, który rzuca EmployeeNotFoundException
gdy pracownik nie istnieje, stwórzmy/dodajmy test integracji spock w EmployeesControllerTest
:
def "should respond with NOT_FOUND Http status code when user with given firstName doesn't exist"() {
when:
def resultingStatusCode
def resultingBody
try {
restTemplate.getForEntity("http://localhost:$runningServerPort/employees/test-id", EmployeesDto)
} catch (HttpStatusCodeException e) {
resultingStatusCode = e.statusCode
resultingBody = new JsonSlurper().parseText(e.getResponseBodyAsString())
}
then:
resultingStatusCode == HttpStatus.NOT_FOUND
with(resultingBody) {
message == "Employee (id = 'test-id') doesn't exist."
code == "employee_not_found"
details == [[employeeId: "test-id"]]
}
}
Oczywiście test zakończy się niepowodzeniem, ponieważ odpowiedź jest zupełnie inna niż ta, której oczekujemy:
Treść odpowiedzi
Przede wszystkim musimy przygotować ciało wyjątkowej odpowiedzi. Nazwijmy go ErrorResponse
i wprowadź nazwę główną projektu:
class ErrorResponse {
private final String message;
private final String code; // code for frontend to tell which message it needs to show for user
private final List<Object> details; // ie. details which field in input failed
// ... constructors, builders, getters, setters, equals, hashcode, toString ... but I prefer to use Lombok instead ;)
}
Czy nie sądzisz, że dobrze byłoby zdefiniować enum
z kodami błędów, na wypadek gdyby inni programiści pracowali z tym kodem? Stwórzmy go:
enum ErrorCode {
EMPLOYEE_NOT_FOUND("employee_not_found");
private final String code;
ErrorCode(String code) {
this.code = code;
}
public String getCode() {
return this.code;
}
}
Wspólna odpowiedź
Następnie musimy utworzyć abstract class
z metodą, która zdefiniuje odpowiedź. Gdzie? To zależy od projektu, ale prawdopodobnie w katalogu głównym projektu, ponieważ musimy uzyskać do niego dostęp z pakietu każdej funkcji.
public abstract class HttpResponseExceptionHandler {
protected ResponseEntity<ErrorResponse> getErrorResponseEntity(
Exception e,
String errorCode,
List<Object> details,
HttpStatus status) {
ErrorResponse errorResponse = new ErrorResponse(e.getMessage(), errorCode, details);
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.set("Content-type", MediaType.APPLICATION_JSON_VALUE);
return new ResponseEntity<>(errorResponse, headers, status);
}
}
Obsługa wyjątków
Następną rzeczą jest przygotowanie @ControllerAdvice
dla każdego pakietu funkcjonalności. Jest to nasze miejsce do definiowania obsługi wyjątków dla błędów funkcjonalności:
@ControllerAdvice
class EmployeeExceptionHandler extends HttpResponseExceptionHandler {
@ExceptionHandler(value = {EmployeeNotFoundException.class})
ResponseEntity<ErrorResponse> handleCustomerAlreadyExists(EmployeeNotFoundException e) {
Map<String, String> detailsMap = Collections.singletonMap("employeeId", e.getEmployeeId());
return getErrorResponseEntity(
e,
ErrorCode.EMPLOYEE_NOT_FOUND.getCode(),
Collections.singletonList(detailsMap),
HttpStatus.NOT_FOUND);
}
}
Oczywiście w @ExceptionHandler
's value
można zdefiniować więcej klas wyjątków, jeśli pasują one do tego samego mapowania.
Weryfikacja
Uruchom wcześniej przygotowany test:
Teraz sprawdź odpowiedź:
To działa!
Zastępowanie domyślnych odpowiedzi na błędy Spring
Prawdopodobnie zauważyłeś, że istnieją pewne domyślne wiosenne programy obsługi wyjątków. Zostały one stworzone w celu uproszczenia odpowiedzi i wychwycenia możliwych wyjątków, takich jak HttpRequestMethodNotSupportedException
. Zastąpmy wspomniany.
Tradycyjnie, utwórz test:
def "should override default spring response for NotSupportedMethodException"() {
when:
def resultingStatusCode
def resultingBody
try {
restTemplate.put("http://localhost:$runningServerPort/employees/test-id", EmployeesDto)
} catch (HttpStatusCodeException e) {
resultingStatusCode = e.statusCode
resultingBody = new JsonSlurper().parseText(e.getResponseBodyAsString())
}
then:
resultingStatusCode == HttpStatus.METHOD_NOT_ALLOWED
with(resultingBody) {
message == "Not supported HTTP method. Available methods are: [GET]"
code == "not_supported_http_method"
}
}
Test kończy się niepowodzeniem, więc musimy nadpisać handleHttpRequestMethodNotSupported
metoda z ResponseEntityExceptionHandler
:
@ControllerAdvice
class SpringRESTExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
pageNotFoundLogger.warn(ex.getMessage());
Set<HttpMethod> supportedMethods = ex.getSupportedHttpMethods();
if (!CollectionUtils.isEmpty(supportedMethods)) {
headers.setAllow(supportedMethods);
}
ErrorResponse errorResponse = new ErrorResponse(
String.format("Not supported HTTP method. Available methods are: %s", supportedMethods),
"not_supported_http_method",
null);
return handleExceptionInternal(ex, errorResponse, headers, HttpStatus.METHOD_NOT_ALLOWED, request);
}
}
Oczywiście należy wymienić "not_supported_http_method"
z niektórymi enum
lub stały
Uruchom test:
Sprawdźmy teraz żądanie:
Wnioski
Jak widać, jest to naprawdę proste rozwiązanie. Możesz po prostu zastąpić domyślne odpowiedzi na błędy Spring, a także niestandardowe odpowiedzi na błędy wyjątków. Gdy format odpowiedzi i @ControllerAdvice
klasy są zdefiniowane, jedyną rzeczą do zrobienia jest przygotowanie nowych metod obsługi.
Chcesz porozmawiać z naszymi ekspertami na temat tworzenia oprogramowania na zamówienie?
P: Czym jest obsługa błędów w Spring Boot?
Obsługa błędów w Spring Boot odnosi się do procesu definiowania sposobu obsługi wyjątków, które mogą wystąpić w aplikacji Spring Boot. Można to zrobić za pomocą adnotacji @ControllerAdvice, aby zdefiniować klasę zawierającą metody z adnotacją @ExceptionHandler, które określają sposób obsługi określonych wyjątków.
P: Czym jest klasa ErrorResponse w obsłudze błędów Spring Boot?
Klasa ErrorResponse jest klasą używaną do definiowania treści wyjątkowej odpowiedzi w obsłudze błędów Spring Boot. Zazwyczaj zawiera ona takie właściwości jak komunikat, kod i szczegóły dotyczące błędu.
P: Czym jest klasa HttpResponseExceptionHandler w obsłudze błędów Spring Boot?
Klasa HttpResponseExceptionHandler jest klasą abstrakcyjną, która definiuje metodę tworzenia standardowej odpowiedzi na wyjątki w Spring Boot. Klasa ta może być rozszerzana przez inne klasy obsługi wyjątków w celu stworzenia spójnego formatu odpowiedzi na błędy.