Java – @ExceptionHandler doesn’t catch exceptions being thrown from Spring Formatters

exception handlingjavaspringspring-bootspring-mvc

I have a controller that contains a show method to display info about the provided entity.

@Controller
@RequestMapping("/owners")
public class OwnersController {

  @RequestMapping(value = "/{owner}", method = RequestMethod.GET,
          produces = MediaType.TEXT_HTML_VALUE)
  public String show(@PathVariable Owner owner, Model model) {
    // Return view
    return "owners/show";
  }
}

To invoke this operation I use http://localhost:8080/owners/1 URL. As you could see, I provide Owner identifier 1.

To be able to convert identifier 1 to a valid Owner element is necessary to define an Spring Formatter and register it on addFormatters method from WebMvcConfigurerAdapter.

I have the following OwnerFormatter:

public class OwnerFormatter implements Formatter<Owner> {

  private final OwnerService ownerService;
  private final ConversionService conversionService;

  public OwnerFormatter(OwnerService ownerService,
      ConversionService conversionService) {
    this.ownerService = ownerService;
    this.conversionService = conversionService;
  }

  @Override
  public Owner parse(String text, Locale locale) throws ParseException {
    if (text == null || !StringUtils.hasText(text)) {
      return null;
    }
    Long id = conversionService.convert(text, Long.class);
    Owner owner = ownerService.findOne(id);
    if(owner == null){
        throw new EntityResultNotFoundException();
    }
    return owner;
  }

  @Override
  public String print(Owner owner, Locale locale) {
    return owner == null ? null : owner.getName();
  }
}

As you could see, I've use findOne method to obtain Owner with id 1. But, what happends if this method returns null because there aren't any Owner with id 1?

To prevent this, I throw a custom Exception called EntityResultNotFoundException. This exception contains the following code:

public class EntityResultNotFoundException extends RuntimeException {
    public EntityResultNotFoundException() {
        super("ERROR: Entity not found");
    }
}

I want to configure project to be able to return errors/404.html when this exception throws, so, following Spring documentation, I have two options:

Option a)

Configure a @ControllerAdvice with @ExceptionHandler annotated method that manages EntityResultNotFoundException:

@ControllerAdvice
public class ExceptionHandlerAdvice {
   @ExceptionHandler(EntityResultNotFoundException.class)
   public String handleEntityResultNotFoundException(){
      return "errors/404";
   }
}

But this is not working.If I change the implementation above to throw the exception inside the controller method, works perfect.

Seems like the Spring Formatter that is called before invoke controller implementation is not beeing monitorized by the @ControllerAdvice.

For me this doesn't makes sense because the Spring Formatter has been called before invoke the controller method to prepare the necessary parameters of the provided @RequestMapping… so Spring knows which method will be invoked… why the Spring Formatter used to prepare the request is not beeing monitorized by the @ControllerAdvice to catch all possible exceptions occured during this "preparation process"?

UPDATE: As Serge Ballesta said on his answer, the @ControllerAdvice is implemented as an AOP advice around the methods of the controller. So it has no way to intercept exception thrown outside of the controller.

I reject this option.

UPDATE 2: After some answers in the Spring Framework JIRA, they suggest me to use a generic @ExceptionHandler that catch all exceptions, and then using some conditionals to check root cause and to be able to know if the exception cause is the exception that I've invoke on the Spring Formatter. I think that this could be another improvement of Spring MVC, because I'm not able to use @ExceptionHandler to catch exceptions being thrown from Spring Formatters. You could check a proof I've uploaded here

I reject this option too.

Option b)

Include a new @Bean SimpleMappingExceptionResolver inside a @Configuration class to map exceptions with the view identifier.

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

  [...]

  @Bean
  public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
      SimpleMappingExceptionResolver resolver =
            new SimpleMappingExceptionResolver();
      Properties mappings = new Properties();
      mappings.setProperty("EntityResultNotFoundException", "errores/404");
      resolver.setExceptionMappings(mappings);
      return resolver;
  }
}

However, the implementation above doesn't works with exceptions throwed on Spring Formatters.

