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

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

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

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

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

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

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

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

9 ответов


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

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

Шаг 1 - легко и менее навязчиво добавлять в приложение следующие свойства.свойства:

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

намного проще, чем изменение существующего экземпляра DispatcherServlet (как показано ниже)! - Джо!--7-->

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


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

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

Шаг 1 - Я нашел гораздо менее навязчивый способ установки флага" throExceptionIfNoHandlerFound". Замените код замены DispatcherServlet ниже (Шаг 1) этим в классе инициализации приложения:

@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 Boot.

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


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

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

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

// 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. теперь, когда spring boot выдаст исключение, когда обработчик не найден, это исключение можно обработать с любыми другими в едином обработчике исключений:

@EnableWebMvc
@ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    ResponseEntity<Object> 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<Object>(errorResponse,((ServiceHttpException)ex).getStatus());
        } else {
            return new ResponseEntity<Object>(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        Map<String,String> 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<Object>(responseBody,HttpStatus.NOT_FOUND);
    }
    ...
}

имейте в виду, что я подумайте, что здесь важна аннотация "@EnableWebMvc". Похоже, без него ничего не получится. И это все-ваше приложение Spring boot теперь поймает все исключения, включая 404s, в указанном выше классе обработчиков, и вы можете делать с ними все, что захотите.

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

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


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

новая @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 аннотация и следующее к приложению.свойства был достаточно:
spring.mvc.throw-exception-if-no-handler-found=true

вы можете найти и играть с источниками here:
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<Object> 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.Выпуск с пружинным каркасом 4.3.7.ОСВОБОЖДАТЬ. Я использую подход 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 адресов вызов для которых не существует метода обработчика (см. Шаг 1 ниже).
  • создайте пользовательский класс ошибок (в моем случае ApiError), который содержит все данные, которые должны быть возвращены клиенту API (см. Шаг 2).
  • создать обработчик исключений, который реагирует на NoHandlerFoundException и возвращает соответствующее сообщение об ошибке клиенту API (см. Шаг 3).
  • напишите тест для него и убедитесь, что он работает (см. Шаг 4).

Ок, теперь на подробности:

Шаг 1: настройка приложения.свойства

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

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

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

Шаг 2: Создайте класс для ошибок API

я сделал класс похожие на тот, который предлагается в в этой статье Блог Евгения Параскив по. Этот класс представляет ошибку 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: Создайте / настройте глобальный обработчик исключений

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

@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") аннотацию можно оставить. Я использую его только при работе с разными профилями. The RegexMatcher обычай Hamcrest matcher и я использую для лучшей обработки полей метки времени. Вот код (я нашел это здесь):

public class RegexMatcher extends TypeSafeMatcher<String> {

    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


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

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

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

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>problem-spring-web</artifactId>
    <version>LATEST</version>
</dependency>

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

public interface NotAcceptableAdviceTrait extends AdviceTrait {

    @ExceptionHandler
    default ResponseEntity<Problem> handleMediaTypeNotAcceptable(
            final HttpMediaTypeNotAcceptableException exception,
            final NativeWebRequest request) {
        return Responses.create(Status.NOT_ACCEPTABLE, exception, request);
    }

}

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

@ControllerAdvice
class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait {

}

@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 = "<OMITTED>";
        }

        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");
    }   

}

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