Обработка исключений службы REST для Spring Boot

Я пытаюсь создать крупномасштабный сервер служб REST. Мы используем Spring Boot 1.2.1 Spring 4.1.5 и Java 8. Наши controllerы реализуют @RestController и стандартные annotations @RequestMapping.

Моя проблема в том, что Spring Boot устанавливает redirect по умолчанию для исключений controllerа для /error . Из документов:

Spring Boot обеспечивает сопоставление / ошибка по умолчанию, которая обрабатывает все ошибки разумным образом и регистрируется как «глобальная» страница ошибки в контейнере сервлетов.

Начиная с лет написания REST-приложений с Node.js, это для меня ничего, кроме разумного. Любое исключение, которое генерирует конечная точка службы, должно возвращаться в ответ. Я не могу понять, почему вы отправляете redirect на наиболее вероятный пользователь Angular или JQuery SPA, который только ищет ответ и не может или не будет предпринимать никаких действий при перенаправлении.

Я хочу создать глобальный обработчик ошибок, который может принимать любое исключение – целенаправленно выбрасываться из метода сопоставления запросов или автоматически генерироваться Spring (404, если метод отслеживания не найден для сигнатуры пути запроса), и возвращать стандартного отформатированного ответа об ошибке (400, 500, 503, 404) клиенту без перенаправления MVC. В частности, мы собираемся принять эту ошибку, зарегистрировать ее в NoSQL с UUID, а затем вернуть клиенту нужный код ошибки HTTP с UUID записи журнала в теле JSON.

Документы были расплывчатыми о том, как это сделать. Мне кажется, что вам нужно либо создать собственную реализацию ErrorController, либо использовать ControllerAdvice в некотором роде , но все примеры, которые я видел, по-прежнему include отправку ответа на какое-то сопоставление ошибок, что не помогает. Другие примеры предполагают, что вам придется перечислить каждый тип исключения, который вы хотите обработать, вместо того, чтобы просто перечислять «Throwable» и получать все.

Может ли кто-нибудь сказать мне, что я пропустил, или указать мне в правильном направлении, как это сделать, не предлагая цепочку, с которой Node.js было бы легче справиться?

9 Solutions collect form web for “Обработка исключений службы REST для Spring Boot”

Новый ответ (2016-04-20)

Использование Spring Boot 1.3.1.RELEASE

Новый шаг 1 – Легко и менее навязчиво добавить следующие свойства в application.properties:

 spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false , spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false 

Намного проще, чем модифицировать существующий экземпляр DispatcherServlet (как показано ниже)! – JO ‘

Если вы работаете с полным RESTful Application, очень важно отключить автоматическое сопоставление статических ресурсов, так как если вы используете конфигурацию по умолчанию Spring Boot для обработки статических ресурсов, тогда обработчик ресурсов будет обрабатывать запрос (он заказывается последним и отображается в / **, что означает, что он выбирает любые запросы, которые не были обработаны каким-либо другим обработчиком в приложении), поэтому сервлет диспетчера не получает возможности выбросить исключение.


Новый ответ (2015-12-04)

Использование Spring Boot 1.2.7.RELEASE

Новый шаг 1 – я обнаружил гораздо менее назойливый способ установки значка «throExceptionIfNoHandlerFound». Замените код замены DispatcherServlet ниже (шаг 1) с этим в classе инициализации приложения:

 @ComponentScan() @EnableAutoConfiguration public class MyApplication extends SpringBootServletInitializer { private static Logger LOG = LoggerFactory.getLogger(MyApplication.class); public static void main(String[] args) { ApplicationContext ctx = SpringApplication.run(MyApplication.class, args); DispatcherServlet dispatcherServlet = (DispatcherServlet)ctx.getBean("dispatcherServlet"); dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); } 

В этом случае мы устанавливаем флаг существующего DispatcherServlet, который сохраняет любую автоматическую конфигурацию с помощью Spring Spring.

