I use @ExceptionHandler
to handle exceptions thrown by my web app, in my case my app returns JSON
response with HTTP status
for error responses to the client.
However, I am trying to figure out how to handle error 404
to return a similar JSON response like with the one handled by @ExceptionHandler
Update:
I mean, when a URL that does not exist is accessed
asked Nov 13, 2012 at 6:45
quarksquarks
33.1k70 gold badges282 silver badges508 bronze badges
I use spring 4.0 and java configuration. My working code is:
@ControllerAdvice
public class MyExceptionController {
@ExceptionHandler(NoHandlerFoundException.class)
public ModelAndView handleError404(HttpServletRequest request, Exception e) {
ModelAndView mav = new ModelAndView("/404");
mav.addObject("exception", e);
//mav.addObject("errorcode", "404");
return mav;
}
}
In JSP:
<div class="http-error-container">
<h1>HTTP Status 404 - Page Not Found</h1>
<p class="message-text">The page you requested is not available. You might try returning to the <a href="<c:url value="/"/>">home page</a>.</p>
</div>
For Init param config:
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
public void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}
}
Or via xml:
<servlet>
<servlet-name>rest-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
See Also: Spring MVC Spring Security and Error Handling
answered Dec 1, 2014 at 8:07
5
With spring > 3.0 use @ResponseStatus
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
...
}
@Controller
public class MyController {
@RequestMapping.....
public void handleCall() {
if (isFound()) {
// do some stuff
}
else {
throw new ResourceNotFoundException();
}
}
}
answered Nov 13, 2012 at 6:52
Yves_TYves_T
1,1702 gold badges10 silver badges16 bronze badges
4
Simplest way to find out is use the following:
@ExceptionHandler(Throwable.class)
public String handleAnyException(Throwable ex, HttpServletRequest request) {
return ClassUtils.getShortName(ex.getClass());
}
If the URL is within the scope of DispatcherServlet then any 404 caused by mistyping or anything else will be caught by this method but if the URL typed is beyond the URL mapping of the DispatcherServlet then you have to either use:
<error-page>
<exception-type>404</exception-type>
<location>/404error.html</location>
</error-page>
or
Provide «/» mapping to your DispatcherServlet mapping URL so as to handle all the mappings for the particular server instance.
answered Nov 13, 2012 at 11:23
LiamLiam
2,83723 silver badges36 bronze badges
3
public final class ResourceNotFoundException extends RuntimeException {
}
@ControllerAdvice
public class AppExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound() {
return "404";
}
}
Just define an Exception, an ExceptionHandler, throw the Exception from your business code controller.
answered Oct 12, 2017 at 7:51
igonejackigonejack
2,34620 silver badges28 bronze badges
1
You can use servlet standard way to handle 404 error. Add following code in web.xml
<error-page>
<exception-type>404</exception-type>
<location>/404error.html</location>
</error-page>
answered Nov 13, 2012 at 9:28
Fu ChengFu Cheng
3,3851 gold badge21 silver badges24 bronze badges
Время на прочтение
9 мин
Количество просмотров 64K
Часто на практике возникает необходимость централизованной обработки исключений в рамках контроллера или даже всего приложения. В данной статье разберём основные возможности, которые предоставляет Spring Framework для решения этой задачи и на простых примерах посмотрим как всё работает. Кому интересна данная тема — добро пожаловать под кат!
Изначально до Spring 3.2 основными способами обработки исключений в приложении были HandlerExceptionResolver и аннотация @ExceptionHandler. Их мы ещё подробно разберём ниже, но они имеют определённые недостатки. Начиная с версии 3.2 появилась аннотация @ControllerAdvice, в которой устранены ограничения из предыдущих решений. А в Spring 5 добавился новый класс ResponseStatusException, который очень удобен для обработки базовых ошибок для REST API.
А теперь обо всём по порядку, поехали!
Обработка исключений на уровне контроллера — @ExceptionHandler
С помощью аннотации @ExceptionHandler можно обрабатывать исключения на уровне отдельного контроллера. Для этого достаточно объявить метод, в котором будет содержаться вся логика обработки нужного исключения, и проаннотировать его.
В качестве примера разберём простой контроллер:
@RestController
public class Example1Controller {
@GetMapping(value = "/testExceptionHandler", produces = APPLICATION_JSON_VALUE)
public Response testExceptionHandler(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws BusinessException {
if (exception) {
throw new BusinessException("BusinessException in testExceptionHandler");
}
return new Response("OK");
}
@ExceptionHandler(BusinessException.class)
public Response handleException(BusinessException e) {
return new Response(e.getMessage());
}
}
Тут я сделал метод testExceptionHandler, который вернёт либо исключение BusinessException, либо успешный ответ — всё зависит от того что было передано в параметре запроса. Это нужно для того, чтобы можно было имитировать как штатную работу приложения, так и работу с ошибкой.
А вот следующий метод handleException предназначен уже для обработки ошибок. У него есть аннотация @ExceptionHandler(BusinessException.class), которая говорит нам о том что для последующей обработки будут перехвачены все исключения типа BusinessException. В аннотации @ExceptionHandler можно прописать сразу несколько типов исключений, например так: @ExceptionHandler({BusinessException.class, ServiceException.class}).
Сама обработка исключения в данном случае примитивная и сделана просто для демонстрации работы метода — по сути вернётся код 200 и JSON с описанием ошибки. На практике часто требуется более сложная логика обработки и если нужно вернуть другой код статуса, то можно воспользоваться дополнительно аннотацией @ResponseStatus, например @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR).
Пример работы с ошибкой:
Пример штатной работы:
Основной недостаток @ExceptionHandler в том что он определяется для каждого контроллера отдельно, а не глобально для всего приложения. Это ограничение можно обойти если @ExceptionHandler определен в базовом классе, от которого будут наследоваться все контроллеры в приложении, но такой подход не всегда возможен, особенно если перед нами старое приложение с большим количеством легаси.
Обработка исключений с помощью HandlerExceptionResolver
HandlerExceptionResolver является общим интерфейсом для обработчиков исключений в Spring. Все исключений выброшенные в приложении будут обработаны одним из подклассов HandlerExceptionResolver. Можно сделать как свою собственную реализацию данного интерфейса, так и использовать существующие реализации, которые предоставляет нам Spring из коробки. Давайте разберем их для начала:
ExceptionHandlerExceptionResolver — этот резолвер является частью механизма обработки исключений с помощью аннотации @ExceptionHandler, о которой я уже упоминал ранее.
DefaultHandlerExceptionResolver — используется для обработки стандартных исключений Spring и устанавливает соответствующий код ответа, в зависимости от типа исключения:
Основной недостаток заключается в том что возвращается только код статуса, а на практике для REST API одного кода часто не достаточно. Желательно вернуть клиенту еще и тело ответа с описанием того что произошло. Эту проблему можно решить с помощью ModelAndView, но не нужно, так как есть способ лучше.
ResponseStatusExceptionResolver — позволяет настроить код ответа для любого исключения с помощью аннотации @ResponseStatus.
В качестве примера я создал новый класс исключения ServiceException:
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public class ServiceException extends Exception {
public ServiceException(String message) {
super(message);
}
}
В ServiceException я добавил аннотацию @ResponseStatus и в value указал что данное исключение будет соответствовать статусу INTERNAL_SERVER_ERROR, то есть будет возвращаться статус-код 500.
Для тестирования данного нового исключения я создал простой контроллер:
@RestController
public class Example2Controller {
@GetMapping(value = "/testResponseStatusExceptionResolver", produces = APPLICATION_JSON_VALUE)
public Response testResponseStatusExceptionResolver(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws ServiceException {
if (exception) {
throw new ServiceException("ServiceException in testResponseStatusExceptionResolver");
}
return new Response("OK");
}
}
Если отправить GET-запрос и передать параметр exception=true, то приложение в ответ вернёт 500-ю ошибку:
Из недостатков такого подхода — как и в предыдущем случае отсутствует тело ответа. Но если нужно вернуть только код статуса, то @ResponseStatus довольно удобная штука.
Кастомный HandlerExceptionResolver позволит решить проблему из предыдущих примеров, наконец-то можно вернуть клиенту красивый JSON или XML с необходимой информацией. Но не спешите радоваться, давайте для начала посмотрим на реализацию.
В качестве примера я сделал кастомный резолвер:
@Component
public class CustomExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
if (ex instanceof CustomException) {
modelAndView.setStatus(HttpStatus.BAD_REQUEST);
modelAndView.addObject("message", "CustomException was handled");
return modelAndView;
}
modelAndView.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
modelAndView.addObject("message", "Another exception was handled");
return modelAndView;
}
}
Ну так себе, прямо скажем. Код конечно работает, но приходится выполнять всю работу руками: сами проверяем тип исключения, и сами формируем объект древнего класса ModelAndView. На выходе конечно получим красивый JSON, но в коде красоты явно не хватает.
Такой резолвер может глобально перехватывать и обрабатывать любые типы исключений и возвращать как статус-код, так и тело ответа. Формально он даёт нам много возможностей и не имеет недостатков из предыдущих примеров. Но есть способ сделать ещё лучше, к которому мы перейдем чуть позже. А сейчас, чтобы убедиться что всё работает — напишем простой контроллер:
@RestController
public class Example3Controller {
@GetMapping(value = "/testCustomExceptionResolver", produces = APPLICATION_JSON_VALUE)
public Response testCustomExceptionResolver(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws CustomException {
if (exception) {
throw new CustomException("CustomException in testCustomExceptionResolver");
}
return new Response("OK");
}
}
А вот и пример вызова:
Видим что исключение прекрасно обработалось и в ответ получили код 400 и JSON с сообщением об ошибке.
Обработка исключений с помощью @ControllerAdvice
Наконец переходим к самому интересному варианту обработки исключений — эдвайсы. Начиная со Spring 3.2 можно глобально и централизованно обрабатывать исключения с помощью классов с аннотацией @ControllerAdvice.
Разберём простой пример эдвайса для нашего приложения:
@ControllerAdvice
public class DefaultAdvice {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Response> handleException(BusinessException e) {
Response response = new Response(e.getMessage());
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
Как вы уже догадались, любой класс с аннотацией @ControllerAdvice является глобальным обработчиком исключений, который очень гибко настраивается.
В нашем случае мы создали класс DefaultAdvice с одним единственным методом handleException. Метод handleException имеет аннотацию @ExceptionHandler, в которой, как вы уже знаете, можно определить список обрабатываемых исключений. В нашем случае будем перехватывать все исключения BusinessException.
Можно одним методом обрабатывать и несколько исключений сразу: @ExceptionHandler({BusinessException.class, ServiceException.class}). Так же можно в рамках эдвайса сделать сразу несколько методов с аннотациями @ExceptionHandler для обработки разных исключений.
Обратите внимание, что метод handleException возвращает ResponseEntity с нашим собственным типом Response:
public class Response {
private String message;
public Response() {
}
public Response(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Таким образом у нас есть возможность вернуть клиенту как код статуса, так и JSON заданной структуры. В нашем простом примере я записываю в поле message описание ошибки и возвращаю HttpStatus.OK, что соответствует коду 200.
Для проверки работы эдвайса я сделал простой контроллер:
@RestController
public class Example4Controller {
@GetMapping(value = "/testDefaultControllerAdvice", produces = APPLICATION_JSON_VALUE)
public Response testDefaultControllerAdvice(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws BusinessException {
if (exception) {
throw new BusinessException("BusinessException in testDefaultControllerAdvice");
}
return new Response("OK");
}
}
В результате, как и ожидалось, получаем красивый JSON и код 200:
А что если мы хотим обрабатывать исключения только от определенных контроллеров?
Такая возможность тоже есть! Смотрим следующий пример:
@ControllerAdvice(annotations = CustomExceptionHandler.class)
public class CustomAdvice {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Response> handleException(BusinessException e) {
String message = String.format("%s %s", LocalDateTime.now(), e.getMessage());
Response response = new Response(message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
Обратите внимание на аннотацию @ControllerAdvice(annotations = CustomExceptionHandler.class). Такая запись означает что CustomAdvice будет обрабатывать исключения только от тех контроллеров, которые дополнительно имеют аннотацию @CustomExceptionHandler.
Аннотацию @CustomExceptionHandler я специально сделал для данного примера:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExceptionHandler {
}
А вот и исходный код контроллера:
@RestController
@CustomExceptionHandler
public class Example5Controller {
@GetMapping(value = "/testCustomControllerAdvice", produces = APPLICATION_JSON_VALUE)
public Response testCustomControllerAdvice(@RequestParam(required = false, defaultValue = "false") boolean exception)
throws BusinessException {
if (exception) {
throw new BusinessException("BusinessException in testCustomControllerAdvice");
}
return new Response("OK");
}
}
В контроллере Example5Controller присутствует аннотация @CustomExceptionHandler, а так же на то что выбрасывается то же исключение что и в Example4Controller из предыдущего примера. Однако в данном случае исключение BusinessException обработает именно CustomAdvice, а не DefaultAdvice, в чём мы легко можем убедиться.
Для наглядности я немного изменил сообщение об ошибке в CustomAdvice — начал добавлять к нему дату:
На этом возможности эдвайсов не заканчиваются. Если мы посмотрим исходный код аннотации @ControllerAdvice, то увидим что эдвайс можно повесить на отдельные типы или даже пакеты. Не обязательно создавать новые аннотации или вешать его на уже существующие.
Исключение ResponseStatusException.
Сейчас речь пойдёт о формировании ответа путём выброса исключения ResponseStatusException:
@RestController
public class Example6Controller {
@GetMapping(value = "/testResponseStatusException", produces = APPLICATION_JSON_VALUE)
public Response testResponseStatusException(@RequestParam(required = false, defaultValue = "false") boolean exception) {
if (exception) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ResponseStatusException in testResponseStatusException");
}
return new Response("OK");
}
}
Выбрасывая ResponseStatusException можно также возвращать пользователю определённый код статуса, в зависимости от того, что произошло в логике приложения. При этом не нужно создавать кастомное исключение и прописывать аннотацию @ResponseStatus — просто выбрасываем исключение и передаём нужный статус-код. Конечно тут возвращаемся к проблеме отсутствия тела сообщения, но в простых случаях такой подход может быть удобен.
Пример вызова:
Резюме: мы познакомились с разными способами обработки исключений, каждый из которых имеет свои особенности. В рамках большого приложения можно встретить сразу несколько подходов, но при этом нужно быть очень осторожным и стараться не переусложнять логику обработки ошибок. Иначе получится что какое-нибудь исключение обработается не в том обработчике и на выходе ответ будет отличаться от ожидаемого. Например если в приложении есть несколько эдвайсов, то при создании нового нужно убедиться, что он не сломает существующий порядок обработки исключений из старых контроллеров.
Так что будьте внимательны и всё будет работать замечательно!
Ссылка на исходники из статьи
Spring Boot provides pretty nifty defaults to handle exceptions and formulate a helpful response in case anything goes wrong. Still, for any number of reasons, an exception can be thrown at runtime and the consumers of your API may get a garbled exception message (or worse, no message at all) with a 500 Internal Server Error
response.
Such a scenario is undesirable, because of the
- usability concerns Although relevant, the default exception message may not be helpful to the consumers of your API.
- security concerns The exception message may expose the internal details of your application to anyone using the API.
This is a pretty common occurrence and customizing the error response so that it is easy to comprehend is often one of the requirements of the API design. Like many other niceties, Spring Boot does a lot of heavy lifting for you; it does not send binding errors (due to validation failure), exceptions, or stacktrace in a response unless you configure them otherwise (see server.error
keys under Server Properties available for a Spring application).
In this post, we’ll explore some of the ways to customize error responses returned by a REST API. We’ll also cover some usecases when Spring Security comes into the picture.
The code written for this post uses:
- Java 14
- Spring Boot 2.3.2
- Postgres 13
- Maven 3.6.3
You can launch an instance of Postgres with Docker using the following Compose
file.
version: '3'
services:
db:
image: postgres:13-alpine
restart: always
ports:
- 5432:5432
environment:
POSTGRES_USER: erin
POSTGRES_PASSWORD: richards
Execute the following command to launch the container.
Configure the project
Generate a Spring Boot project with Spring Initializr, and add spring-boot-starter-web
, spring-boot-starter-data-jdbc
, and postgresql
as dependencies.
Your pom.xml
would look like this.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>dev.mflash.guides</groupId>
<artifactId>spring-rest-error-handling</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>14</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Rename application.properties
to application.yml
, open the file, and add the following database configuration.
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/spring
username: erin
password: richards
Create an API
Say, you want to save a Book
object, described by the following entity.
// src/main/java/dev/mflash/guides/resterror/domain/Book.java
public class Book {
private @Id long id;
private String title;
private String author;
private Genre genre;
// getters, setters, etc.
}
The id
will be of type SERIAL
in Postgres which will be automatically incremented by the database.
Create the required table using the following SQL statement.
CREATE TABLE book (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
genre TEXT NOT NULL
);
Note that the genre
field is backed by an enum described as follows.
public enum Genre {
fantasy,
thriller,
scifi
}
Define a BookRepository
to perform database operations.
// src/main/java/dev/mflash/guides/resterror/persistence/BookRepository.java
public interface BookRepository extends CrudRepository<Book, Long> {
}
Create a BookController
to expose some endpoints.
// src/main/java/dev/mflash/guides/resterror/controller/BookController.java
@RequestMapping("/book")
public @RestController class BookController {
private final BookRepository repository;
public BookController(BookRepository repository) {
this.repository = repository;
}
@PutMapping
public Map<String, String> addBook(Book newBook) {
repository.save(newBook);
return Map.of("message", String.format("Save successful for %s", newBook.getTitle()));
}
@GetMapping
public List<Book> getAllBooks() {
List<Book> allBooks = List.of();
repository.findAll().forEach(allBooks::add);
return allBooks;
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable long id) {
Optional<Book> book = repository.findById(id);
if (book.isEmpty()) {
throw new DataAccessResourceFailureException(String.format("No resource found for id (%s)", id));
}
return book.get();
}
@PatchMapping("/{id}")
public Map<String, String> editBook(@PathVariable long id, Book editedBook) {
final Optional<Book> saved = repository.findById(id);
if (saved.isPresent()) {
final Book savedBook = saved.get();
final Book patchedBook = new Book();
patchedBook.setId(savedBook.getId());
patchedBook.setTitle(
!editedBook.getTitle().equals(savedBook.getTitle()) ? editedBook.getTitle() : savedBook.getTitle());
patchedBook.setAuthor(
!editedBook.getAuthor().equals(savedBook.getAuthor()) ? editedBook.getAuthor() : savedBook.getAuthor());
patchedBook.setGenre(
!editedBook.getGenre().equals(savedBook.getGenre()) ? editedBook.getGenre() : savedBook.getGenre());
repository.save(patchedBook);
} else {
throw new InvalidDataAccessApiUsageException("Couldn't patch unrelated or non-existent records");
}
return Map.of("message", String.format("Patch successful for %s", editedBook.getTitle()));
}
@DeleteMapping("/{id}")
public Map<String, String> deleteBook(@PathVariable long id) {
if (repository.deleteById(id) < 1) {
throw new InvalidDataAccessApiUsageException("Couldn't delete a non-existent record");
}
return Map.of("message", String.format("Deletion successful for book with id: %s", id));
}
}
Launch the application and try to access a non-existent book.
$ curl --location --request GET 'http://localhost:8080/book/0'
{
"timestamp": "2020-07-25T15:54:09.567+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/book/0"
}
The operation failed, obviously, and you received a JSON response with some useful fields. These fields are managed by DefaultErrorAttributes
class and the response is formed by an implementation of HandlerExceptionResolver
. However, we can already see what is amiss here.
- There is no message clarifying what exactly went wrong.
- It is the client that passed the incorrect id but the status code indicates it is a server error.
- Since the record was not found, a
404 Not found
would’ve been the accurate status.
Send the correct status using ResponseStatusException
The fastest way to address the issue with status is to throw Spring provided ResponseStatusException
which accepts an HttpStatus
.
// src/main/java/dev/mflash/guides/resterror/controller/BookController.java
@RequestMapping("/book")
public @RestController class BookController {
// Rest of the controller
@GetMapping("/{id}")
public Book getBookById(@PathVariable long id) {
Optional<Book> book = repository.findById(id);
if (book.isEmpty()) {
throw new ResponseStatusException(NOT_FOUND, String.format("No resource found for id (%s)", id));
}
return book.get();
}
@PatchMapping("/{id}")
public Map<String, String> editBook(@PathVariable long id, Book editedBook) {
final Optional<Book> saved = repository.findById(id);
if (saved.isPresent()) {
// logic for patch
} else {
throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Couldn't patch unrelated or non-existent records");
}
return Map.of("message", String.format("Patch successful for %s", editedBook.getTitle()));
}
@DeleteMapping("/{id}")
public Map<String, String> deleteBook(@PathVariable long id) {
if (repository.deleteById(id) < 1) {
throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Couldn't delete a non-existent record");
}
return Map.of("message", String.format("Deletion successful for book with id: %s", id));
}
}
Launch the application and try the request again.
$ curl --location --request GET 'http://localhost:8080/book/0'
{
"timestamp": "2020-07-26T07:07:30.313+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/book/0"
}
We are getting the correct status now but it’d be useful to let the client know the cause of this issue. Also, it’d be useful to throw relevant custom exceptions instead of the same exception everywhere.
Exception handling with @ControllerAdvice
and @ExceptionHandler
If you examine ResponseStatusException
, you’d notice that it saves the message in a variable reason
. We’d prefer to map this to the message
key in the response above. Let’s create a class RestResponse
that can hold this response.
// src/main/java/dev/mflash/guides/resterror/exception/RestResponse.java
public class RestResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private int status;
private String error;
private String message;
private String path;
// getters, setters, etc.
public static RestResponseBuilder builder() {
return new RestResponseBuilder();
}
}
Let’s also create a RestResponseBuilder
that can provide a fluent API to create RestResponse
objects from a variety of inputs.
// src/main/java/dev/mflash/guides/resterror/exception/RestResponseBuilder.java
public class RestResponseBuilder {
private int status;
private String error;
private String message;
private String path;
public RestResponseBuilder status(int status) {
this.status = status;
return this;
}
public RestResponseBuilder status(HttpStatus status) {
this.status = status.value();
if (status.isError()) {
this.error = status.getReasonPhrase();
}
return this;
}
public RestResponseBuilder error(String error) {
this.error = error;
return this;
}
public RestResponseBuilder exception(ResponseStatusException exception) {
HttpStatus status = exception.getStatus();
this.status = status.value();
if (!Objects.requireNonNull(exception.getReason()).isBlank()) {
this.message = exception.getReason();
}
if (status.isError()) {
this.error = status.getReasonPhrase();
}
return this;
}
public RestResponseBuilder message(String message) {
this.message = message;
return this;
}
public RestResponseBuilder path(String path) {
this.path = path;
return this;
}
public RestResponse build() {
RestResponse response = new RestResponse();
response.setStatus(status);
response.setError(error);
response.setMessage(message);
response.setPath(path);
return response;
}
public ResponseEntity<RestResponse> entity() {
return ResponseEntity.status(status).headers(HttpHeaders.EMPTY).body(build());
}
}
We can configure a @ControllerAdvice
that can trigger methods to handle ResponseStatusException
. This method has to be annotated by @ExceptionHandler
which specifies what type of exceptions a method can handle.
// src/main/java/dev/mflash/guides/resterror/exception/RestErrorHandler.java
@RestControllerAdvice
public class RestErrorHandler {
private static final Logger logger = LoggerFactory.getLogger(RestErrorHandler.class);
@ExceptionHandler(ResponseStatusException.class)
ResponseEntity<?> handleStatusException(ResponseStatusException ex, WebRequest request) {
logger.error(ex.getReason(), ex);
return RestResponse.builder()
.exception(ex)
.path(request.getDescription(false).substring(4))
.entity();
}
}
In the above codeblock, handleStatusException
will be invoked whenever a ResponseStatusException
is thrown and instead of the boilerplate Spring response, an instance of RestResponse
would be returned with the reason.
$ curl --location --request GET 'http://localhost:8080/book/0'
{
"timestamp": "2020-07-26T15:24:38.0644948",
"status": 404,
"error": "Not Found",
"message": "No resource found for id (0)",
"path": "/book/0"
}
Note that we’re also injecting a WebRequest
instance to get the path
. Besides path
, a WebRequest
can provide a whole lot of other details about the request and client. Also, you may want to log the exceptions in the handler method, else Spring will not print them on the logs.
At this point, you may continue to throw ResponseStatusException
throughout your application or you can choose to extend it to define custom exceptions with specific HttpStatus
. But what about exceptions that are thrown by a third-party?
Handling Exceptions thrown by a third-party
One approach is to rethrow such exceptions as ResponseStatusException
; this can be done wherever you encounter them. Another way is to write handler methods to intercept them in the @RestControllerAdvice
above. It makes things a bit cleaner, but you can’t handle every exception out there. To deal with this, you can write a generic exception handler that may handle Exception
class.
To get you started, Spring offers a ResponseEntityExceptionHandler
class that provides a huge number of handlers for the exceptions thrown by Spring. You can extend this class and implement your handlers on top of it. Even better, you can override the existing handlers to customize their behavior. Let’s modify RestErrorHandler
as follows.
// src/main/java/dev/mflash/guides/resterror/exception/RestErrorHandler.java
@RestControllerAdvice
public class RestErrorHandler extends ResponseEntityExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(RestErrorHandler.class);
@ExceptionHandler(ResponseStatusException.class)
ResponseEntity<?> handleStatusException(ResponseStatusException ex, WebRequest request) {
logger.error(ex.getReason(), ex);
return handleResponseStatusException(ex, request);
}
@ExceptionHandler(Exception.class)
ResponseEntity<?> handleAllExceptions(Exception ex, WebRequest request) {
logger.error(ex.getLocalizedMessage(), ex);
return handleEveryException(ex, request);
}
@SuppressWarnings("unchecked")
protected @Override ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
HttpStatus status, WebRequest request) {
ResponseEntity<?> responseEntity;
if (!status.isError()) {
responseEntity = handleStatusException(ex, status, request);
} else if (INTERNAL_SERVER_ERROR.equals(status)) {
request.setAttribute("javax.servlet.error.exception", ex, 0);
responseEntity = handleEveryException(ex, request);
} else {
responseEntity = handleEveryException(ex, request);
}
return (ResponseEntity<Object>) responseEntity;
}
protected ResponseEntity<RestResponse> handleResponseStatusException(ResponseStatusException ex, WebRequest request) {
return RestResponse.builder()
.exception(ex)
.path(getPath(request))
.entity();
}
protected ResponseEntity<RestResponse> handleStatusException(Exception ex, HttpStatus status, WebRequest request) {
return RestResponse.builder()
.status(status)
.message("Execution halted")
.path(getPath(request))
.entity();
}
protected ResponseEntity<RestResponse> handleEveryException(Exception ex, WebRequest request) {
return RestResponse.builder()
.status(INTERNAL_SERVER_ERROR)
.message("Server encountered an error")
.path(getPath(request))
.entity();
}
private String getPath(WebRequest request) {
return request.getDescription(false).substring(4);
}
}
A lot of things are going on here.
handleResponseStatusException
method specifically handlesResponseStatusException
handleStatusException
method handles exceptions when the status is not an error status (the statuses in 1xx, 2xx and 3xx series)handleEveryException
method handles all other exceptions and sets their status as500 Internal Server Error
- we’re also overriding
handleExceptionInternal
to translate the exceptions thrown by Spring to returnRestResponse
- finally, we’ve defined
handleAllExceptions
handler that serves as a catch-all. If no specific error handler is found for an exception, this method will be invoked.
To test this, launch the application and try to put a new book with genre
as kids
.
$ curl --location --request PUT 'http://localhost:8080/book'
--header 'Content-Type: application/json'
--data-raw '{
"title": "The Land of Roar",
"author": "Jenny McLachlan",
"genre": "kids"
}'
{
"timestamp": "2020-07-26T16:30:51.3444858",
"status": 500,
"error": "Internal Server Error",
"message": "Server encountered an error",
"path": "/book"
}
Since we’ve not configured any constant named kids
in the Genre
enum, Jackson will serialize the genre
field as null which would violate NOT NULL
constraint in the database. The application will throw a DataAccessException
as a result. Since there’s no handler defined for this exception in RestErrorHandler
class, the handleAllExceptions
handler method will be invoked, sending the response seen above.
Error Handling for Spring Security
What happens if you add Spring Security in our application? After adding the JWT-based authentication (from the post Securing Spring Boot APIs with JWT Authentication) in our application, try to hit any BookController
endpoint.
$ curl --location --request GET 'http://localhost:8080/book'
{
"timestamp": "2020-07-26T11:36:58.225+00:00",
"status": 403,
"error": "Forbidden",
"message": "",
"path": "/book"
}
You’d notice that the exception handler that we configured earlier is not being invoked and we’re getting the default response. This happens because our custom advice is invoked after Spring Security’s servlet filters have verified the user. Since the user authentication failed, the handlers were never invoked.
Handle Authentication failure with AuthenticationEntryPoint
AuthenticationEntryPoint
’s commence
method is called when an AuthenticationException
is thrown. You can implement this interface to return a customized response.
// src/main/java/dev/mflash/guides/resterror/security/CustomAuthenticationEntryPoint.java
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
public @Override void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
logger.error(e.getLocalizedMessage(), e);
String message = RestResponse.builder()
.status(UNAUTHORIZED)
.error("Unauthenticated")
.message("Insufficient authentication details")
.path(request.getRequestURI())
.json();
response.setStatus(UNAUTHORIZED.value());
response.setContentType(APPLICATION_JSON_VALUE);
response.getWriter().write(message);
}
}
You can be as generic or versatile in handling different types of AuthenticationException
s as you need. Configure this entrypoint in the security configuration as follows.
// src/main/java/dev/mflash/guides/resterror/security/SecurityConfiguration.java
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String LOGIN_URL = "/user/login";
private final CustomUserDetailsService userDetailsService;
public SecurityConfiguration(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected @Override void configure(HttpSecurity http) throws Exception {
http.cors().and()
.csrf().disable()
.authorizeRequests().antMatchers(POST, REGISTRATION_URL).permitAll()
.anyRequest().authenticated().and()
.addFilter(new CustomAuthenticationFilter(authenticationManager()))
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()).and()
.addFilter(new CustomAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(STATELESS);
}
// other configurations
}
To handle authorization failures, you can implement the AccessDeniedHandler
interface.
// src/main/java/dev/mflash/guides/resterror/security/CustomAccessDeniedHandler.java
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
public @Override void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
throws IOException {
logger.error(e.getLocalizedMessage(), e);
String message = RestResponse.builder()
.status(FORBIDDEN)
.message("Invalid Authorization token")
.path(request.getRequestURI())
.json();
response.setStatus(FORBIDDEN.value());
response.setContentType(APPLICATION_JSON_VALUE);
response.getWriter().write(message);
}
}
Similar to the AuthenticationEntryPoint
approach, you can handle different scenarios that can lead to authorization failure. Call the handle
method implemented above whenever such scenarios are encountered. An example is given below.
// src/main/java/dev/mflash/guides/resterror/security/CustomAuthorizationFilter.java
public class CustomAuthorizationFilter extends BasicAuthenticationFilter {
private final AccessDeniedHandler accessDeniedHandler;
public CustomAuthorizationFilter(AuthenticationManager authenticationManager, AccessDeniedHandler accessDeniedHandler) {
super(authenticationManager);
this.accessDeniedHandler = accessDeniedHandler;
}
protected @Override void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(AUTH_HEADER_KEY);
if (Objects.isNull(header) || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
if (header.startsWith(TOKEN_PREFIX)) {
try {
var authentication = getAuthentication(header);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
accessDeniedHandler.handle(request, response, new AccessDeniedException(e.getLocalizedMessage(), e));
}
}
}
private UsernamePasswordAuthenticationToken getAuthentication(String header) {
if (header.startsWith(TOKEN_PREFIX)) {
String username = parseToken(header);
return new UsernamePasswordAuthenticationToken(username, null, List.of());
} else {
throw new AccessDeniedException("Failed to parse authentication token");
}
}
}
For this to work, you’ll have to inject the CustomAccessDeniedHandler
in CustomAuthorizationFilter
through the security configuration, as follows.
// src/main/java/dev/mflash/guides/resterror/security/SecurityConfiguration.java
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String LOGIN_URL = "/user/login";
private final CustomUserDetailsService userDetailsService;
public SecurityConfiguration(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected @Override void configure(HttpSecurity http) throws Exception {
http.cors().and()
.csrf().disable()
.authorizeRequests().antMatchers(POST, REGISTRATION_URL).permitAll()
.anyRequest().authenticated().and()
.addFilter(new CustomAuthenticationFilter(authenticationManager()))
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()).and()
.addFilter(new CustomAuthorizationFilter(authenticationManager(), new CustomAccessDeniedHandler()))
.sessionManagement().sessionCreationPolicy(STATELESS);
}
// other configurations
}
Launch the application again and try accessing an endpoint without any authentication details.
$ curl --location --request GET 'http://localhost:8080/book'
{
"timestamp": "2020-07-26T20:38:10.166004300",
"status": 401,
"error": "Unauthenticated",
"message": "Insufficient authentication details",
"path": "/book"
}
A 401 Unauthenticated
error was sent, informing that authentication details were not sufficient for this request. Now, add an expired Bearer token in an Authorization
header and send the request again.
$ curl --location --request GET 'http://localhost:8080/book'
--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJlbWlseS5icm9udGVAZXhhbXBsZS5jb20iLCJleHAiOjE1OTY1NTI0MTZ9.utTGxu-CTJglYQku9GMZPsOl-J8rni363nBaGbodNiP7D0J66Znf-fZZ-Hz_iVCO7CHj_s4E6Xuw68HCwyTZig'
{
"timestamp": "2020-07-26T20:57:27.164265300",
"status": 403,
"error": "Forbidden",
"message": "Invalid Authorization token",
"path": "/book"
}
This time, a 403 Forbidden
error was sent indicating that even though the authentication was successful, the token was invalid. Generate a new token by sending a login request.
$ curl --location --request POST 'http://localhost:8080/login'
--header 'Content-Type: application/json'
--data-raw '{
"email": "arya.antrix@example.com",
"password": "pa55word"
}'
You’ll receive a response 200 OK
with an Authorization
header that contains a token.
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJlbWlseS5icm9udGVAZXhhbXBsZS5jb20iLCJleHAiOjE1OTY2NDE0MjJ9.xK9KvbdlPN_r_rJzwfaidYY2r83pvGsXgIw8LQokvMbVXCyF9fZnV1CgnVc1pjQeswFq8rOGhmgEdCHp7DbR8w
Using this token, try sending the request again.
$ curl --location --request GET 'http://localhost:8080/book'
--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJlbWlseS5icm9udGVAZXhhbXBsZS5jb20iLCJleHAiOjE1OTY2NDE0MjJ9.xK9KvbdlPN_r_rJzwfaidYY2r83pvGsXgIw8LQokvMbVXCyF9fZnV1CgnVc1pjQeswFq8rOGhmgEdCHp7DbR8w'
[
{
"id": 1,
"title": "Kill Orbit",
"author": "Joel Dane",
"genre": "scifi"
}
]
You’ll receive a 200 OK
with a list of books saved in the database, as expected.
Source code
- spring-rest-error-handling
Related
- Securing Spring Boot APIs with JWT Authentication
- Spring Security: Authentication and Authorization In-Depth
В этой статье — обзор способов обработки исключений в Spring Boot.
Приложение
Мы рассмотрим простое REST API приложение с одной сущностью Person и с одним контроллером.
Класс Person:
@Entity @NoArgsConstructor @AllArgsConstructor @Data public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; }
Контроллер PersonController:
@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 Person person) { return personRepository.save(person); } @PutMapping("/{id}") public Person updatePerson(@RequestBody Person person, @PathVariable long id) { Person oldPerson = personRepository.getOne(id); oldPerson.setName(person.getName()); return personRepository.save(oldPerson); } }
При старте приложения выполняется скрипт data.sql, который добавляет в базу данных H2 одну строку — Person c id=1. То есть Person c id=2 в базе отсутствует.
При попытке запросить Person c id=2:
GET localhost:8080/persons/2
метод контроллера getPerson() выбрасывает исключение — в данном случае наше пользовательское MyEntityNotFoundException:
public class MyEntityNotFoundException extends RuntimeException { public MyEntityNotFoundException(Long id) { super("Entity is not found, id="+id); } }
BasicErrorController
По умолчанию все исключения попадают на адрес /error в BasicErrorController, в метод error():
@Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { ... @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = this.getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity(status); } else { Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity(body, status); } } ... }
Если поставить в этом методе break point, то будет понятно, из каких атрибутов собирается ответное JSON сообщение.
Проверим ответ по умолчанию, запросив с помощью клиента Postman отсутствующий Person, чтобы выбросилось MyEntityNotFoundException:
GET localhost:8080/persons/2
Получим ответ:
{ "timestamp": "2021-02-28T15:33:56.339+00:00", "status": 500, "error": "Internal Server Error", "message": "Entity is not found, id=2", "path": "/persons/2" }
Причем для того, чтобы поле message было непустым, в application.properties нужно включить свойство:
server.error.include-message=always
В текущей версии Spring сообщение не включается по умолчанию из соображений безопасности.
Обратите внимание, что поле status JSON-тела ответа дублирует реальный http-код ответа. В Postman он виден:
Поле message заполняется полем message выброшенного исключения.
Независимо от того, какое исключение выбросилось: пользовательское или уже существующее, ответ стандартный — в том смысле, что набор полей одинаковый. Меняется только внутренняя часть и, возможно, код ответа (он не обязательно равен 500, некоторые существующие в Spring исключения подразумевают другой код).
Но структура ответа сохраняется.
Проверим это.
Не пользовательское исключение
Например, если изменить код, убрав пользовательское MyEntityNotFoundException, то при отсутствии Person исключение будет все равно выбрасываться, но другое:
@GetMapping(value = "/{personId}") public Person getPerson(@PathVariable("personId") long personId){ return personRepository.findById(personId).get(); }
findById() возвращает тип Optional, а Optional.get() выбрасывает исключение NoSuchElementException с другим сообщением:
java.util.NoSuchElementException: No value present
в итоге при запросе несуществующего Person:
GET localhost:8080/persons/2
ответ сохранит ту же структуру, но поменяется поле message:
{ "timestamp": "2021-02-28T15:44:20.065+00:00", "status": 500, "error": "Internal Server Error", "message": "No value present", "path": "/persons/2" }
Вернем обратно пользовательское исключение MyEntityNotFoundException.
Попробуем поменять ответ, выдаваемый в ответ за запрос. Статус 500 для него явно не подходит.
Рассмотрим способы изменения ответа.
@ResponseStatus
Пока поменяем только статус ответа. Сейчас возвращается 500, а нам нужен 404 — это логичный ответ, если ресурс не найден.
Для этого аннотируем наше исключение:
@ResponseStatus(HttpStatus.NOT_FOUND) public class MyEntityNotFoundException extends RuntimeException { public MyEntityNotFoundException(Long id) { super("Entity is not found, id="+id); } }
Теперь ответ будет таким:
{ "timestamp": "2021-02-28T15:54:37.070+00:00", "status": 404, "error": "Not Found", "message": "Entity is not found, id=2", "path": "/persons/2" }
Уже лучше.
@ControllerAdvice
Есть еще более мощный способ изменить ответ — @ControllerAdvice, и он имеет больший приоритет, чем @ResponseStatus.
В @ControllerAdvice можно не только изменить код ответа, но и тело. К тому же один обработчик можно назначить сразу для нескольких исключений.
Допустим мы хотим, чтобы ответ на запрос несуществующего Person имел такую структуру:
@Data @AllArgsConstructor @NoArgsConstructor public class ApiError { private String message; private String debugMessage; }
Для этого создадим обработчик в @ControllerAdvice, который перехватывает наше исключение MyEntityNotFoundException:
@ControllerAdvice public class RestExceptionHandler { @ExceptionHandler({MyEntityNotFoundException.class, EntityNotFoundException.class}) protected ResponseEntity<Object> handleEntityNotFoundEx(RuntimeException ex, WebRequest request) { ApiError apiError = new ApiError("entity not found ex", ex.getMessage()); return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND); } }
Теперь в ответ на запрос
GET localhost:8080/persons/2
мы получаем статус 404 с телом:
{ "message": "entity not found ex", "debugMessage": "Entity is not found, id=2" }
Но помимо MyEntityNotFoundException, наш обработчик поддерживает и javax.persistence.EntityNotFoundException (см. код выше).
Попробуем сделать так, чтобы оно возникло.
Это исключение EntityNotFoundException возникает в методе updatePerson() в контроллера. А именно, когда мы обращаемся с помощью метода PUT к несуществующей сущности в попытке назначить ей имя:
PUT localhost:8080/persons/2 { "name": "new name" }
В этом случае мы тоже получим ответ с новой структурой:
{ "message": "entity not found ex", "debugMessage": "Unable to find ru.sysout.model.Person with id 2" }
Итого, обработчик в @ControllerAdvice позволил изменить не только код ответа, но и тело сообщение. Причем один обработчик мы применили для двух исключений.
Последовательность проверок
Обратите внимание, что MyEntityNotFoundException мы «обработали» дважды — изменили код с помощью @ResponseStatus (1) и прописали в @ContollerAdvice — тут изменили как код, так и тело ответа (2). Эти обработки могли быть противоречивы, но существует приоритет:
- Когда выбрасывается исключение MyEntityNotFoundException, сначала Spring проверяет @ControllerAdvice-класс. А именно, нет ли в нем обработчика, поддерживающего наше исключение. Если обработчик есть, то исключение в нем и обрабатывается. В этом случае код @ResponseStatus значения не имеет, и в BasicErrorController исключение тоже не идет.
- Если исключение не поддерживается в @ControllerAdvice-классе, то оно идет в BasicErrorController. Но перед этим Spring проверяет, не аннотировано ли исключение аннотацией @ResponseStatus. Если да, то код ответа меняется, как указано в @ResponseStatus. Далее формируется ответ в BasicErrorController.
- Если же первые два условия не выполняются, то исключение обрабатывается сразу в BasicErrorController — там формируется стандартный ответ со стандартным кодом (для пользовательских исключений он равен 500).
Но и стандартный ответ можно изменить, для этого нужно расширить класс DefaultErrorAttributes.
Попробуем это сделать.
Изменение DefaultErrorAttributes
Давайте добавим в стандартный ответ еще одно поле. Для этого расширим класс:
@Component public class CustomErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map<String, Object> errorAttributes=super.getErrorAttributes(webRequest, options); errorAttributes.put("newAttribute", "value"); return errorAttributes; } }
В Map errorAttributes перечисляются поля ответа. Мы взяли их из родительского метода и добавили свое поле newAttribute.
Чтобы выполнить проверку, надо убрать @ControllerAdvice, поскольку он самый приоритетный и с ним мы даже не дойдем до BasicErrorController со «стандартными» полями.
Далее запросим ресурс:
localhost:8080/persons/2
Получим ответ:
{ "timestamp": "2021-02-28T18:50:14.479+00:00", "status": 404, "error": "Not Found", "message": "Entity is not found, id=2", "path": "/persons/2", "newAttribute": "value" }
В JSON-ответе появилось дополнительное поле.
ResponseStatusException
Рассмотрим еще один вариант, позволяющий сразу протолкнуть код ответа и сообщение стандартные поля, не прописывая обработку пользовательских или встроенных исключений. А вместо этого просто выбросив специально предназначенное исключение ResponseStatusException.
Изменим код метода контроллера getPerson():
@GetMapping(value = "/{personId}") public Person getPerson(@PathVariable("personId") long personId){ return personRepository.findById(personId).orElseThrow(() -> new ResponseStatusException( HttpStatus.NOT_FOUND, "Person Not Found")); }
Теперь тут не выбрасывается ни MyEntityNotFoundException, ни java.util.NoSuchElementException. А выбрасывается ResponseStatusException с заданным сообщением и кодом ответа.
Теперь при запросе
GET localhost:8080/persons/2
ответ будет таким:
{ "timestamp": "2021-03-01T07:42:19.164+00:00", "status": 404, "error": "Not Found", "message": "Person Not Found", "path": "/persons/2", "newAttribute": "value" }
Как код, так и сообщение появилось в полях стандартного ответа.
ResponseStatusException не вступает в конкуренцию ни со способом @ControllerAdvice, ни с @ResponseStatus — просто потому, что это другое исключение.
Итоги
Код примера доступен на GitHub. В следующей части мы унаследуем RestExceptionHandler от ResponseEntityExceptionHandler. Это класс-заготовка, которая уже обрабатывает ряд исключений.