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

jcg*_*cia 8 java spring exception-handling spring-mvc spring-boot

我有一个控制器,其中包含一个 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";
  }
}
Run Code Online (Sandbox Code Playgroud)

要调用此操作,我使用http://localhost:8080/owners/1 URL。如您所见,我提供了所有者标识符 1。

为了能够将标识符 1 转换为有效的 Owner 元素,必须定义 Spring Formatter 并将其注册到addFormatters方法 from 上WebMvcConfigurerAdapter

我有以下几点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();
  }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我已经使用findOne方法来获取 id 为 1 的所有者。但是,如果由于没有任何 id 为 1 的所有者,此方法返回 null 会发生什么?

为了防止这种情况,我抛出了一个名为EntityResultNotFoundException. 此异常包含以下代码:

public class EntityResultNotFoundException extends RuntimeException {
    public EntityResultNotFoundException() {
        super("ERROR: Entity not found");
    }
}
Run Code Online (Sandbox Code Playgroud)

我想将项目配置为能够在errors/404.html抛出此异常时返回,因此,按照Spring 文档,我有两个选择:

选项a)

配置一个@ControllerAdvice@ExceptionHandler注释的方法来管理EntityResultNotFoundException

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

但这不起作用。如果我更改上面的实现以在控制器方法中抛出异常,则效果完美。

似乎在调用控制器实现之前调用的 Spring Formatter 没有被@ControllerAdvice.

对我来说这没有意义,因为在调用控制器方法之前已经调用了 Spring Formatter 来准备所提供的必要参数@RequestMapping......所以 Spring 知道将调用哪个方法......为什么 Spring Formatter 用于准备请求未被监视@ControllerAdvice以捕获在此“准备过程”期间发生的所有可能的异常?

更新:正如 Serge Ballesta 在他的回答中所说,@ControllerAdvice 被实现为围绕控制器方法的 AOP 建议。所以它没有办法拦截控制器外抛出的异常。

我拒绝这个选项。

更新 2:在 Spring Framework JIRA 中得到一些答案之后,他们建议我使用一个泛型@ExceptionHandler来捕获所有异常,然后使用一些条件来检查根本原因并能够知道异常原因是否是我遇到的异常在 Spring Formatter 上调用。我认为这可能是 Spring MVC 的另一项改进,因为我无法@ExceptionHandler用来捕获从 Spring Formatters 抛出的异常。你可以检查我在这里上传的证明

我也拒绝这个选项。

选项 b)

@Bean SimpleMappingExceptionResolver@Configuration类中包含一个 new以将异常与视图标识符映射。

@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;
  }
}
Run Code Online (Sandbox Code Playgroud)

但是,上面的实现不适用于 Spring Formatters 上抛出的异常。

更新:我一直在调试 Spring 代码,我发现有两件事可能是对 Spring Framework 的有趣改进。

首先,DispatcherServlet是加载所有注册HandlerExceptionResolverinitStrategiesinitHandlerExceptionResolvers方法。此方法以正确的顺序获取所有 HandlerExceptionResolvers,然后,使用以下代码再次对它们进行排序:

AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
Run Code Online (Sandbox Code Playgroud)

问题在于,此方法委托findOrder尝试从 HandlerExceptionResolver 获取顺序的方法,该方法是 的实例Ordered。正如你所看到的,我没有在我注册的@Bean 上定义顺序,所以当试图从我声明的 bean 获取订单时SimpleMappingExceptionResolver使用的是 LOWEST_PRECEDENCE。这导致 Spring 使用DefaultHandlerExceptionResolver因为是第一个返回结果的。

因此,为了解决这个问题,我使用以下代码向我声明的 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;
  }  
Run Code Online (Sandbox Code Playgroud)

现在,当AnnotationAwareOrderComparator所有注册HandlerExceptionResolver的排序SimpleMappingExceptionResolver是第一个时,它将被用作解析器。

无论如何,还没有工作。我继续调试,我看到现在正在使用doResolveExceptionfromSimpleMappingExceptionResolver来解决异常,所以没关系。但是,findMatchingViewName尝试获取映射视图的方法返回 null。

问题是findMatchingViewName试图检查接收到的异常是否与 的 exceptionMappings 上定义的某些异常匹配SimpleMappingExceptionResolver,但它只检查getDepth方法内部的超类。应该是需要检查原因异常。

我已经应用了以下解决方法来继续工作(如果深度无效,只需扩展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;
    }

}
Run Code Online (Sandbox Code Playgroud)

我认为这个实现真的很有趣,因为还使用了原因异常类而不是只使用超类异常。我将在 Spring Framework github 上创建一个新的 Pull-Request,包括这个改进。

通过这两个更改(顺序和扩展SimpleMappingExceptionResolver),我能够捕获从 Spring Formatter 抛出的异常并返回自定义视图。

Ser*_*sta 3

似乎在调用控制器实现之前调用的 Spring Formatter 没有被 @ControllerAdvice 监控

很好,这正是发生的事情。

为什么用于准备请求的 Spring Formatter 没有被 @ControllerAdvice 监控以捕获在此“准备过程”期间发生的所有可能的异常

因为 @ControllerAdvice 是作为围绕控制器方法的 AOP 建议实现的。所以它无法拦截控制器外部抛出的异常。

作为解决方法,您可以声明一个全局HandlerExceptionResolver来处理您的自定义异常。