Еще одна вещь, которую я нашел – аннотация @EnableWebMvc смертельно опасна для Spring Boot. Да, эта аннотация позволяет такие вещи, как возможность поймать все исключения controllerа, как описано ниже, но также убивает много полезной автоматической конфигурации, которую обычно предоставляет Spring Boot. Используйте эту аннотацию с особой осторожностью, когда используете Spring Boot.


Оригинальный ответ:

После намного большего количества исследований и последующих решений по размещенным здесь решениям (спасибо за помощь!) И небольшому количеству трассировки во время выполнения кода Spring, я наконец нашел конфигурацию, которая будет обрабатывать все Исключения (а не ошибки, но читать дальше) включая 404s.

Шаг 1 – сообщите SpringBoot о прекращении использования MVC для ситуаций «обработчик не найден». Мы хотим, чтобы Spring выбрала исключение вместо того, чтобы вернуть клиенту переадресацию вида на «/ error». Для этого вам нужно иметь запись в одном из ваших classов конфигурации:

 // NEW CODE ABOVE REPLACES THIS! (2015-12-04) @Configuration public class MyAppConfig { @Bean // Magic entry public DispatcherServlet dispatcherServlet() { DispatcherServlet ds = new DispatcherServlet(); ds.setThrowExceptionIfNoHandlerFound(true); return ds; } } 

Недостатком этого является то, что он заменяет сервлет диспетчера по умолчанию. Это еще не проблема для нас, без каких-либо побочных эффектов или проблем с выполнением. Если вы собираетесь делать что-либо еще с сервлетом диспетчера по другим причинам, это место, где можно их сделать.

Шаг 2. Теперь, когда весенняя загрузка вызовет исключение, если обработчик не найден, это исключение можно обрабатывать с любыми другими в едином обработчике исключений:

 @EnableWebMvc @ControllerAdvice public class ServiceExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(Throwable.class) @ResponseBody ResponseEntity handleControllerException(HttpServletRequest req, Throwable ex) { ErrorResponse errorResponse = new ErrorResponse(ex); if(ex instanceof ServiceException) { errorResponse.setDetails(((ServiceException)ex).getDetails()); } if(ex instanceof ServiceHttpException) { return new ResponseEntity(errorResponse,((ServiceHttpException)ex).getStatus()); } else { return new ResponseEntity(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR); } } @Override protected ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { Map responseBody = new HashMap<>(); responseBody.put("path",request.getContextPath()); responseBody.put("message","The URL you have reached is not in service at this time (404)."); return new ResponseEntity(responseBody,HttpStatus.NOT_FOUND); } ... } 

Имейте в виду, что я думаю, что примечание «@EnableWebMvc» здесь значимо. Кажется, что ничто из этого не работает без него. И это все – ваше приложение для загрузки Spring теперь поймает все исключения, включая 404s, в classе вышеперечисленного обработчика, и вы можете сделать с ними, как вам будет угодно.

Один последний момент – кажется, нет способа заставить это поймать выброшенные Ошибки. У меня есть сумасшедшая идея использования аспектов для улавливания ошибок и превращения их в исключения, которые могут быть рассмотрены выше, но у меня еще не было времени, чтобы попытаться реализовать это. Надеюсь, это поможет кому-то.

Любые комментарии / исправления / улучшения будут оценены.

С Spring Boot 1.4+ добавлены новые classные classы для упрощения обработки исключений, что помогает удалить шаблонный код.

Для обработки исключений предоставляется новое @RestControllerAdvice , это комбинация @ControllerAdvice и @ResponseBody . Вы можете удалить @ResponseBody в методе @ExceptionHandler когда используете эту новую аннотацию.

т.е.

 @RestControllerAdvice public class GlobalControllerExceptionHandler { @ExceptionHandler(value = { Exception.class }) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiErrorResponse unknownException(Exception ex, WebRequest req) { return new ApiErrorResponse(...); } } 

