Skip to content

@ExceptionHandler should match cause as well (e.g. for exception thrown from argument formatter) [SPR-14291] #18863

Closed
@spring-projects-issues

Description

@spring-projects-issues

Juan Carlos García del Canto opened SPR-14291 and commented

The following problem is related with this StackOverFlow question.

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 happens 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)

Read StackOverFlow question, this is not related with this issue.

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.

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 _SimpleMappingExceptionResolver_is 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.

What do you think about it?


Affects: 4.2.5

Reference URL: http://stackoverflow.com/questions/37322182/exceptionhandler-doesnt-catch-exceptions-being-thrown-from-spring-formatters

Issue Links:

Referenced from: commits 981c894

1 votes, 4 watchers

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions