Содержание
- Custom Error Messages in Spring REST API
- Overview
- Return a Generic Error Message using @ResponseStatus
- Return Error Message Using Custom Error Object
- Return Error Message Using HashMap
- Handle Bad Request Exceptions
- Type Mismatch Exceptions
- Bean Validation Exceptions
- Handle Media Type Not Supported Exception
- Handle Request Body Not Readable Exception
- Handle HTTP Request Method Not Supported Exception
- Default Exception Handler
- Summary
- Spring Boot REST API – обработка исключений. Часть 1
- Приложение
- BasicErrorController
- Не пользовательское исключение
- @ResponseStatus
- @ControllerAdvice
- Последовательность проверок
- Изменение DefaultErrorAttributes
- ResponseStatusException
- Итоги
Custom Error Messages in Spring REST API
Create global or application-level Exception handlers and return Custom Error Messages in Spring REST APIs.
Overview
Effective communication is the key to healthy and efficient relationships. Interestingly, the same applies to any Client and Server relationships. The client’s request may succeed or fail on the server. However, the server should provide the most appropriate status code in either of the outcomes.
Although sending a correct status code is enough for a client to take real action based on the outcome of a request, in case of failures, the client may need more details about what went wrong. For example, failure details like the exception type and an error message can help clients log the error or provide appropriate failure messages to their clients.
This article will teach How to handle different failures and return Custom Error Messages from a Spring REST API. If you don’t know how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.
Return a Generic Error Message using @ResponseStatus
The most basic way of returning an error message from a REST API is to use the @ResponseStatus annotation. We can add the error message in the annotation’s reason field. Although we can only return a generic error message, we can specify exception-specific error messages.
Next is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception-specific error messages.
The Exception handler class has three exception handlers, each of which returns a specific HTTP Response Status. Each response status specifies a reason field with a particular error message.
To view the error message in the response, ensure you have turned on include-messages in the server configuration. To learn more about Spring Boot server configurations, please visit Spring Boot Embedded Tomcat Configuration.
Next is an example of a response object the REST API returns. Note that the response object has the specified error message.
Although we can specify exception-specific error messages, it is still not informative. Therefore in the following sections, we will learn how to return a more specific error message from Spring REST API.
Return Error Message Using Custom Error Object
Let’s create a class representing the error message and the status code. We will return an instance of that in case of errors.
Next is the Error class representing the status code and a String message. We use a few Lombok annotations that introduce regular getter and setter methods and a constructor using the final fields.
Custom Response Error Class
Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.
The exception handler returns an instance of the Error class populated with the exception message and HTTP Status Code.
Now, we can throw our Not Found Exception with a custom error message.
When the REST API cannot find the requested resource, we get a detailed error as a response.
Return Error Message Using HashMap
Also, instead of creating a dedicated error class, we can return a detailed error message using a simple HashMap. Next is an example of producing and returning a Custom Error Message using Java HashMap.
Handle Bad Request Exceptions
The Bad Request errors are the Client errors where the client’s request doesn’t meet the requirements of the target server. This section will see how to handle Bad Request exceptions and provide a custom or detailed error response.
Type Mismatch Exceptions
The Type Mismatch Exceptions occur when Spring Controller cannot map the request parameters, path variables, or header values into controller method arguments. This section covers the handling of MethodArgumentTypeMismatchException and TypeMismatchException.
Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when there is a type mismatch while setting Bean properties. Also, both exceptions provide a detailed error message that we can use to prepare the Error object.
To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.
Note that the controller advice catches both exceptions; however, the method arguments accept an exception of type TypeMismatchException because it is the parent of the other exception.
Next, the snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.
Bean Validation Exceptions
The Bean Validation exceptions occur when the request contents do not pass the provided validations.
The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API’s exception handler.
Here we have created a List to represent individual binding errors and add that to the response Map. Instead, we can add a List field to the Error class we created in the previous section and populate the list with individual errors.
Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or PATCH endpoint on the server cannot handle the content type sent by the client. The REST Controllers on the server specify the content type they can support. When the media type that a client sends doesn’t match, the client gets this exception back.
To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.
As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type we provided and a list of actually supported media types. Thus, we create a custom error message based on the available information.
The above snippet shows a client’s sample error response when it sends a request with an invalid media type.
Handle Request Body Not Readable Exception
Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when the request body is missing or unreadable.
Handle HTTP Request Method Not Supported Exception
The HttpMethodNotSupportedException occurs when the HTTP endpoint on the REST API does not support the HTTP request method. Let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.
As seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods. We use it to form a clear error message.
The snippet showed an example response when the client attempted to execute a GET endpoint, while the REST API supports only POST.
Default Exception Handler
Similarly, we can create a default exception handler advice that handles all Exception types. Spring attempts to find the most specific handler when we have multiple exception handlers and falls back to the default handler if there is no suitable handler.
Above is an example of writing a default exception handler that returns an error message by the exception instance and an HTTP Status of 500.
Summary
This detailed tutorial taught us how to Return Custom Error Messages in Spring REST API. Firstly, we understood that Spring returns a generic error message and the most suitable HTTP Status Code by default. However, we can write our exception handlers for specific exceptions using @ControllerAdvice and produce a custom and detailed error response.
For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.
Источник
Spring Boot REST API – обработка исключений. Часть 1
В этой статье — обзор способов обработки исключений в Spring Boot.
Приложение
Мы рассмотрим простое REST API приложение с одной сущностью Person и с одним контроллером.
При старте приложения выполняется скрипт data.sql, который добавляет в базу данных H2 одну строку — Person c То есть Person c в базе отсутствует.
При попытке запросить Person c id=2:
метод контроллера getPerson() выбрасывает исключение — в данном случае наше пользовательское MyEntityNotFoundException:
BasicErrorController
По умолчанию все исключения попадают на адрес /error в BasicErrorController, в метод error():
Если поставить в этом методе break point, то будет понятно, из каких атрибутов собирается ответное JSON сообщение.
Проверим ответ по умолчанию, запросив с помощью клиента Postman отсутствующий Person, чтобы выбросилось MyEntityNotFoundException:
Причем для того, чтобы поле message было непустым, в application.properties нужно включить свойство:
Обратите внимание, что поле status JSON-тела ответа дублирует реальный http-код ответа. В Postman он виден:
Поле message заполняется полем message выброшенного исключения.
Независимо от того, какое исключение выбросилось: пользовательское или уже существующее, ответ стандартный — в том смысле, что набор полей одинаковый. Меняется только внутренняя часть и, возможно, код ответа (он не обязательно равен 500, некоторые существующие в Spring исключения подразумевают другой код).
Но структура ответа сохраняется.
Не пользовательское исключение
Например, если изменить код, убрав пользовательское MyEntityNotFoundException, то при отсутствии Person исключение будет все равно выбрасываться, но другое:
findById() возвращает тип Optional, а Optional.get() выбрасывает исключение NoSuchElementException с другим сообщением:
в итоге при запросе несуществующего Person:
ответ сохранит ту же структуру, но поменяется поле message:
Вернем обратно пользовательское исключение MyEntityNotFoundException.
Попробуем поменять ответ, выдаваемый в ответ за запрос. Статус 500 для него явно не подходит.
Рассмотрим способы изменения ответа.
@ResponseStatus
Пока поменяем только статус ответа. Сейчас возвращается 500, а нам нужен 404 — это логичный ответ, если ресурс не найден.
Для этого аннотируем наше исключение:
Теперь ответ будет таким:
@ControllerAdvice
Есть еще более мощный способ изменить ответ — @ControllerAdvice, и он имеет больший приоритет, чем @ResponseStatus.
В @ControllerAdvice можно не только изменить код ответа, но и тело. К тому же один обработчик можно назначить сразу для нескольких исключений.
Допустим мы хотим, чтобы ответ на запрос несуществующего Person имел такую структуру:
Для этого создадим обработчик в @ControllerAdvice, который перехватывает наше исключение MyEntityNotFoundException:
Теперь в ответ на запрос
мы получаем статус 404 с телом:
Но помимо MyEntityNotFoundException, наш обработчик поддерживает и javax.persistence.EntityNotFoundException (см. код выше).
Попробуем сделать так, чтобы оно возникло.
Это исключение EntityNotFoundException возникает в методе updatePerson() в контроллера. А именно, когда мы обращаемся с помощью метода PUT к несуществующей сущности в попытке назначить ей имя:
В этом случае мы тоже получим ответ с новой структурой:
Итого, обработчик в @ControllerAdvice позволил изменить не только код ответа, но и тело сообщение. Причем один обработчик мы применили для двух исключений.
Последовательность проверок
Обратите внимание, что MyEntityNotFoundException мы «обработали» дважды — изменили код с помощью @ResponseStatus (1) и прописали в @ContollerAdvice — тут изменили как код, так и тело ответа (2). Эти обработки могли быть противоречивы, но существует приоритет:
- Когда выбрасывается исключение MyEntityNotFoundException, сначала Spring проверяет @ControllerAdvice-класс. А именно, нет ли в нем обработчика, поддерживающего наше исключение. Если обработчик есть, то исключение в нем и обрабатывается. В этом случае код @ResponseStatus значения не имеет, и в BasicErrorController исключение тоже не идет.
- Если исключение не поддерживается в @ControllerAdvice-классе, то оно идет в BasicErrorController. Но перед этим Spring проверяет, не аннотировано ли исключение аннотацией @ResponseStatus. Если да, то код ответа меняется, как указано в @ResponseStatus. Далее формируется ответ в BasicErrorController.
- Если же первые два условия не выполняются, то исключение обрабатывается сразу в BasicErrorController — там формируется стандартный ответ со стандартным кодом (для пользовательских исключений он равен 500).
Но и стандартный ответ можно изменить, для этого нужно расширить класс DefaultErrorAttributes.
Попробуем это сделать.
Изменение DefaultErrorAttributes
Давайте добавим в стандартный ответ еще одно поле. Для этого расширим класс:
В Map errorAttributes перечисляются поля ответа. Мы взяли их из родительского метода и добавили свое поле newAttribute.
Чтобы выполнить проверку, надо убрать @ControllerAdvice, поскольку он самый приоритетный и с ним мы даже не дойдем до BasicErrorController со «стандартными» полями.
Далее запросим ресурс:
В JSON-ответе появилось дополнительное поле.
ResponseStatusException
Рассмотрим еще один вариант, позволяющий сразу протолкнуть код ответа и сообщение стандартные поля, не прописывая обработку пользовательских или встроенных исключений. А вместо этого просто выбросив специально предназначенное исключение ResponseStatusException.
Изменим код метода контроллера getPerson():
Теперь тут не выбрасывается ни MyEntityNotFoundException, ни java.util.NoSuchElementException. А выбрасывается ResponseStatusException с заданным сообщением и кодом ответа.
Теперь при запросе
ответ будет таким:
Как код, так и сообщение появилось в полях стандартного ответа.
ResponseStatusException не вступает в конкуренцию ни со способом @ControllerAdvice, ни с @ResponseStatus — просто потому, что это другое исключение.
Итоги
Код примера доступен на GitHub. В следующей части мы унаследуем RestExceptionHandler от ResponseEntityExceptionHandler. Это класс-заготовка, которая уже обрабатывает ряд исключений.
Источник
Create global or application-level Exception handlers and return Custom Error Messages in Spring REST APIs.
Overview
Effective communication is the key to healthy and efficient relationships. Interestingly, the same applies to any Client and Server relationships. The client’s request may succeed or fail on the server. However, the server should provide the most appropriate status code in either of the outcomes.
Although sending a correct status code is enough for a client to take real action based on the outcome of a request, in case of failures, the client may need more details about what went wrong. For example, failure details like the exception type and an error message can help clients log the error or provide appropriate failure messages to their clients.
This article will teach How to handle different failures and return Custom Error Messages from a Spring REST API. If you don’t know how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.
Return a Generic Error Message using @ResponseStatus
The most basic way of returning an error message from a REST API is to use the @ResponseStatus annotation. We can add the error message in the annotation’s reason field. Although we can only return a generic error message, we can specify exception-specific error messages.
Next is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception-specific error messages.
Code language: Java (java)
@ControllerAdvice public class ApplicationExceptionHandler { @ResponseStatus( value = HttpStatus.NOT_FOUND, reason = "Requested Student Not Found") @ExceptionHandler(StudentNotFoundException.class) public void handleException(StudentNotFoundException e) { } @ResponseStatus( value = HttpStatus.BAD_REQUEST, reason = "Received Invalid Input Parameters") @ExceptionHandler(InputValidationException.class) public void handleException(InputValidationException e) { } @ResponseStatus( value = HttpStatus.GATEWAY_TIMEOUT, reason = "Upstream Service Not Responding, Try Again") @ExceptionHandler(ServiceUnavailableException.class) public void handleException(ServiceUnavailableException e) { } }
The Exception handler class has three exception handlers, each of which returns a specific HTTP Response Status. Each response status specifies a reason field with a particular error message.
To view the error message in the response, ensure you have turned on include-messages in the server configuration. To learn more about Spring Boot server configurations, please visit Spring Boot Embedded Tomcat Configuration.
Code language: YAML (yaml)
server: error: include-message: always
Next is an example of a response object the REST API returns. Note that the response object has the specified error message.
Code language: JSON / JSON with Comments (json)
{ "timestamp": "", "status": 404, "error": "Not Found", "message": "Requested Student Not Found", "path": "/students/Jack" }
Although we can specify exception-specific error messages, it is still not informative. Therefore in the following sections, we will learn how to return a more specific error message from Spring REST API.
Return Error Message Using Custom Error Object
Let’s create a class representing the error message and the status code. We will return an instance of that in case of errors.
Next is the Error class representing the status code and a String message. We use a few Lombok annotations that introduce regular getter and setter methods and a constructor using the final fields.
Custom Response Error Class
Code language: Java (java)
@Data @RequiredArgsConstructor public class Error { private final HttpStatus httpStatus; private final String message; }
Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.
@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity handleException( StudentNotFoundException e) { Error error = new Error(HttpStatus.NOT_FOUND, e.getLocalizedMessage()); return new ResponseEntity<>(error, error.getHttpStatus()); }
Code language: Java (java)
The exception handler returns an instance of the Error class populated with the exception message and HTTP Status Code.
Now, we can throw our Not Found Exception with a custom error message.
Code language: Java (java)
throw new StudentNotFoundException ("Student service failed, studentId : " + studentId);
When the REST API cannot find the requested resource, we get a detailed error as a response.
Code language: JSON / JSON with Comments (json)
{ "httpStatus": "NOT_FOUND", "message": "Student service failed, studentId : Jack" }
Return Error Message Using HashMap
Also, instead of creating a dedicated error class, we can return a detailed error message using a simple HashMap. Next is an example of producing and returning a Custom Error Message using Java HashMap.
Code language: Java (java)
@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity<Map<String, String>> handleException(StudentNotFoundException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.NOT_FOUND.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); }
Handle Bad Request Exceptions
The Bad Request errors are the Client errors where the client’s request doesn’t meet the requirements of the target server. This section will see how to handle Bad Request exceptions and provide a custom or detailed error response.
Type Mismatch Exceptions
The Type Mismatch Exceptions occur when Spring Controller cannot map the request parameters, path variables, or header values into controller method arguments. This section covers the handling of MethodArgumentTypeMismatchException and TypeMismatchException.
Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when there is a type mismatch while setting Bean properties. Also, both exceptions provide a detailed error message that we can use to prepare the Error object.
To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.
Code language: Java (java)
@ExceptionHandler({ MethodArgumentTypeMismatchException.class, TypeMismatchException.class }) public ResponseEntity<Map<String, String>> handleException(TypeMismatchException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
Note that the controller advice catches both exceptions; however, the method arguments accept an exception of type TypeMismatchException because it is the parent of the other exception.
Next, the snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.
Code language: JSON / JSON with Comments (json)
{ "httpStatus": "BAD_REQUEST", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; nested exception is java.lang.NumberFormatException: For input String: "Jack"" }
Bean Validation Exceptions
The Bean Validation exceptions occur when the request contents do not pass the provided validations.
The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API’s exception handler.
Code language: Java (java)
@ExceptionHandler({ BindException.class, MethodArgumentNotValidException.class }) public ResponseEntity<Map<String, Object>> handleException(BindException e) { List<String> errors = new ArrayList<>(); e.getFieldErrors() .forEach(err -> errors.add(err.getField() + ": " + err.getDefaultMessage())); e.getGlobalErrors() .forEach(err -> errors.add(err.getObjectName() + ": " + err.getDefaultMessage())); Map<String, Object> errorResponse = Map.of( "error", errors, "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
Here we have created a List<String> to represent individual binding errors and add that to the response Map. Instead, we can add a List<String>
field to the Error class we created in the previous section and populate the list with individual errors.
Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or PATCH endpoint on the server cannot handle the content type sent by the client. The REST Controllers on the server specify the content type they can support. When the media type that a client sends doesn’t match, the client gets this exception back.
To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.
Code language: Java (java)
@ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpMediaTypeNotSupportedException e) { String provided = e.getContentType().toString(); List<String> supported = e.getSupportedMediaTypes().stream() .map(MimeType::toString) .collect(Collectors.toList()); String error = provided + " is not one of the supported media types (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.UNSUPPORTED_MEDIA_TYPE); }
As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type we provided and a list of actually supported media types. Thus, we create a custom error message based on the available information.
Code language: JSON / JSON with Comments (json)
{ "error":"text/plain;charset=UTF-8 is not one of the supported media types ( application/octet-stream, text/plain, application/xml, text/xml, application/x-www-form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, application/json, application/*+json, */*)", "message":"Content type 'text/plain;charset=UTF-8' not supported", "status":"415 UNSUPPORTED_MEDIA_TYPE" }
The above snippet shows a client’s sample error response when it sends a request with an invalid media type.
Handle Request Body Not Readable Exception
Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when the request body is missing or unreadable.
Code language: Java (java)
@ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Map<String, String>> handleException( HttpMessageNotReadableException e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
Handle HTTP Request Method Not Supported Exception
The HttpMethodNotSupportedException occurs when the HTTP endpoint on the REST API does not support the HTTP request method. Let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.
Code language: Java (java)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpRequestMethodNotSupportedException e) throws IOException { String provided = e.getMethod(); List<String> supported = List.of(e.getSupportedMethods()); String error = provided + " is not one of the supported Http Methods (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.METHOD_NOT_ALLOWED.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED); }
As seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods. We use it to form a clear error message.
Code language: JSON / JSON with Comments (json)
{ "error": "GET is not one of the supported Http Methods (POST)", "message": "Request method 'GET' not supported", "status": "405 METHOD_NOT_ALLOWED" }
The snippet showed an example response when the client attempted to execute a GET endpoint, while the REST API supports only POST.
Default Exception Handler
Similarly, we can create a default exception handler advice that handles all Exception types. Spring attempts to find the most specific handler when we have multiple exception handlers and falls back to the default handler if there is no suitable handler.
Code language: Java (java)
@ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleException( Exception e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.INTERNAL_SERVER_ERROR.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); }
Above is an example of writing a default exception handler that returns an error message by the exception instance and an HTTP Status of 500.
Summary
This detailed tutorial taught us how to Return Custom Error Messages in Spring REST API. Firstly, we understood that Spring returns a generic error message and the most suitable HTTP Status Code by default. However, we can write our exception handlers for specific exceptions using @ControllerAdvice and produce a custom and detailed error response.
For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.
- Type Parameters:
T
— the body type
public class ResponseEntity<T>
extends HttpEntity<T>
Extension of HttpEntity
that adds an HttpStatusCode
status code.
Used in RestTemplate
as well as in @Controller
methods.
In RestTemplate
, this class is returned by
getForEntity()
and
exchange()
:
ResponseEntity<String> entity = template.getForEntity("https://example.com", String.class); String body = entity.getBody(); MediaType contentType = entity.getHeaders().getContentType(); HttpStatus statusCode = entity.getStatusCode();
This can also be used in Spring MVC as the return value from an
@Controller
method:
@RequestMapping("/handle") public ResponseEntity<String> handle() { URI location = ...; HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setLocation(location); responseHeaders.set("MyResponseHeader", "MyValue"); return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED); }
Or, by using a builder accessible via static methods:
@RequestMapping("/handle") public ResponseEntity<String> handle() { URI location = ...; return ResponseEntity.created(location).header("MyResponseHeader", "MyValue").body("Hello World"); }
- Since:
- 3.0.2
- Author:
- Arjen Poutsma, Brian Clozel
- See Also:
-
getStatusCode()
RestOperations.getForEntity(String, Class, Object...)
RestOperations.getForEntity(String, Class, java.util.Map)
RestOperations.getForEntity(URI, Class)
RequestEntity
-
Nested Class Summary
Nested Classes
static interface
Defines a builder that adds a body to the response entity.
static interface
Defines a builder that adds headers to the response entity.
-
Field Summary
-
Constructor Summary
Constructors
Create a
ResponseEntity
with a status code only.Create a
ResponseEntity
with headers and a status code.Create a
ResponseEntity
with a body and status code.Create a
ResponseEntity
with a body, headers, and a raw status code.Create a
ResponseEntity
with a body, headers, and a status code. -
Method Summary
accepted()
Create a builder with an ACCEPTED status.
created(URI location)
Create a new builder with a CREATED status
and a location header set to the given URI.boolean
Return the HTTP status code of the response.
int
int
hashCode()
noContent()
notFound()
A shortcut for creating a
ResponseEntity
with the given body
and the OK status, or an empty body and a
NOT FOUND status in case of an
Optional.empty() parameter.ok()
Create a builder with the status set to OK.
ok(T body)
A shortcut for creating a
ResponseEntity
with the given body
and the status set to OK.status(int status)
Create a builder with the given status.
Create a builder with the given status.
toString()
-
Constructor Details
-
ResponseEntity
Create a
ResponseEntity
with a status code only.- Parameters:
status
— the status code
-
ResponseEntity
Create a
ResponseEntity
with a body and status code.- Parameters:
body
— the entity bodystatus
— the status code
-
ResponseEntity
Create a
ResponseEntity
with headers and a status code.- Parameters:
headers
— the entity headersstatus
— the status code
-
ResponseEntity
Create a
ResponseEntity
with a body, headers, and a status code.- Parameters:
body
— the entity bodyheaders
— the entity headersstatus
— the status code
-
ResponseEntity
Create a
ResponseEntity
with a body, headers, and a raw status code.- Parameters:
body
— the entity bodyheaders
— the entity headersrawStatus
— the status code value- Since:
- 5.3.2
-
-
Method Details
-
getStatusCode
Return the HTTP status code of the response.
- Returns:
- the HTTP status as an HttpStatus enum entry
-
getStatusCodeValue
Return the HTTP status code of the response.
- Returns:
- the HTTP status as an int value
- Since:
- 4.3
-
equals
- Overrides:
equals
in classHttpEntity<T>
-
hashCode
public int hashCode()
- Overrides:
hashCode
in classHttpEntity<T>
-
toString
- Overrides:
toString
in classHttpEntity<T>
-
status
Create a builder with the given status.
- Parameters:
status
— the response status- Returns:
- the created builder
- Since:
- 4.1
-
status
Create a builder with the given status.
- Parameters:
status
— the response status- Returns:
- the created builder
- Since:
- 4.1
-
ok
Create a builder with the status set to OK.
- Returns:
- the created builder
- Since:
- 4.1
-
ok
A shortcut for creating a
ResponseEntity
with the given body
and the status set to OK.- Parameters:
body
— the body of the response entity (possibly empty)- Returns:
- the created
ResponseEntity
- Since:
- 4.1
-
of
A shortcut for creating a
ResponseEntity
with the given body
and the OK status, or an empty body and a
NOT FOUND status in case of an
Optional.empty() parameter.- Returns:
- the created
ResponseEntity
- Since:
- 5.1
-
of
- Parameters:
body
— the problem detail to use- Returns:
- the created builder
- Since:
- 6.0
-
created
Create a new builder with a CREATED status
and a location header set to the given URI.- Parameters:
location
— the location URI- Returns:
- the created builder
- Since:
- 4.1
-
accepted
Create a builder with an ACCEPTED status.
- Returns:
- the created builder
- Since:
- 4.1
-
noContent
Create a builder with a NO_CONTENT status.
- Returns:
- the created builder
- Since:
- 4.1
-
badRequest
- Returns:
- the created builder
- Since:
- 4.1
-
notFound
Create a builder with a NOT_FOUND status.
- Returns:
- the created builder
- Since:
- 4.1
-
unprocessableEntity
- Returns:
- the created builder
- Since:
- 4.1.3
-
internalServerError
- Returns:
- the created builder
- Since:
- 5.3.8
-
When you develop a Spring Bool RESTful service, you as a programmer are responsible for handling exceptions in the service. For instance, by properly handling exceptions, you can stop the disruption of the normal flow of the application. In addition, proper exception handling ensures that the code doesn’t break when an exception occurs.
Another important thing is to ensure as a programmer is not to send any exceptions or error stacks to clients. Exception and error messages sent to clients should be short and meaningful.
In this post, I will explain how to gracefully handle exceptions in Spring Boot RESTful services.
Dependency
For this post, we will create a Sprinfg Boot RESTful service that performs CRUD operations on Blog entities. We will use embedded H2 as the database. The following code shows the dependencies of the application in the pom.xml
file.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> </dependency>
In the context of our Blog
RESTful service, the application may encounter several types of exceptions. For example, the database may be down. Another scenario can be a user trying to save an already existing blog. Or a user trying to access a blog yet to be published.
You should handle such scenarios gracefully in the application.
As an example, for database failure, the application throws SQLException.
Instead of returning the exception stack trace to client, you should return a meaningful exception message.
The Entity Class
The code for the Blog
Entity class is this.
Blog.java
@Entity public class Blog { @Id private int blogId; private String blogTitle; private String blogCreator; private int yearOfPost; // No-Args and Parametrized Constructor //Getters and Setters }
It is a JPA Entity class annotated with the @Entity
annotation and corresponding getters and setters for the fields.
The Repository
This is the Blog Repository
Interface.
BlogRepository.java
package org.springframework.guru.repository; import org.springframework.guru.model.Blog; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface BlogRepository extends CrudRepository<Blog,Integer> { }
Here, the BlogRepository
extends the CrudRepository
of Spring Data JPA.
Custom Exception Classes
In our application, we will create custom exception classes. Such classes enable us to customize an exception according to the callers’ needs.
We will create two custom exception classes:
BlogAlreadyExistsException
: Is thrown when a user tries to add an already existing blog.BlogNotFoundException
: Is thrown when a user tries to access a blog that is not present.
The code of the BlogAlreadyExistsException
class is this.
BlogAlreadyExistsException.java
package org.springframework.guru.exception; public class BlogAlreadyExistsException extends RuntimeException { private String message; public BlogAlreadyExistsException(String message) { super(message); this.message = message; } public BlogAlreadyExistsException() { } }
The code for the BlogNotFoundException
class is this.
BlogNotFoundException.java
package org.springframework.guru.exception; public class BlogNotFoundException extends RuntimeException { private String message; public BlogNotFoundException(String message) { super(message); this.message = message; } public BlogNotFoundException() { } }
The Service
This is the BlogService
interface which has various methods to perform operations on Blog
entities.
BlogService.java
package org.springframework.guru.service; import org.springframework.guru.exception.BlogAlreadyExistsException; import org.springframework.guru.exception.BlogNotFoundException; import org.springframework.guru.model.Blog; import java.util.List; public interface BlogService { Blog saveBlog(Blog blog) throws BlogAlreadyExistsException; List getAllBlogs() throws BlogNotFoundException; Blog getBlogById(int id) throws BlogNotFoundException; }
In the preceding BlogService interface, the saveBlog()
method declares that it throws BlogAlreadyExistsException
. The two other methods, getAllBlogs()
and getBlogById()
declares that they throw BlogNotFoundException
.
The service implementation class for BlogService
is this.
BlogServiceImpl.java
@Service public class BlogServiceImpl implements BlogService { private BlogRepository blogRepository; @Autowired public BlogServiceImpl(BlogRepository blogRepository) { this.blogRepository = blogRepository; } @Override public Blog saveBlog(Blog blog) { if (blogRepository.existsById(blog.getBlogId())) { throw new BlogAlreadyExistsException(); } Blog savedBlog = blogRepository.save(blog); return savedBlog; } @Override public List getAllBlogs() { return (List) blogRepository.findAll(); } @Override public Blog getBlogById(int id) throws BlogNotFoundException { Blog blog; if (blogRepository.findById(id).isEmpty()) { throw new BlogNotFoundException(); } else { blog = blogRepository.findById(id).get(); } return blog; } }
The preceding BlogServiceImpl
class implements the methods declared in the BlogService
interface.
There are two paths in exception handling. One is the code handles the exception using a try-catch block. The other is to propagate back a custom exception to the caller. The preceding service class uses the latter approach.
Line 12 – Line 3 checks if the blog already exists in the database. If true the method throws a BlogAlreadyExistsException
. Else, the method saves the Blog
object.
Line 27 – Line 28 throws a BlogNotFoundException
if the Blog
with the specified Id is not present in the database.
The Controller
The code for the BlogController
is this.
BlogController.java
@RestController @RequestMapping("api/v1") public class BlogController { private BlogService blogService; @Autowired public BlogController(BlogService blogService) { this.blogService = blogService; } @PostMapping("/blog") public ResponseEntity saveBlog(@RequestBody Blog blog) throws BlogAlreadyExistsException { Blog savedBlog = blogService.saveBlog(blog); return new ResponseEntity<>(savedBlog, HttpStatus.CREATED); } @GetMapping("/blogs") public ResponseEntity<List> getAllBlogs() throws BlogNotFoundException { return new ResponseEntity<List>((List) blogService.getAllBlogs(), HttpStatus.OK); } @GetMapping("blog/{id}") public ResponseEntity getBlogById(@PathVariable("id") int id) throws BlogNotFoundException { return new ResponseEntity(blogService.getBlogById(id), HttpStatus.OK); }
The preceding controller class is not handling the custom exceptions. Instead, it throws the exceptions back to the caller – which in our scenario is a REST client. This is not what we want – directly sending back exceptions to clients.
Instead, we should handle the exception and send back a short and meaningful exception message to the client. We can use different approaches to achieve this.
Approach 1: Traditional try-catch Block
The first approach is to use Java try-catch block to handle the exception in the controller methods. The code to handle BlogNotFoundException
in the getBlogById()
method is this.
@GetMapping("blog/{id}") public ResponseEntity getBlogById(@PathVariable("id") int id) { try{ return new ResponseEntity(blogService.getBlogById(id), HttpStatus.OK); } catch(BlogNotFoundException blogNotFoundException ){ return new ResponseEntity(blogNotFoundException.getMessage(), HttpStatus.CONFLICT); } }
In the preceding code, the call to the BlogService.getBlogById()
method is wrapped in a try
block. If a method call to getBlogById()
throws BlogNotFoundException
, the catch
block handles the exception. In the catch
block, the ResponseEntity
object is used to send a custom error message with a status code as a response.
Approach 2: Spring @ExceptionHandler Annotation
Spring provides the @ExceptionHandler
annotation to handle exceptions in specific handler classes or handler methods.
Spring configuration will detect this annotation and register the method as an exception handler. The method will handle the exception and its subclasses passed to the annotation.
@ExceptionHandler(value = BlogAlreadyExistsException.class) public ResponseEntity handleBlogAlreadyExistsException(BlogAlreadyExistsException blogAlreadyExistsException) { return new ResponseEntity("Blog already exists", HttpStatus.CONFLICT); }
When any method in the controller throws the BlogAlreadyExistsException
exception, Spring invokes the handleBlogAlreadyExistsException()
method. This method returns a ResponseEntity
that wraps a custom error message and a status code.
When you run the application and send a POST request to add an existing blog, you will get this output.
Approach 3: Global Exception Handling with @ControllerAdvice
The @ExceptionHandler
annotation is only active for that particular class where it is declared. If you want a global exception handler you can use Spring AOP. A global exception handler provides a standard way of handling exceptions throughout the application. In addition, it considerably reduces the amount of code written for exception handling.
The Spring @ExceptionHandler
along with @ControllerAdvice
of Spring AOP enables a mechanism to handle exceptions globally.
The code for the GlobalExceptionHandler
class is this.
GlobalExceptionHandler.java
@ControllerAdvice public class GlobalExceptionHandler { @Value(value = "${data.exception.message1}") private String message1; @Value(value = "${data.exception.message2}") private String message2; @Value(value = "${data.exception.message3}") private String message3; @ExceptionHandler(value = BlogNotFoundException.class) public ResponseEntity blogNotFoundException(BlogNotFoundException blogNotFoundException) { return new ResponseEntity(message2, HttpStatus.NOT_FOUND); } @ExceptionHandler(value = Exception.class) public ResponseEntity<> databaseConnectionFailsException(Exception exception) { return new ResponseEntity<>(message3, HttpStatus.INTERNAL_SERVER_ERROR); } }
The @ControllerAdvice
annotation in Line 1 consolidates multiple @ExceptionHandlers
into a single, global exception handling component.
The @Value
annotation injects exception messages specified in the application.properties
file into the fields.
The application.properties
file is this.
data.exception.message1=BlogAlreadyExists data.exception.message2=BlogNotFound data.exception.message3=DataConnectivityisLost
Let’s send a GET Request tolocalhost:8080/api/v1/blog/2
to retrieve an unpublished blog. The response is shown in this Figure.
You can find the source code of this post on Github
For in-depth knowledge on the Spring Framework and Spring Boot, you can check my Udemy Best Seller Course Spring Framework 5: Beginner to Guru
Во время работы вашего приложения часто будут возникать исключительные ситуации. Когда у вас простое консольное приложение, то все просто – ошибка выводится в консоль. Но как быть с веб-приложением?
Допустим у пользователя отсутсвует доступ, или он передал некорректные данные. Лучшим вариантом будет в ответ на такие ситуации, отправлять пользователю сообщения с описанием ошибки. Это позволит клиенту вашего API скорректировать свой запрос.
В данной статье разберём основные возможности, которые предоставляет SpringBoot для решения этой задачи и на простых примерах посмотрим как всё работает.
@ExceptionHandler
@ExceptionHandler
позволяет обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод в контроллере, в котором будет содержаться вся логика обработки нужного исключения, и пометить его аннотацией.
Для примера у нас будет сущность Person
, бизнес сервис к ней и контроллер. Контроллер имеет один эндпойнт, который возвращает пользователя по логину. Рассмотрим классы нашего приложения:
Сущность Person
:
package dev.struchkov.general.sort;
import java.text.MessageFormat;
public class Person {
private String lastName;
private String firstName;
private Integer age;
//getters and setters
}
Контроллер PersonController
:
package dev.struchkov.example.controlleradvice.controller;
import dev.struchkov.example.controlleradvice.domain.Person;
import dev.struchkov.example.controlleradvice.service.PersonService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("api/person")
@RequiredArgsConstructor
public class PersonController {
private final PersonService personService;
@GetMapping
public ResponseEntity<Person> getByLogin(@RequestParam("login") String login) {
return ResponseEntity.ok(personService.getByLoginOrThrown(login));
}
@GetMapping("{id}")
public ResponseEntity<Person> getById(@PathVariable("id") UUID id) {
return ResponseEntity.ok(personService.getById(id).orElseThrow());
}
}
И наконец PersonService
, который будет возвращать исключение NotFoundException
, если пользователя не будет в мапе persons
.
package dev.struchkov.example.controlleradvice.service;
import dev.struchkov.example.controlleradvice.domain.Person;
import dev.struchkov.example.controlleradvice.exception.NotFoundException;
import lombok.NonNull;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@Service
public class PersonService {
private final Map<UUID, Person> people = new HashMap<>();
public PersonService() {
final UUID komarId = UUID.randomUUID();
people.put(komarId, new Person(komarId, "komar", "Алексей", "ertyuiop"));
}
public Person getByLoginOrThrown(@NonNull String login) {
return people.values().stream()
.filter(person -> person.getLogin().equals(login))
.findFirst()
.orElseThrow(() -> new NotFoundException("Пользователь не найден"));
}
public Optional<Person> getById(@NonNull UUID id) {
return Optional.ofNullable(people.get(id));
}
}
Перед тем, как проверить работу исключения, давайте посмотрим на успешную работу эндпойнта.
Все отлично. Нам в ответ пришел код 200, а в теле ответа пришел JSON нашей сущности. А теперь мы отправим запрос с логином пользователя, которого у нас нет. Посмотрим, что сделает Spring по умолчанию.
Обратите внимание, ошибка 500 – это стандартный ответ Spring на возникновение любого неизвестного исключения. Также исключение было выведено в консоль.
Как я уже говорил, отличным решением будет сообщить пользователю, что он делает не так. Для этого добавляем метод с аннотацией @ExceptionHandler
, который будет перехватывать исключение и отправлять понятный ответ пользователю.
@RequestMapping("api/person")
@RequiredArgsConstructor
public class PersonController {
private final PersonService personService;
@GetMapping
public ResponseEntity<Person> getByLogin(@RequestParam("login") String login) {
return ResponseEntity.ok(personService.getByLoginOrThrown(login));
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> handleException(NotFoundException exception) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
}
Вызываем повторно наш метод и видим, что мы стали получать понятное описание ошибки.
Но теперь вернулся 200 http код, куда корректнее вернуть 404 код.
Однако некоторые разработчики предпочитают возвращать объект, вместо ResponseEntity<T>
. Тогда вам необходимо воспользоваться аннотацией @ResponseStatus
.
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
public ErrorMessage handleException(NotFoundException exception) {
return new ErrorMessage(exception.getMessage());
}
Если попробовать совместить ResponseEntity<T>
и @ResponseStatus
, http-код будет взят из ResponseEntity<T>
.
Главный недостаток @ExceptionHandler
в том, что он определяется для каждого контроллера отдельно. Обычно намного проще обрабатывать все исключения в одном месте.
Хотя это ограничение можно обойти если @ExceptionHandler
будет определен в базовом классе, от которого будут наследоваться все контроллеры в приложении, но такой подход не всегда возможен, особенно если перед нами старое приложение с большим количеством легаси.
HandlerExceptionResolver
Как мы знаем в программировании магии нет, какой механизм задействуется, чтобы перехватывать исключения?
Интерфейс HandlerExceptionResolver
является общим для обработчиков исключений в Spring. Все исключений выброшенные в приложении будут обработаны одним из подклассов HandlerExceptionResolver
. Можно сделать как свою собственную реализацию данного интерфейса, так и использовать существующие реализации, которые предоставляет нам Spring из коробки.
Давайте разберем стандартные для начала:
ExceptionHandlerExceptionResolver
— этот резолвер является частью механизма обработки исключений помеченных аннотацией @ExceptionHandler
, которую мы рассмотрели выше.
DefaultHandlerExceptionResolver
— используется для обработки стандартных исключений Spring и устанавливает соответствующий код ответа, в зависимости от типа исключения:
Exception | HTTP Status Code |
---|---|
BindException | 400 (Bad Request) |
ConversionNotSupportedException | 500 (Internal Server Error) |
HttpMediaTypeNotAcceptableException | 406 (Not Acceptable) |
HttpMediaTypeNotSupportedException | 415 (Unsupported Media Type) |
HttpMessageNotReadableException | 400 (Bad Request) |
HttpMessageNotWritableException | 500 (Internal Server Error) |
HttpRequestMethodNotSupportedException | 405 (Method Not Allowed) |
MethodArgumentNotValidException | 400 (Bad Request) |
MissingServletRequestParameterException | 400 (Bad Request) |
MissingServletRequestPartException | 400 (Bad Request) |
NoSuchRequestHandlingMethodException | 404 (Not Found) |
TypeMismatchException | 400 (Bad Request) |
Мы можем создать собственный HandlerExceptionResolver
. Назовем его CustomExceptionResolver
и вот как он будет выглядеть:
package dev.struchkov.example.controlleradvice.service;
import dev.struchkov.example.controlleradvice.exception.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CustomExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
final ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
if (e instanceof NotFoundException) {
modelAndView.setStatus(HttpStatus.NOT_FOUND);
modelAndView.addObject("message", "Пользователь не найден");
return modelAndView;
}
modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
modelAndView.addObject("message", "При выполнении запроса произошла ошибка");
return modelAndView;
}
}
Мы создаем объект представления – ModelAndView
, который будет отправлен пользователю, и заполняем его. Для этого проверяем тип исключения, после чего добавляем в представление сообщение о конкретной ошибке и возвращаем представление из метода. Если ошибка имеет какой-то другой тип, который мы не предусмотрели в этом обработчике, то мы отправляем сообщение об ошибке при выполнении запроса.
Так как мы пометили этот класс аннотацией @Component
, Spring сам найдет и внедрит наш резолвер куда нужно. Посмотрим, как Spring хранит эти резолверы в классе DispatcherServlet
.
Все резолверы хранятся в обычном ArrayList
и в случае исключнеия вызываются по порядку, при этом наш резолвер оказался последним. Таким образом, если непосредственно в контроллере окажется @ExceptionHandler
обработчик, то наш кастомный резолвер не будет вызван, так как обработка будет выполнена в ExceptionHandlerExceptionResolver
.
Важное замечание. У меня не получилось перехватить здесь ни одно Spring исключение, например MethodArgumentTypeMismatchException
, которое возникает если передавать неверный тип для аргументов @RequestParam
.
Этот способ был показан больше для образовательных целей, чтобы показать в общих чертах, как работает этот механизм. Не стоит использовать этот способ, так как есть вариант намного удобнее.
@RestControllerAdvice
Исключения возникают в разных сервисах приложения, но удобнее всего обрабатывать все исключения в каком-то одном месте. Именно для этого в SpringBoot предназначены аннотации @ControllerAdvice
и @RestControllerAdvice
. В статье мы рассмотрим @RestControllerAdvice
, так как у нас REST API.
На самом деле все довольно просто. Мы берем методы помеченные аннотацией @ExceptionHandler
, которые у нас были в контроллерах и переносим в отдельный класс аннотированный @RestControllerAdvice
.
package dev.struchkov.example.controlleradvice.controller;
import dev.struchkov.example.controlleradvice.domain.ErrorMessage;
import dev.struchkov.example.controlleradvice.exception.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@RestControllerAdvice
public class ExceptionApiHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> notFoundException(NotFoundException exception) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorMessage> mismatchException(MethodArgumentTypeMismatchException exception) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
}
За обработку этих методов класса точно также отвечает класс ExceptionHandlerExceptionResolver
. При этом мы можем здесь перехватывать даже стандартные исключения Spring, такие как MethodArgumentTypeMismatchException
.
На мой взгляд, это самый удобный и простой способ обработки возвращаемых пользователю исключений.
Еще про обработку
Все написанное дальше относится к любому способу обработки исключений.
Запись в лог
Важно отметить, что исключения больше не записываются в лог. Если помимо ответа пользователю, вам все же необходимо записать это событие в лог, то необходимо добавить строчку записи в методе обработчике, например вот так:
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorMessage> handleException(NotFoundException exception) {
log.error(exception.getMessage(), exception);
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorMessage(exception.getMessage()));
}
Перекрытие исключений
Вы можете использовать иерархию исключений с наследованием и обработчики исключений для всей своей иерархии. В таком случае обработка исключения будет попадать в самый специализированный обработчик.
Допустим мы бросаем NotFoundException
, как в примере выше, который наследуется от RuntimeException
. И у вас будет два обработчика исключений для NotFoundException
и RuntimeException
. Исключение попадет в обработчик для NotFoundException
. Если этот обработчик убрать, то попадет в обработчик для RuntimeException
.
Резюмирую
Обработка исключений это важная часть REST API. Она позволяет возвращать клиентам информационные сообщения, которые помогут им скорректировать свой запрос.
Мы можем по разному реализовать обработку в зависимости от нашей архитектуры. Предпочитаемым способом считаю вариант с @RestControllerAdvice
. Этот вариант самый чистый и понятный.
В части 1 мы рассмотрели варианты обработки исключений, выбрасываемых в контроллере.
Самый гибкий из них — @ControllerAdvice — он позволяет изменить как код, так и тело стандартного ответа при ошибке. Кроме того, он позволяет в одном методе обработать сразу несколько исключений — они перечисляются над методом.
В первой части мы создавали @ControllerAdvice с нуля, но в Spring Boot существует заготовка — ResponseEntityExceptionHandler, которую можно расширить. В ней уже обработаны многие исключения, например: NoHandlerFoundException, HttpMessageNotReadableException, MethodArgumentNotValidException и другие (всего десяток-другой исключений).
Приложение
Обрабатывать исключения будем в простом Spring Boot приложении из первой части. Оно предоставляет REST API для сущности Person:
@Entity @NoArgsConstructor @AllArgsConstructor @Data public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Size(min = 3, max = 10) private String name; }
Только в этот раз поле name аннотировано javax.validation.constraints.Size.
А также перед аргументом Person в методах контроллера стоит аннотация @Valid:
@RestController @RequestMapping("/persons") public class PersonController { @Autowired private PersonRepository personRepository; @GetMapping public List<Person> listAllPersons() { List<Person> persons = personRepository.findAll(); return persons; } @GetMapping(value = "/{personId}") public Person getPerson(@PathVariable("personId") long personId) { return personRepository.findById(personId).orElseThrow(() -> new MyEntityNotFoundException(personId)); } @PostMapping public Person createPerson(@RequestBody @Valid Person person) { return personRepository.save(person); } @PutMapping("/{id}") public Person updatePerson(@RequestBody @Valid Person person, @PathVariable long id) { Person oldPerson = personRepository.getOne(id); oldPerson.setName(person.getName()); return personRepository.save(oldPerson); } }
Аннотация @Valid заставляет Spring проверять валидность полей объекта Person, например условие @Size(min = 3, max = 10). Если пришедший в контроллер объект не соответствует условиям, то будет выброшено MethodArgumentNotValidException — то самое, для которого в ResponseEntityExceptionHandler уже задан обработчик. Правда, он выдает пустое тело ответа. Вообще все обработчики из ResponseEntityExceptionHandler выдают корректный код ответа, но пустое тело.
Мы это исправим. Поскольку для MethodArgumentNotValidException может возникнуть несколько ошибок (по одной для каждого поля сущности Person), добавим в наше пользовательское тело ответа список List с ошибками. Он предназначен именно для MethodArgumentNotValidException (не для других исключений).
Итак, ApiError по сравнению с 1-ой частью теперь содержит еще список errors:
@Data @AllArgsConstructor @NoArgsConstructor public class ApiError { private String message; private String debugMessage; @JsonInclude(JsonInclude.Include.NON_NULL) private List<String> errors; public ApiError(String message, String debugMessage){ this.message=message; this.debugMessage=debugMessage; } }
Благодаря аннотации @JsonInclude(JsonInclude.Include.NON_NULL) этот список будет включен в ответ только в том случае, если мы его зададим. Иначе ответ будет содержать только message и debugMessage, как в первой части.
Класс обработки исключений
Например, на исключение MyEntityNotFoundException ответ не поменяется, обработчик такой же, как в первой части:
@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { ... @ExceptionHandler({MyEntityNotFoundException.class, EntityNotFoundException.class}) protected ResponseEntity<Object> handleEntityNotFoundEx(MyEntityNotFoundException ex, WebRequest request) { ApiError apiError = new ApiError("Entity Not Found Exception", ex.getMessage()); return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND); } ... }
Но в отличие от 1 части, теперь RestExceptionHandler расширяет ResponseEntityExceptionHandler. А значит, он наследует различные обработчики исключений, и мы их можем переопределить. Сейчас они все возвращают пустое тело ответа, хотя и корректный код.
HttpMessageNotReadableException
Переопределим обработчик, отвечающий за HttpMessageNotReadableException. Это исключение возникает тогда, когда тело запроса, приходящего в метод контроллер, нечитаемое — например, некорректный JSON.
За это исключение отвечает метод handleHttpMessageNotReadable(), его и переопределим:
@Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { ApiError apiError = new ApiError("Malformed JSON Request", ex.getMessage()); return new ResponseEntity(apiError, status); }
Проверим ответ, сделав запрос с некорректным JSON-телом запроса (он пойдет в метод updatePerson() контроллера):
PUT localhost:8080/persons/1 { 11"name": "alice" }
Получаем ответ с кодом 400 (Bad Request) и телом:
{ "message": "Malformed JSON Request", "debugMessage": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field namen at [Source: (PushbackInputStream); line: 2, column: 5]" }
Теперь ответ содержит не только корректный код, но и тело с информативными сообщениями. Если бы мы не переопределяли обработчик, вернулся бы только код 400.
А если бы не расширяли класс ResponseEntityExceptionHandler, все эти обработчики в принципе не были бы задействованы и вернулся бы стандартный ответ из BasicErrorController:
{ "timestamp": "2021-03-01T16:53:04.197+00:00", "status": 400, "error": "Bad Request", "message": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field namen at [Source: (PushbackInputStream); line: 2, column: 5]", "path": "/persons/1" }
MethodArgumentNotValidException
Как говорилось выше, чтобы выбросилось это исключение, в контроллер должен прийти некорректный Person. В смысле корректный JSON, но условие @Valid чтоб не выполнялось: например, поле name имело бы неверную длину (а она должна быть от 3 до 10, как указано в аннотации @Size).
Попробуем сделать запрос с коротким name:
POST http://localhost:8080/persons { "name": "al" }
Получим ответ:
{ "message": "Method Argument Not Valid", "debugMessage": "Validation failed for argument [0] in public ru.sysout.model.Person ru.sysout.controller.PersonController.createPerson(ru.sysout.model.Person): [Field error in object 'person' on field 'name': rejected value [al]; codes [Size.person.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name],10,3]; default message [размер должен находиться в диапазоне от 3 до 10]] ", "errors": [ "размер должен находиться в диапазоне от 3 до 10" ] }
Тут пошел в ход список ошибок, который мы добавили в ApiError. Мы его заполняем в переопределенном обработчике исключения:
@Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(x -> x.getDefaultMessage()) .collect(Collectors.toList()); ApiError apiError = new ApiError("Method Argument Not Valid", ex.getMessage(), errors); return new ResponseEntity<>(apiError, status); }
Вообще говоря, стандартный ответ, выдаваемый BasicErrorController, тоже будет содержать этот список ошибок по полям, если в application.properties включить свойство:
server.error.include-binding-errors=always
В этом случае (при отсутствии нашего RestExceptionHandler с @ControlleAdvice) ответ будет таким:
{ "timestamp": "2021-03-01T17:15:37.134+00:00", "status": 400, "error": "Bad Request", "message": "Validation failed for object='person'. Error count: 1", "errors": [ { "codes": [ "Size.person.name", "Size.name", "Size.java.lang.String", "Size" ], "arguments": [ { "codes": [ "person.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" }, 10, 3 ], "defaultMessage": "размер должен находиться в диапазоне от 3 до 10", "objectName": "person", "field": "name", "rejectedValue": "al", "bindingFailure": false, "code": "Size" } ], "path": "/persons/" }
Мы просто сократили информацию.
MethodArgumentTypeMismatchException
Полезно знать еще исключение MethodArgumentTypeMismatchException, оно возникает, если тип аргумента неверный. Например, наш метод контроллера получает Person по id:
@GetMapping(value = "/{personId}") public Person getPerson(@PathVariable("personId") Long personId) throws EntityNotFoundException { return personRepository.getOne(personId); }
А мы передаем не целое, а строковое значение id:
GET http://localhost:8080/persons/mn
Тут то и возникает исключение MethodArgumentTypeMismatchException. Давайте его обработаем:
@ExceptionHandler(MethodArgumentTypeMismatchException.class) protected ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex,HttpStatus status, WebRequest request) { ApiError apiError = new ApiError(); apiError.setMessage(String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName())); apiError.setDebugMessage(ex.getMessage()); return new ResponseEntity<>(apiError, status); }
Проверим ответ сервера (код ответа будет 400):
{ "message": "The parameter 'personId' of value 'mn' could not be converted to type 'long'", "debugMessage": "Failed to convert value of type 'java.lang.String' to required type 'long'; nested exception is java.lang.NumberFormatException: For input string: "mn"" }
NoHandlerFoundException
Еще одно полезное исключение — NoHandlerFoundException. Оно возникает, если на данный запрос не найдено обработчика.
Например, сделаем запрос:
GET http://localhost:8080/pers
По данному адресу у нас нет контроллера, так что возникнет NoHandlerFoundException. Добавим обработку исключения:
@Override protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>(new ApiError("No Handler Found", ex.getMessage()), status); }
Только учтите, для того, чтобы исключение выбрасывалось, надо задать свойства в файле application.properties:
spring.mvc.throw-exception-if-no-handler-found=true spring.web.resources.add-mappings=false
Проверим ответ сервера (код ответа 404):
{ "message": "No Handler Found", "debugMessage": "No handler found for GET /pers" }
Если же не выбрасывать NoHandlerFoundException и не пользоваться нашим обработчиком, то ответ от BasicErrorController довольно непонятный, хотя код тоже 404:
{ "timestamp": "2021-03-01T17:35:59.204+00:00", "status": 404, "error": "Not Found", "message": "No message available", "path": "/pers" }
Обработчик по умолчанию
Этот обработчик будет ловить исключения, не пойманные предыдущими обработчиками:
@ExceptionHandler(Exception.class) protected ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) { ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "prosto exception", ex); return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR); }
Заключение
Мы рассмотрели:
- как сделать обработку исключений в едином классе, аннотированном @ControllerAdvice;
- как переопределить формат JSON-ответа, выдаваемого при возникновении исключения;
- как воспользоваться классом-заготовкой ResponseEntityExceptionHandler и переопределить его обработчики так, чтобы тело ответов не было пустым;
Обратите внимание, что все не переопределенные методы ResponseEntityExceptionHandler будут выдавать пустое тело ответа.
Код примера доступен на GitHub.