Для обработки 404 ошибок добавлена ​​аннотация @EnableWebMvc а для application.properties достаточно:
spring.mvc.throw-exception-if-no-handler-found=true

Здесь вы можете найти и поиграть с источниками:
https://github.com/magiccrafter/spring-boot-exception-handling

Я думаю, ResponseEntityExceptionHandler отвечает вашим требованиям. Пример кода для HTTP 400:

 @ControllerAdvice public class MyExceptionHandler extends ResponseEntityExceptionHandler { @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class, HttpRequestMethodNotSupportedException.class}) public ResponseEntity badRequest(HttpServletRequest req, Exception exception) { // ... } } 

Вы можете проверить это сообщение

Как насчет этого кода? Я использую резервное сопоставление запросов, чтобы поймать ошибки 404.

 @Controller @ControllerAdvice public class ExceptionHandlerController { @ExceptionHandler(Exception.class) public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) { //If exception has a ResponseStatus annotation then use its response code ResponseStatus responseStatusAnnotation = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); return buildModelAndViewErrorPage(request, response, ex, responseStatusAnnotation != null ? responseStatusAnnotation.value() : HttpStatus.INTERNAL_SERVER_ERROR); } @RequestMapping("*") public ModelAndView fallbackHandler(HttpServletRequest request, HttpServletResponse response) throws Exception { return buildModelAndViewErrorPage(request, response, null, HttpStatus.NOT_FOUND); } private ModelAndView buildModelAndViewErrorPage(HttpServletRequest request, HttpServletResponse response, Exception ex, HttpStatus httpStatus) { response.setStatus(httpStatus.value()); ModelAndView mav = new ModelAndView("error.html"); if (ex != null) { mav.addObject("title", ex); } mav.addObject("content", request.getRequestURL()); return mav; } } 

Хотя это старый вопрос, я хотел бы поделиться своими мыслями по этому поводу. Надеюсь, что это будет полезно для некоторых из вас.

В настоящее время я создаю REST API, который использует Spring Boot 1.5.2.RELEASE с Spring Framework 4.3.7.RELEASE. Я использую подход Java Config (в отличие от конфигурации XML). Кроме того, мой проект использует глобальный механизм обработки исключений, используя аннотацию @RestControllerAdvice (см. Ниже).

Мой проект имеет те же требования, что и у вас: я хочу, чтобы мой REST API возвращал HTTP 404 Not Found с сопроводительной полезной нагрузкой JSON в ответе HTTP клиенту API, когда он пытается отправить запрос на URL-адрес, который не существует. В моем случае полезная нагрузка JSON выглядит так (что явно отличается от значения Spring Boot по умолчанию, кстати):

 { "code": 1000, "message": "No handler found for your request.", "timestamp": "2017-11-20T02:40:57.628Z" } 

Я, наконец, заработал. Вот основные задачи, которые вам нужно сделать вкратце:

  • Убедитесь, что NoHandlerFoundException если клиенты API вызывают URLS, для которых не существует метода обработчика (см. Шаг 1 ниже).
  • Создайте собственный class ошибок (в моем случае ApiError ), который содержит все данные, которые должны быть возвращены клиенту API (см. Шаг 2).
  • Создайте обработчик исключений, который реагирует на NoHandlerFoundException и возвращает правильное сообщение об ошибке клиенту API (см. Шаг 3).
  • Напишите тест для него и убедитесь, что он работает (см. Шаг 4).

Хорошо, теперь к деталям:

Шаг 1: Настройте application.properties

Мне пришлось добавить следующие два параметра конфигурации в файл application.properties проекта:

 spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false , spring.mvc.throw-exception-if-no-handler-found=true spring.resources.add-mappings=false 

Это гарантирует, что NoHandlerFoundException в случаях, когда клиент пытается получить доступ к URL-адресу, для которого не существует метода controllerа, который мог бы обрабатывать запрос.

Шаг 2. Создание classа для ошибок API.

Я сделал class, подобный тому, который был предложен в этой статье в блоге Евгения Паращива. Этот class представляет ошибку API. Эта информация отправляется клиенту в тело ответа HTTP в случае ошибки.

 public class ApiError { private int code; private String message; private Instant timestamp; public ApiError(int code, String message) { this.code = code; this.message = message; this.timestamp = Instant.now(); } public ApiError(int code, String message, Instant timestamp) { this.code = code; this.message = message; this.timestamp = timestamp; } // Getters and setters here... } 

Шаг 3: Создание / настройка глобального обработчика исключений

Я использую следующий class для обработки исключений (для простоты я удалил операторы импорта, код регистрации и некоторые другие, не относящиеся к делу fragmentы кода):

 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiError noHandlerFoundException( NoHandlerFoundException ex) { int code = 1000; String message = "No handler found for your request."; return new ApiError(code, message); } // More exception handlers here ... } 

Шаг 4: Напишите тест

Я хочу убедиться, API всегда возвращает правильные сообщения об ошибках вызывающему клиенту, даже в случае сбоя. Таким образом, я написал тест следующим образом:

 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SprintBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @ActiveProfiles("dev") public class GlobalExceptionHandlerIntegrationTest { public static final String ISO8601_DATE_REGEX = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$"; @Autowired private MockMvc mockMvc; @Test @WithMockUser(roles = "DEVICE_SCAN_HOSTS") public void invalidUrl_returnsHttp404() throws Exception { RequestBuilder requestBuilder = getGetRequestBuilder("/does-not-exist"); mockMvc.perform(requestBuilder) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code", is(1000))) .andExpect(jsonPath("$.message", is("No handler found for your request."))) .andExpect(jsonPath("$.timestamp", RegexMatcher.matchesRegex(ISO8601_DATE_REGEX))); } private RequestBuilder getGetRequestBuilder(String url) { return MockMvcRequestBuilders .get(url) .accept(MediaType.APPLICATION_JSON); } 

@ActiveProfiles("dev") можно оставить в стороне. Я использую его только при работе с разными профилями. RegexMatcher – это настраиваемый совпадение Hamcrest, которое я использую, чтобы лучше обрабатывать поля timestamp. Вот код (я нашел его здесь ):

 public class RegexMatcher extends TypeSafeMatcher { private final String regex; public RegexMatcher(final String regex) { this.regex = regex; } @Override public void describeTo(final Description description) { description.appendText("matches regular expression=`" + regex + "`"); } @Override public boolean matchesSafely(final String string) { return string.matches(regex); } // Matcher method you can call on this matcher class public static RegexMatcher matchesRegex(final String string) { return new RegexMatcher(regex); } } 

Некоторые дополнительные заметки с моей стороны:

  • Во многих других сообщениях в StackOverflow люди предложили установить аннотацию @EnableWebMvc . Это не было необходимо в моем случае.
  • Этот подход хорошо работает с MockMvc (см. Тест выше).

По умолчанию Spring Boot дает json сведения об ошибках.

 curl -v localhost:8080/greet | json_pp [...] < HTTP/1.1 400 Bad Request [...] { "timestamp" : 1413313361387, "exception" : "org.springframework.web.bind.MissingServletRequestParameterException", "status" : 400, "error" : "Bad Request", "path" : "/greet", "message" : "Required String parameter 'name' is not present" } 

Он также работает для всех типов ошибок сопоставления запросов. Проверьте эту статью http://www.jayway.com/2014/10/19/spring-boot-error-responses/

Если вы хотите создать журнал для NoSQL. Вы можете создать @ControllerAdvice, где вы будете регистрировать его, а затем повторно выбросить исключение. В документации есть пример https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Для controllerов REST я бы рекомендовал использовать Zalando Problem Spring Web .

https://github.com/zalando/problem-spring-web

Если Spring Boot стремится внедрить некоторую автоматическую конфигурацию, эта библиотека делает больше для обработки исключений. Вам просто нужно добавить зависимость:

  org.zalando problem-spring-web LATEST  

И затем определите одну или несколько характеристик рекомендаций для своих исключений (или используйте предоставленные по умолчанию)

 public interface NotAcceptableAdviceTrait extends AdviceTrait { @ExceptionHandler default ResponseEntity handleMediaTypeNotAcceptable( final HttpMediaTypeNotAcceptableException exception, final NativeWebRequest request) { return Responses.create(Status.NOT_ACCEPTABLE, exception, request); } } 

Затем вы можете определить рекомендации controllerа для обработки исключений как:

 @ControllerAdvice class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait { } 

Решение с dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); и @EnableWebMvc @ControllerAdvice работал для меня с Spring Boot 1.3.1, пока не работал на 1.2.7

@RestControllerAdvice – это новая функция Spring Framework 4.3 для обработки исключения с помощью RestfulApi с помощью сквозного решения:

  package com.khan.vaquar.exception; import javax.servlet.http.HttpServletRequest; import org.owasp.esapi.errors.IntrusionException; import org.owasp.esapi.errors.ValidationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import com.khan.vaquar.domain.ErrorResponse; /** * Handles exceptions raised through requests to spring controllers. **/ @RestControllerAdvice public class RestExceptionHandler { private static final String TOKEN_ID = "tokenId"; private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class); /** * Handles InstructionExceptions from the rest controller. * * @param e IntrusionException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IntrusionException.class) public ErrorResponse handleIntrusionException(HttpServletRequest request, IntrusionException e) { log.warn(e.getLogMessage(), e); return this.handleValidationException(request, new ValidationException(e.getUserMessage(), e.getLogMessage())); } /** * Handles ValidationExceptions from the rest controller. * * @param e ValidationException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = ValidationException.class) public ErrorResponse handleValidationException(HttpServletRequest request, ValidationException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); if (e.getUserMessage().contains("Token ID")) { tokenId = ""; } return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getUserMessage()); } /** * Handles JsonProcessingExceptions from the rest controller. * * @param e JsonProcessingException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = JsonProcessingException.class) public ErrorResponse handleJsonProcessingException(HttpServletRequest request, JsonProcessingException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getOriginalMessage()); } /** * Handles IllegalArgumentExceptions from the rest controller. * * @param e IllegalArgumentException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public ErrorResponse handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getMessage()); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = UnsupportedOperationException.class) public ErrorResponse handleUnsupportedOperationException(HttpServletRequest request, UnsupportedOperationException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getMessage()); } /** * Handles MissingServletRequestParameterExceptions from the rest controller. * * @param e MissingServletRequestParameterException * @return error response POJO */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MissingServletRequestParameterException.class) public ErrorResponse handleMissingServletRequestParameterException( HttpServletRequest request, MissingServletRequestParameterException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.BAD_REQUEST.value(), e.getClass().getSimpleName(), e.getMessage()); } /** * Handles NoHandlerFoundExceptions from the rest controller. * * @param e NoHandlerFoundException * @return error response POJO */ @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(value = NoHandlerFoundException.class) public ErrorResponse handleNoHandlerFoundException(HttpServletRequest request, NoHandlerFoundException e) { String tokenId = request.getParameter(TOKEN_ID); log.info(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.NOT_FOUND.value(), e.getClass().getSimpleName(), "The resource " + e.getRequestURL() + " is unavailable"); } /** * Handles all remaining exceptions from the rest controller. * * This acts as a catch-all for any exceptions not handled by previous exception handlers. * * @param e Exception * @return error response POJO */ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(value = Exception.class) public ErrorResponse handleException(HttpServletRequest request, Exception e) { String tokenId = request.getParameter(TOKEN_ID); log.error(e.getMessage(), e); return new ErrorResponse( tokenId, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getClass().getSimpleName(), "An internal error occurred"); } } 
  • catch исключение указателем в C ++
  • Оскорбительно ли использовать IDisposable и «использование» в качестве средства для получения «видимого поведения» для безопасности исключений?
  • Давайте будем гением компьютера.