UPDATE: I've been debugging Spring code and I've found two things that maybe could be an interesting improvement to Spring Framework.

First of all, DispatcherServlet is loading all registered HandlerExceptionResolver from initStrategies and initHandlerExceptionResolvers methods. This method is taken all HandlerExceptionResolvers in the correct order, but then, uses the following code to order them again:

AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);

The problem is that this method delegates on the findOrder method that tries to obtain order from the HandlerExceptionResolver that is instance of Ordered. As you could see, I've not defined order on my registered @Bean, so when tries to obtain order from my declared bean SimpleMappingExceptionResolver is using LOWEST_PRECEDENCE. This causes that Spring uses DefaultHandlerExceptionResolver because is the first one that returns a result.

So, to solve this I've added order value to my declared bean with the following code.

  @Bean
  public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
      SimpleMappingExceptionResolver resolver =
            new SimpleMappingExceptionResolver();
      Properties mappings = new Properties();
      mappings.setProperty("EntityResultNotFoundException", "errores/404");
      resolver.setOrder(-1);
      resolver.setExceptionMappings(mappings);
      return resolver;
  }  

Now, when AnnotationAwareOrderComparator sorts all registered HandlerExceptionResolver the SimpleMappingExceptionResolveris the first one and it will be used as resolver.

Anyway, is not working yet. I've continue debugging and I've saw that now is using doResolveException from SimpleMappingExceptionResolver to resolve the exception, so it's ok. However, the method findMatchingViewName that tries to obtain the mapped view returns null.

The problem is that findMatchingViewName is trying to check if the received exception match with some exception defined on the exceptionMappings of the SimpleMappingExceptionResolver, but it's only checking the super classes inside getDepth method. Should be necessary to check the cause exception.

I've applied the following workaround to continue working (just extend SimpleMappingExceptionResolver and implements findMatchingViewName method to try to find matching view again with cause exception if depth is not valid)

public class CauseAdviceSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver{

    /**
     * Find a matching view name in the given exception mappings.
     * @param exceptionMappings mappings between exception class names and error view names
     * @param ex the exception that got thrown during handler execution
     * @return the view name, or {@code null} if none found
     * @see #setExceptionMappings
     */
    @Override
    protected String findMatchingViewName(Properties exceptionMappings, Exception ex) {
        String viewName = null;
        String dominantMapping = null;
        int deepest = Integer.MAX_VALUE;
        for (Enumeration<?> names = exceptionMappings.propertyNames(); names.hasMoreElements();) {
            String exceptionMapping = (String) names.nextElement();
            int depth = getDepth(exceptionMapping, ex);
            if (depth >= 0 && (depth < deepest || (depth == deepest &&
                    dominantMapping != null && exceptionMapping.length() > dominantMapping.length()))) {
                deepest = depth;
                dominantMapping = exceptionMapping;
                viewName = exceptionMappings.getProperty(exceptionMapping);
            }else if(ex.getCause() instanceof Exception){
                return findMatchingViewName(exceptionMappings, (Exception) ex.getCause() );
            }
        }
        if (viewName != null && logger.isDebugEnabled()) {
            logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() +
                    "], based on exception mapping [" + dominantMapping + "]");
        }
        return viewName;
    }

}

I think that this implementation is really interesting because also use the cause exception class instead of use only the superclasses exceptions. I'm going to create a new Pull-Request on Spring Framework github including this improvement.

With these 2 changes (order and extending SimpleMappingExceptionResolver) I'm able to catch an exception thrown from Spring Formatter and return a custom view.

Best Answer

Seems like the Spring Formatter that is called before invoke controller implementation is not beeing monitorized by the @ControllerAdvice

Good catch, that's exactly what happens.

why the Spring Formatter used to prepare the request is not beeing monitorized by the @ControllerAdvice to catch all possible exceptions occured during this "preparation process"

Because the @ControllerAdvice is implemented as an AOP advice around the methods of the controller. So it has no way to intercept exception thrown outside of the controller.

As a workaround, you can declare a global HandlerExceptionResolver to process your custom exception.