
Всем привет! Проверяя задания в учебном центре моей компании, обнаружил, что двумя словами описать то, как можно избавиться от ResponseEntity<?> в контроллерах не получится, и необходимо написать целую статью. Для начала, немного введения.
ВАЖНО! Статья написана для новичков в программировании и Spring в часности, которые знакомы со Spring на базовом уровне.
Что такое ResponseEntity<>? Представим ситуацию - у нас есть интернет магазин. И, при примитивной реализации, мы переходим по продуктам, передавая его Id в качестве параметра@RequestParam. Например, наш код выглядит таким образом:
@ResponseBody @GetMapping("/products") public Product getProduct(@RequestParam Long id){ return productsService.findById(id); }
При запросе через адресную строку браузера, вывод будет в виде JSON, таким:
{"id":1,"title":"Milk","price":100}
Однако, если мы обратимся к продукту, который у нас отсутствует, например с id=299, то получим следующую картину:

Для пользователя, или даже для фронтендщика, будет абсолютно непонятно, что пошло не так и в чём проблема. Совершая тот же запрос через Postman, ситуация яснее не будет:
{ "timestamp": "2022-06-30T18:21:03.634+00:00", "status": 500, "error": "Internal Server Error", "path": "/app/api/v1/products" }
И вот тут мы переходим к ResponseEntity<>. Этот объект представляет собой оболочку для Java классов, благодаря которой мы в полной мере сможем реализовать RESTfull архитектуру. Суть использования сводится к тому, чтобы вместо прямого возвращаемого типа данных в контроллере, использовать оболочку ResponseEntity<> и возвращать конечному пользователю, или, что скорее всего вероятно - фронту, JSON, который бы более-менее подробно описывал ошибку. Выглядит такой код примерно так:
@GetMapping("/products") public ResponseEntity<?> getProductRe(Long id){ try { Product product = productsService.findById(id).orElseThrow(); return new ResponseEntity<>(product, HttpStatus.OK); } catch (Exception e){ return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }
Что здесь происходит? Вместо строгого типа Product, мы ставим ResponseEntity<?>, где под ? понимается любой Java объект. Конструктор ResponseEntity позволяет перегружать этот объект, добавляя в него не только наш возвращаемый тип, но и статус, чтобы фронтенд мог понимать, что именно пошло не так. Например, при корректном исполнении программы, передавая id=1, мы увидим просто успешно переданный объект Product с кодом 200, а вот в случае продукта с id = 299 результат уже будет такой:

Всё ещё не красиво, но уже хотя бы понятно, что продукт не найден. Мы имеем статус код 404 и фронт уже как-то может с этим работать. Это здорово, но нам бы хотелось более конкретного описания ошибки и результата. Давайте, в таком случае, создадим новый класс:
public class AppError { private int statusCode; private String message; public int getStatusCode() { return statusCode; } public void setStatusCode(int statusCode) { this.statusCode = statusCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public AppError() { } public AppError(int statusCode, String message) { this.statusCode = statusCode; this.message = message; } }
Это будет вспомогательный класс. Его задача - принять наше сообщение и переслать его фронту вместе со статусом 404. Как мы это сделаем? Очень просто:
@GetMapping("/products") public ResponseEntity<?> getProduct(Long id){ try { Product product = productsService.findById(id).orElseThrow(); return new ResponseEntity<>(product, HttpStatus.OK); } catch (Exception e){ return new ResponseEntity<>(new AppError(HttpStatus.NOT_FOUND.value(), "Product with id " + id + " nor found"), HttpStatus.NOT_FOUND); } }
В этом примере, если мы ловим ошибку, просто отдаём в конструктор ResponseEntity наш кастомный объект и статус 404. Теперь, если мы попробуем получить продукт с id = 299, то ответ будет таким:
{ "statusCode": 404, "message": "Product with id 299 nor found" }
Отлично! Этого мы и хотели. Стало понятно, в чём проблема. Фронтенд легко распарсит этот JSON и обработает наше сообщение. Однако, сам метод контроллера теперь выглядит не слишком красиво. Да и когда сталкиваешься с чужим кодом, любой из нас сразу хотел бы видеть тип объекта, который будет возвращаться, а не какой-то там ResponseEntity со знаком вопроса в скобочках. Тут мы и переходим к основному материалу статьи.
Как избавиться от ResponseEntity в сигнатуре метода контроллера, при этом сохранив информативность возвращаемой ошибки?
Для этого нам понадобиться глобальный обработчик ошибок, который нам любезно предоставляется в пакете со спрингом. Для начала, создадим какой-то свой кастомный Exception, в котором унаследуемся от RuntimeException:
public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } }
Здесь ничего особенного. Интересное начинается дальше. Давайте внимательно посмотрим на листинг этого класса:
@ControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler public ResponseEntity<AppError> catchResourceNotFoundException(ResourceNotFoundException e) { log.error(e.getMessage(), e); return new ResponseEntity<>(new AppError(HttpStatus.NOT_FOUND.value(), e.getMessage()), HttpStatus.NOT_FOUND); } }
Начнём сверху вниз. @ControllerAdvice используется для глобальной обработки ошибок в приложении Spring. То есть любой Exception, выпадающий в нашем приложении, будет замечен нашим ControllerAdvice. @Slf4j используется для логгирования, заострять внимание мы на этом не будем. Далее создаём собственный класс, назвать его можем как угодно. И вот тут уже интересное - аннотация@ExceptionHandlerнад методом. Эта аннотация позволяет нам указать, что мы хотим перехватывать и обрабатывать исключения определённого типа, если они возникают, и зашивать их в ResponseEntity, чтобы вернуть ответ нашему фронту. В аргументах метода указываем, какую именно ошибку мы собираемся ловить. В данном случае, это наш кастомный ResourceNotFoundException. И возвращать мы будем точно такой же ResponseEntity, как и в примере выше, однако прописываем мы его уже всего 1 раз - в этом классе. Спринг на этапе обработки этой ошибки самостоятельно поймёт, что в методе нашего контроллера вместо нашего класса Product нужно будет вернуть ResponseEntity.
Теперь мы можем убрать из контроллера все ResponseEntity:
@GetMapping("/products") public Product getProduct(Long id){ return productsService.findById(id); }
А логику появления ошибки перенести в сервисный слой:
public Product findById(Long id) { return productsRepository.findById(id).orElseThrow( () -> new ResourceNotFoundException("Product with id " + id + " not found")); }
Теперь, если продукт не будет найден, выбросится ResourceNotFoundException. Наш глобальный обработчик исключений поймает это исключение, самостоятельно преобразует его в ResponseEntity и вместо Product'a вернут JSON с подробным описанием ошибки, как и прежде:
{ "statusCode": 404, "message": "Product with id 299 not found" }
Таким образом, мы избавились от ResponseEntity и кучи лишнего кода, переписываемого из метода в метод, при этом сохранив всю функциональность, которую нам предоставляет ResponseEntity.
