As you already know, to return an error response in the Spring Boot application, we can throw a built-in ResponseStatusException or create a custom exception using the @ResponseStatus annotation and also throw it.

In this topic, we are going to learn how to use a combination of @ControllerAdvice and @ExceptionHandler@ControllerAdvice allows us to create a global class for intercepting various exceptions from different parts of an application, and @ExceptionHandler lets us customize error responses. Their union represents a more flexible approach to handling exceptions in applications. Also, in conjunction with the @ControllerAdvice annotated class, the ResponseEntityExceptionHandler class will be considered, which is used to customize the handling of standard Spring exceptions.

Controller preparation

Let’s take an example from the previous topic. It’s a web app that returns information about the flight by its ID.

Here is a POJO for the flight info:

public class FlightInfo {

    private long id;

    private String from;

    private String to;

    private String gate;

    // constructor, getters, setters
}

Here is a custom exception that will be thrown if flightInfo is not found in a collection:

public class FlightNotFoundException extends RuntimeException {

    public FlightNotFoundException(String message) {
        super(message);
    }
}

Also, we have a REST controller that contains the flightInfoList and getFlightInfo() methods, where FlightNotFoundException can be thrown:

@RestController
public class FlightController {

    private final List<FlightInfo> flightInfoList = Collections.synchronizedList(
            new ArrayList<>());

    public FlightController() {
        flightInfoList.add(
                new FlightInfo(
                        1,
                        "Delhi Indira Gandhi",
                        "Stuttgart",
                        "D80"));
        flightInfoList.add(
                new FlightInfo(
                        2,
                        "Tokyo Haneda",
                        "Frankfurt",
                        "110"));
    }

    @GetMapping("flights/{id}")
    public FlightInfo getFlightInfo(@PathVariable long id) {
        for (var flightInfo : flightInfoList) {
            if (flightInfo.getId() == id) {
                return flightInfo;
            }
        }

        throw new FlightNotFoundException("Flight info not found id=" + id); 
    }
}

@ExceptionHandler

For any uncaught exception, Spring returns 500 Internal Server Error with specific fields in the response body. The good news is that thanks to @ExceptionHandler we can handle exceptions and customize them in different ways: change the status code, set a specific response body, and more.

Let’s design a class representing our custom error message in the response body. For example, it has statusCodetimestampmessage, and description fields.

public class CustomErrorMessage {
    private int statusCode;
    private LocalDateTime timestamp;
    private String message;
    private String description;

    public CustomErrorMessage(
            int statusCode, 
            LocalDateTime timestamp, 
            String message, 
            String description) {

        this.statusCode = statusCode;
        this.timestamp = timestamp;
        this.message = message;
        this.description = description;
    }

    // getters ...
}

The handling of a FlightNotFoundException will take place in the handleFlightNotFound method annotated with @ExceptionHandler. In this method, we fill in the fields of the CustomErrorMessage using method arguments and then return a ResponseEntity with the created body and the NOT_FOUND http status.

@ExceptionHandler(FlightNotFoundException.class)
public ResponseEntity<CustomErrorMessage> handleFlightNotFound(
      FlightNotFoundException e, WebRequest request) {

    CustomErrorMessage body = new CustomErrorMessage(
            HttpStatus.NOT_FOUND.value(),
            LocalDateTime.now(),
            e.getMessage(),
            request.getDescription(false));

    return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}

The @ExceptionHandler method can support only a certain list of parameters and return types. You can find all the possible types in the documentation.

In our example, we can place the @ExceptionHandler method in the FlightController class. In that case, the handler can only handle exceptions from the controller where the method is placed (FlightController).

@ControllerAdvice

Another useful annotation for handling exceptions is @ControllerAdvice. A class annotated with @ControllerAdvice can intercept exceptions from different controllers. If we place the @ExceptionHandler method inside the @ControllerAdvice class, this will enable the handler to be applied to the exceptions from the @ControllerAdvice class, and therefore to different exceptions from multiple controllers.

Now let’s create a special class with the @ControllerAdvice annotation and place the handleFlightNotFound() method there.

@ControllerAdvice
public class ControllerExceptionHandler {

    @ExceptionHandler(FlightNotFoundException.class)
    public ResponseEntity<CustomErrorMessage> handleFlightNotFound(
            FlightNotFoundException e, WebRequest request) {

        CustomErrorMessage body = new CustomErrorMessage(
                HttpStatus.NOT_FOUND.value(),
                LocalDateTime.now(),
                e.getMessage(),
                request.getDescription(false));

        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }
}

Now if we try to get a non-existent flight, the Postman will show us our CustomErrorMessage response body and the NOT_FOUND status code.

ResponseEntityExceptionHandler

In the @ControllerAdvice class, we can handle not only custom exceptions but also standard exceptions thrown by Spring itself. To do this, the ResponseEntityExceptionHandler abstract class will help us. This class contains many methods for handling different types of Spring exceptions: handleMethodArgumentNotValidhandleMissingPathVariable, and so on. A full list of methods can be found here.

Let’s consider an example with MethodArgumentNotValidException. This exception will be thrown if the validation on an argument annotated with @Valid fails. To make Spring throw this exception in our application, let’s make the minimum value of the id field from the FlightInfo class equal to 1. Then, let’s create a new method in the controller that adds a validated FlightInfo object to flightInfoList.

public class FlightInfo {

    @Min(1)
    private long id;

    private String from;

    private String to;

    private String gate;
    
    //getters...
}
@RestController
public class FlightController {

    private final List<FlightInfo> flightInfoList = Collections.synchronizedList(
            new ArrayList<>());

    // constructor

    // getFlightInfo method

    @PostMapping("/flights/new")
    public void addNewFlightInfo(@Valid @RequestBody FlightInfo flightInfo) {
        flightInfoList.add(flightInfo);
    }
}

If we try to send flightInfo’s data with id=-1, by default, our application will throw a MethodArgumentNotValidException with the body and the status as shown below.

By default, Spring Boot doesn’t include the message field in a response. To enable it, add this line in the application.properties file: server.error.include-message=always

If it’s not enough for us, we can change the response body using the ResponseEntityExceptionHandler class.

The ResponseEntityExceptionHandler class defines the handleMethodArgumentNotValid method that returns a null body by default. To customize the handleMethodArgumentNotValid method, you can simply extend the ResponseEntityExceptionHandler class by the @ControllerAdvice class and override that method.

@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {

    // handleFlightNotFound method

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        // Just like a POJO, a Map is also converted to a JSON key-value structure
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", status.value());
        body.put("timestamp", LocalDateTime.now());
        body.put("exception", ex.getClass());
        return new ResponseEntity<>(body, headers, status);
    }
}

Now the result of passing invalid flightInfo to the addNewFlightInfo method contains custom JSON in the error body.

Conclusion

In this topic, we’ve learned how to use the @ControllerAdvice and @ExceptionHandler annotations. Now you know how to create a class for handling exceptions easily in one place. You can throw exceptions in any controllers and all of the exceptions will be processed by the special method annotated with @ExceptionHandler in the class annotated with @ControllerAdvice. Also, @ControllerAdvice classes can handle internal Spring exceptions by overriding the methods from the ResponseEntityExceptionHandler abstract class.

Leave a Reply

Your email address will not be published.