Setting Precedence of Multiple @ControllerAdvice @ExceptionHandlers
Asked Answered
L

9

128

I have multiple classes annotated with @ControllerAdvice, each with an @ExceptionHandler method in.

One handles Exception with the intention that if no more specific handler is found, this should be used.

Sadly Spring MVC appears to be always using the most generic case (Exception) rather than more specific ones (IOException for example).

Is this how one would expect Spring MVC to behave? I'm trying to emulate a pattern from Jersey, which assesses each ExceptionMapper (equivalent component) to determine how far the declared type that it handles is from the exception that has been thrown, and always uses the nearest ancestor.

Loiretcher answered 21/10, 2013 at 15:16 Comment(0)
H
195

Is this how one would expect Spring MVC to behave?

As of Spring 4.3.7, here's how Spring MVC behaves: it uses HandlerExceptionResolver instances to handle exceptions thrown by handler methods.

By default, the web MVC configuration registers a single HandlerExceptionResolver bean, a HandlerExceptionResolverComposite, which

delegates to a list of other HandlerExceptionResolvers.

Those other resolvers are

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver

registered in that order. For the purpose of this question we only care about ExceptionHandlerExceptionResolver.

An AbstractHandlerMethodExceptionResolver that resolves exceptions through @ExceptionHandler methods.

At context initialization, Spring will generate a ControllerAdviceBean for each @ControllerAdvice annotated class it detects. The ExceptionHandlerExceptionResolver will retrieve these from the context, and sort them using using AnnotationAwareOrderComparator which

is an extension of OrderComparator that supports Spring's Ordered interface as well as the @Order and @Priority annotations, with an order value provided by an Ordered instance overriding a statically defined annotation value (if any).

It'll then register an ExceptionHandlerMethodResolver for each of these ControllerAdviceBean instances (mapping available @ExceptionHandler methods to the exception types they're meant to handle). These are finally added in the same order to a LinkedHashMap (which preserves iteration order).

When an exception occurs, the ExceptionHandlerExceptionResolver will iterate through these ExceptionHandlerMethodResolver and use the first one that can handle the exception.

So the point here is: if you have a @ControllerAdvice with an @ExceptionHandler for Exception that gets registered before another @ControllerAdvice class with an @ExceptionHandler for a more specific exception, like IOException, that first one will get called. As mentioned earlier, you can control that registration order by having your @ControllerAdvice annotated class implement Ordered or annotating it with @Order or @Priority and giving it an appropriate value.

Hayley answered 21/10, 2013 at 17:18 Comment(2)
Further, in case of multiple @ExceptionHandler methods within a @ControllerAdvice, the one handling the most specific superclass of the thrown exception is chosen.Cotyledon
In spring boot 2.3.3 it doesn't require @Order annotation on a subclass overriding a controller advice ExceptionHandler method from a parent controller advice classBly
F
124

Sotirios Delimanolis was very helpful in his answer, on further investigation we found that, in spring 3.2.4 anyway, the code that looks for @ControllerAdvice annotations also checks for the presence of @Order annotations and sorts the list of ControllerAdviceBeans.

The resulting default order for all controllers without the @Order annotation is Ordered#LOWEST_PRECEDENCE which means if you have one controller that needs to be the lowest priority then ALL your controllers need to have a higher order.

Here's an example showing how to have two exception handler classes with ControllerAdvice and Order annotations that can serve appropriate responses when either a UserProfileException or RuntimeException occurs.

class UserProfileException extends RuntimeException {
}

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
class UserProfileExceptionHandler {
    @ExceptionHandler(UserProfileException)
    @ResponseBody
    ResponseEntity<ErrorResponse> handleUserProfileException() {
        ....
    }
}

@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
class DefaultExceptionHandler {

    @ExceptionHandler(RuntimeException)
    @ResponseBody
    ResponseEntity<ErrorResponse> handleRuntimeException() {
        ....
    }
}
  • See ControllerAdviceBean#initOrderFromBeanType()
  • See ControllerAdviceBean#findAnnotatedBeans()
  • See ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache()

Enjoy!

Freida answered 9/12, 2013 at 14:47 Comment(0)
W
28

The order of exception handlers can be changed using the @Order annotation.

For example:

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomExceptionHandler {

    //...

}

@Order's value can be any integer.

Woven answered 16/12, 2013 at 12:9 Comment(1)
Thank you for giving imports in your answer!Excursion
H
6

