@ControllerAdvice 注解类没有捕捉到服务层抛出的异常

时间:2021-01-21 23:55:12

标签: java spring spring-boot rest exception

我正在尝试将错误处理集中在我的 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 个答案:

答案 0 :(得分:3)

使用 @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 的实现放入类路径,替换 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

    @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 年 4 月修订) https://www.baeldung.com/exception-handling-for-rest-with-spring(2020 年 12 月 31 日)