简体   繁体   English

@ExceptionHandler不会捕获从Spring Formatters抛出的异常

[英]@ExceptionHandler doesn't catch exceptions being thrown from Spring Formatters

I have a controller that contains a show method to display info about the provided entity. 我有一个控制器,其中包含一个show方法来显示有关提供的实体的信息。

@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. 要调用此操作,我使用http:// localhost:8080 / owners / 1 URL。 As you could see, I provide Owner identifier 1. 如您所见,我提供了所有者标识符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 . 为了能够标识1转换为有效用户元件是必要的,以限定弹簧格式化,并将其注册在addFormatters从方法WebMvcConfigurerAdapter

I have the following OwnerFormatter : 我有以下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? 正如您所看到的,我使用findOne方法来获取ID为1的Owner。但是,如果此方法返回null,因为没有任何ID为1的所有者,会发生什么?

To prevent this, I throw a custom Exception called EntityResultNotFoundException . 为了防止这种情况,我抛出一个名为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: 我想配置项目以便在抛出此异常时返回errors/404.html ,因此,在Spring文档之后 ,我有两个选择:

Option a) 选项a)

Configure a @ControllerAdvice with @ExceptionHandler annotated method that manages EntityResultNotFoundException : 使用管理EntityResultNotFoundException @ExceptionHandler注释方法配置@ControllerAdvice

@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 . 看起来像在调用控制器实现之前调用的Spring Formatter没有被@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"? 对我来说这没有意义,因为在调用控制器方法之前调用了Spring Formatter来准备所提供的@RequestMapping的必要参数...所以Spring知道将调用哪个方法...为什么Spring Formatter用来准备请求是不是由@ControllerAdvice监控,以捕获在“准备过程”期间发生的所有可能的异常?

UPDATE : As Serge Ballesta said on his answer, the @ControllerAdvice is implemented as an AOP advice around the methods of the controller. 更新 :正如Serge Ballesta在答案中所说,@ ControllerAdvice是作为围绕控制器方法的AOP建议实现的。 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. 更新2:在Spring Framework JIRA中得到一些答案之后, 他们建议我使用一个捕获所有异常的通用@ExceptionHandler ,然后使用一些条件来检查根本原因并且能够知道异常原因是否是我的异常已经调用了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. 我认为这可能是Spring MVC的另一个改进,因为我无法使用@ExceptionHandler来捕获从Spring Formatters抛出的异常。 You could check a proof I've uploaded here 您可以查看我在此处上传的证明

I reject this option too. 我也拒绝这个选项。

Option b) 选项b)

Include a new @Bean SimpleMappingExceptionResolver inside a @Configuration class to map exceptions with the view identifier. @Configuration类中包含一个新的@Bean SimpleMappingExceptionResolver以使用视图标识符映射异常。

@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. 但是,上面的实现不适用于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. 更新 :我一直在调试Spring代码,我发现两件事可能是Spring Framework的一个有趣的改进。

First of all, DispatcherServlet is loading all registered HandlerExceptionResolver from initStrategies and initHandlerExceptionResolvers methods. 首先, DispatcherServletinitStrategiesinitHandlerExceptionResolvers方法加载所有已注册的HandlerExceptionResolver This method is taken all HandlerExceptionResolvers in the correct order, but then, uses the following code to order them again: 此方法以正确的顺序获取所有HandlerExceptionResolvers,但随后,使用以下代码再次对它们进行排序:

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 . 问题是此方法委托findOrder方法尝试从作为Ordered实例的HandlerExceptionResolver获取顺序。 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. 正如您所看到的,我没有在已注册的@Bean上定义顺序,因此当尝试从我声明的bean获取顺序时SimpleMappingExceptionResolver使用LOWEST_PRECEDENCE。 This causes that Spring uses DefaultHandlerExceptionResolver because is the first one that returns a result. 这导致Spring使用DefaultHandlerExceptionResolver因为它是第一个返回结果的人。

So, to solve this I've added order value to my declared bean with the following code. 所以,为了解决这个问题,我使用以下代码为我声明的bean添加了顺序值。

  @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. 现在,当AnnotationAwareOrderComparator所有已注册的HandlerExceptionResolver进行排序时, SimpleMappingExceptionResolver是第一个,它将用作解析器。

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. 我继续调试,我发现现在正在使用doResolveExceptionSimpleMappingExceptionResolver来解决异常,所以没关系。 However, the method findMatchingViewName that tries to obtain the mapped view returns null. 但是,尝试获取映射视图的方法findMatchingViewName返回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. 问题是findMatchingViewName试图检查收到的异常是否与findMatchingViewName exceptionMappings上定义的异常SimpleMappingExceptionResolver ,但它只检查getDepth方法中的超类。 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) 我已应用以下变通方法继续工作(只需扩展SimpleMappingExceptionResolver并实现findMatchingViewName方法,尝试再次查找匹配视图,如果深度无效则导致异常)

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. 我认为这个实现非常有趣,因为还使用cause异常类而不是仅使用超类异常。 I'm going to create a new Pull-Request on Spring Framework github including this improvement. 我将在Spring Framework github上创建一个新的Pull-Request,包括这个改进。

With these 2 changes (order and extending SimpleMappingExceptionResolver ) I'm able to catch an exception thrown from Spring Formatter and return a custom view. 通过这两个更改(顺序和扩展SimpleMappingExceptionResolver ),我能够捕获从Spring Formatter抛出的异常并返回自定义视图。

Seems like the Spring Formatter that is called before invoke controller implementation is not beeing monitorized by the @ControllerAdvice 看起来像在调用控制器实现之前调用的Spring Formatter没有被@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" 为什么用于准备请求的Spring Formatter没有被@ControllerAdvice监视,以捕获在“准备过程”期间发生的所有可能的异常

Because the @ControllerAdvice is implemented as an AOP advice around the methods of the controller. 因为@ControllerAdvice是作为围绕控制器方法的AOP建议实现的。 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. 作为一种变通方法,您可以声明一个全局HandlerExceptionResolver来处理您的自定义异常。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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