I also found in the documentation that :

https://docs.spring.io/spring-framework/docs/4.3.4.RELEASE/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.html#getExceptionHandlerMethod-org.springframework.web.method.HandlerMethod-java.lang.Exception-

ExceptionHandlerMethod

protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception)

Find an @ExceptionHandler method for the given exception. The default implementation searches methods in the class hierarchy of the controller first and if not found, it continues searching for additional @ExceptionHandler methods assuming some @ControllerAdvice Spring-managed beans were detected. Parameters: handlerMethod - the method where the exception was raised (may be null) exception - the raised exception Returns: a method to handle the exception, or null

So this means that if you want to solve this issue, you will need to add your specific exception handler within the controller throwing those exception. ANd to define one and only ControllerAdvice handling the Global default exception handler.

This simplies the process and we don't need the Order annotation to handle the problem.

Hydrated answered 18/4, 2016 at 15:11 Comment(0)
A
5

you can also use a number value, like below

@Order(value = 100)

Lower values have higher priority. The default value is * {@code Ordered.LOWEST_PRECEDENCE},indicating lowest priority (losing to any other * specified order value)

Achromatous answered 25/11, 2019 at 7:29 Comment(1)
I like this approach more than @Order(Ordered.HIGHEST_PRECEDENCE) or @Order(Ordered.LOWEST_PRECEDENCE), because it gives you more flexibility and you can better structure the priority of multiple advice beans by assigning lower or higher value to the @OrderSparrow
J
4

Important Class to be handled :

@Order(Ordered.HIGHEST_PRECEDENCE)
public class FunctionalResponseEntityExceptionHandler 
{
    private final Logger logger = LoggerFactory.getLogger(FunctionalResponseEntityExceptionHandler.class);

    @ExceptionHandler(EntityNotFoundException.class)
    public final ResponseEntity<Object> handleFunctionalExceptions(EntityNotFoundException ex, WebRequest request)
    {
        logger.error(ex.getMessage() + " " + ex);
        ExceptionResponse exceptionResponse= new ExceptionResponse(new Date(), ex.getMessage(),
                request.getDescription(false),HttpStatus.NOT_FOUND.toString());
        return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
    }
}

Other Exceptions with Low priority

@ControllerAdvice
public class GlobalResponseEntityExceptionHandler extends ResponseEntityExceptionHandler
{
    private final Logger logger = LoggerFactory.getLogger(GlobalResponseEntityExceptionHandler.class);
    
    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllException(Exception ex, WebRequest request)
    {
        logger.error(ex.getMessage()+ " " + ex);
        ExceptionResponse exceptionResponse= new ExceptionResponse(new Date(), ex.toString(),
                request.getDescription(false),HttpStatus.INTERNAL_SERVER_ERROR.toString());
    }
}
Jalisajalisco answered 14/7, 2020 at 14:9 Comment(0)
T
3

There's a similar situation convered in the excellent "Exception Handling in Spring MVC" post on the Spring blog, in the section entitled Global Exception Handling. Their scenario involves checking for ResponseStatus annotations registered on the exception class, and if present, rethrowing the exception to let the framework handle them. You might be able to use this general tactic - try to determine if there is a might be a more appropriate handler out there and rethrowing.

Alternatively, there's some other exception handling strategies covered that you might look at instead.

Transonic answered 3/12, 2013 at 22:55 Comment(0)
D
1

If you want separate your exception handlers(like me), you can use @Import to do this.

@ControllerAdvice
class MyCustomExceptionHandler {
  ...
}
@ControllerAdvice
class MyOtherCustomExceptionHandler {
  ...
}
@Import({MyCustomExceptionHandler.class,MyOtherCustomExceptionHandler.class})
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
class ApplicationExceptionHandler{
  //Generic exception handlers
}
Diabetes answered 11/10, 2022 at 20:36 Comment(0)
E
0

Spring Framework sorts beans annotated with @ControllerAdvice using OrderComparator.

However, @ExceptionHandler methods present within a single class are sorted using ExceptionDepthComparator.

It means that a possible workaround is to put all @ExceptionHandler methods into a single class.

@ControllerAdvice
class ExceptionHandler {

    @ExceptionHandler(IOException.class)
    ResponseEntity<Object> handleIOException() {
        ....
    }

    @ExceptionHandler(Exception.class)
    ResponseEntity<Object> handleException() {
        ....
    }
}
Electrocautery answered 14/12, 2023 at 21:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.