аннотированный класс @ControllerAdvice не улавливает исключение, генерируемое на уровне сервиса

#java #spring #spring-boot #rest #exception

#java #весна #весенняя загрузка #rest #исключение

Вопрос:

Я пытаюсь централизовать обработку ошибок в моем приложении spring boot. В настоящее время я обрабатываю только одно потенциальное исключение (исключение NoSuchElementException), это совет контроллера:

 
import java.util.NoSuchElementException;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler(NoSuchElementException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public DispatchError dispatchNotFound(NoSuchElementException exception) {
        System.out.println("asdasdasd");
        return new DispatchError(exception.getMessage());
    }
}
 

И вот служба, которая выдает исключения:

 
import java.util.List;

import com.deliveryman.deliverymanapi.model.entities.Dispatch;
import com.deliveryman.deliverymanapi.model.repositories.DispatchRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DaoService {

    @Autowired
    DispatchRepository dispatchRepo;

    public Dispatch findByShipmentNumber(long shipmentNumber) {
        return dispatchRepo.findById(shipmentNumber).orElseThrow();
    }

    public List<Dispatch> findByUser(String user, String status) {
        if(status == null) {
            return dispatchRepo.findByOriginator(user).orElseThrow();
        } else {
            return dispatchRepo.findByOriginatorAndStatus(user, status).orElseThrow();
        }
    }

    public Dispatch createDispatch(Dispatch dispatch) { //TODO parameter null check exception
        return dispatchRepo.save(dispatch);
    }

}
 

Проблема в том, что как только я отправляю запрос на несуществующий ресурс, отображаемое сообщение json является сообщением spring по умолчанию. Это должно быть мое пользовательское сообщение об ошибке json (DispatchError).

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

Может кто-нибудь объяснить мне, почему это происходит?

Комментарии:

1. Какой класс выдает NoSuchElementException ?

2. Матеус, класс обслуживания с orElseThrow() необязательного класса

Ответ №1:

Либо аннотируйте свой класс рекомендаций контроллера с помощью @ResponseBody

 @ControllerAdvice
@ResponseBody
public class ExceptionController {
   ...
 

или замените @ControllerAdvice на @RestControllerAdvice .

Протестировано и проверено на моем компьютере с помощью источника из вашего совета контроллера.

Из источника для @RestControllerAdvice

 @ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
  ...
 

Следовательно, @RestControllerAdvice является сокращением для

 @ControllerAdvice
@ResponseBody
 

Из исходного документа для @ResponseBody

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

Альтернативное использование @ControllerAdvice только:

 @ControllerAdvice
public class ExceptionHandlerAdvice {

    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<DispatchError> dispatchNotFound(NoSuchElementException exception) {
        return new ResponseEntity<>(new DispatchError(exception.getMessage()), HttpStatus.NOT_FOUND);
    }
}
 

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

 package no.mycompany.myapp.error;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;

@RestController
@RequiredArgsConstructor
public class ErrorHandler implements ErrorController {

    private static final String ERROR_PATH = "/error";
    private final ErrorAttributes errorAttributes;

    @RequestMapping(ERROR_PATH)
    DispatchError handleError(WebRequest webRequest) {
        var attrs = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE));
        return new DispatchError((String) attrs.get("message"));
    }

    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }
}
 

Ввод реализации ErrorController в classpath заменяет Spring BasicErrorController .

При усилении @RestControllerAdvice контроллер ошибок больше не действует для NoSuchElementException .

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

 package no.mycompany.myapp.error;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.Map;

@Data
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiError {

    private long timestamp = new Date().getTime();
    private int status;
    private String message;
    private String url;
    private Map<String, String> validationErrors;

    public ApiError(int status, String message, String url) {
        this.status = status;
        this.message = message;
        this.url = url;
    }

    public ApiError(int status, String message, String url, Map<String, String> validationErrors) {
        this(status, message, url);
        this.validationErrors = validationErrors;
    }
}
 

Для ErrorHandler приведенного выше замените handleError на this

     @RequestMapping(ERROR_PATH)
    ApiError handleError(WebRequest webRequest) {
        var attrs = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE));
        return new ApiError(
                (Integer) attrs.get("status"),
                (String) attrs.get("message"), // consider using predefined message(s) here
                (String) attrs.get("path"));
    }
 

Рекомендации по обработке исключений проверки

 package no.mycompany.myapp.error;

import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;

@RestControllerAdvice
public class ExceptionHandlerAdvice {

    private static final String ERROR_MSG = "validation error";

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    ApiError handleValidationException(MethodArgumentNotValidException exception, HttpServletRequest request) {

        return new ApiError(
                HttpStatus.BAD_REQUEST.value(),
                ERROR_MSG,
                request.getServletPath(),
                exception.getBindingResult().getFieldErrors().stream()
                    .collect(Collectors.toMap(
                            FieldError::getField,
                            FieldError::getDefaultMessage,
                            // mergeFunction handling multiple errors for a field
                            (firstMessage, secondMessage) -> firstMessage)));
    }
}
 

Связанная конфигурация в application.yml

 server:
  error:
    include-message: always
    include-binding-errors: always
 

При использовании application.properties

 server.error.include-message=always
server.error.include-binding-errors=always
 

При использовании Spring Data JPA рассмотрите возможность использования следующего параметра для отключения второй проверки.

 spring:
  jpa:
    properties:
      javax:
        persistence:
          validation:
            mode: none
 

Дополнительная информация об обработке исключений в Spring:

https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc (пересмотрено в апреле 2018 года) https://www.baeldung.com/exception-handling-for-rest-with-spring (31 декабря 2020 г.)

Комментарии:

1. Ссылочный код, не использующий ResponseEntity, совпадает с моим фактическим кодом; возвращает развернутый pojo. Я удовлетворен вашим ответом, но не полностью, я ожидаю более подробного объяснения того, как spring фиксирует эти исключения и как он знает, когда использовать дескриптор исключения по умолчанию или настроенный.

2. @EdgarHernandez: Я чувствую, что тема слишком обширна для ответа SO. Вы не возражаете, если я отправлю вам эту ссылку на хорошо написанную статью по этой теме? Статья была пересмотрена в 2018 году. spring.io/blog/2013/11/01/exception-handling-in-spring-mvc БР

3. @EdgarHernandez: Спасибо, что приняли мой ответ. Я добавил к своему ответу еще немного кода, который может оказаться для вас полезным. BR