简体   繁体   中英

@ControllerAdvice annotated class is not catching the exception thrown in the service layer

I'm trying to centralize the error handling in my spring boot app. Currently i'm only handling one potential exception (NoSuchElementException), this is the controller advice:


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

And here's the service which throws the exceptions:


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

}

The problem is that once I send a request for an inexistent resource, the json message shown is the spring's default one. It should be my custom json error message (DispatchError).

Now, this is fixed by adding a @ResponseBody to the exception handler method but the thing is that I was using an old code of mine as reference, which works as expected without the @ResponseBody annotation.

Can someone explain me why this is happening?

Either annotate your controller advice class with @ResponseBody

@ControllerAdvice
@ResponseBody
public class ExceptionController {
   ...

or replace @ControllerAdvice with @RestControllerAdvice .

Tested and verified on my computer with source from your controller advice.

From source for @RestControllerAdvice

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

Hence, @RestControllerAdvice is shorthand for

@ControllerAdvice
@ResponseBody

From source doc for @ResponseBody

Annotation that indicates a method return value should be bound to the web response body. Supported for annotated handler methods.

Alternative using @ControllerAdvice only:

@ControllerAdvice
public class ExceptionHandlerAdvice {

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

I do have a theory on what's going on in your old app. With the advice from your question, and the error handler below, I can create a behaviour where the DispatchError instance appears to be returned by advice (advice is executed), but is actually returned by error controller.

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

Putting an implementation of ErrorController into classpath, replaces Spring's BasicErrorController .

When reinforcing @RestControllerAdvice , error controller is no longer in effect for NoSuchElementException .

In most cases, an ErrorController implementation that handles all errors, in combination with advice exception handlers for more complex exceptions like MethodArgumentNotValidException , should be sufficient. This will require a generic error DTO like this

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

For ErrorHandler above, replace handleError with 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"));
    }

Advice with validation exception handling

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

Related config in application.yml

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

When using application.properties

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

When using Spring Data JPA, consider using the following setting for turning off a second validation.

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

More information on exception handling in Spring:

https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc (revised April 2018) https://www.baeldung.com/exception-handling-for-rest-with-spring (December 31, 2020)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM