Spring:RestController 和 Controller 的不同异常处理程序

Posted

技术标签:

【中文标题】Spring:RestController 和 Controller 的不同异常处理程序【英文标题】:Spring: Different exception handler for RestController and Controller 【发布时间】:2017-09-05 15:30:01 【问题描述】:

在 Spring 中,您可以通过 @ControllerAdvice 和 @ExceptionHandler 注解设置“全局”异常处理程序。我正在尝试利用这种机制来拥有两个全局异常处理程序:

RestControllerExceptionHandler - 对于带有 @RestController 注释的任何控制器,它都应该以 json 格式返回错误响应 ControllerExceptionHandler - 应该将错误消息打印到任何其他控制器的屏幕上(带有 @Controller 注释)

问题是当我声明这两个异常处理程序时,spring总是使用ControllerExceptionHandler而不是RestControllerExceptionHandler来处理异常。

如何使这项工作? 顺便说一句:我尝试使用 @Order 注释,但这似乎不起作用。

这是我的异常处理程序:

// should handle all exception for classes annotated with         
@ControllerAdvice(annotations = RestController.class)
public class RestControllerExceptionHandler 


  @ExceptionHandler(Exception.class)
  public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e) 

    // below object should be serialized to json
    ErrorResponse errorResponse = new ErrorResponse("asdasd"); 

    return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
  

// should handle exceptions for all the other controllers
@ControllerAdvice(annotations = Controller.class)
public class ControllerExceptionHandler 


  @ExceptionHandler(Exception.class)
  public ResponseEntity<String> handleUnexpectedException(Exception e) 
    return new ResponseEntity<String>("Unexpected exception, HttpStatus.INTERNAL_SERVER_ERROR);
  




当我删除ControllerExceptionHandler 时,spring 正确调用了RestControllerExceptionHandler(仅适用于带有@RestController 注释的类)....但是当我添加ControllerExceptionHandler 时,所有内容都通过ControllerExceptionHandler 进行。为什么?

【问题讨论】:

This article 描述了类似的概念并没有为默认控制器建议定义任何参数(即其中没有 annotations 属性)。如果可行,你试过了吗? @M.Prokhorov:这正是我想要实现的目标。当我看这篇文章时,我也在做同样的事情。但对我来说,由于某种原因这不起作用:/ @M.Prokhorov:我想通了。你的文章有助于深入研究这个问题。我在春天遇到了一个错误(请参阅我自己的答案)。再次感谢。 【参考方案1】:

经过一些更深入的调查,似乎字母顺序很重要:/。

当我将RestControllerExceptionHandler 重命名为ARestControllerExceptionHandler(按字母顺序在ControllerExceptionHandler 之前)时,一切都按预期工作! ARestControllerExceptionHandler 正确处理来自RestControllers 的异常,ControllerExceptionHandler 处理来自其他控制器的异常。

我在春季为此创建了一个错误:https://jira.spring.io/browse/SPR-15432

--- 编辑:

我收到了 SPR-15432 的答案,建议可以使用 @Order (org.springframework.core.annotation.Order) 注释或实现 Ordered 接口来解决此问题。

这对我之前不起作用,但似乎我导入了错误的@Order 注释。(来自 log4j2 而不是 spring)。修复此问题后,它可以工作。固定版本如下:

// should handle all exception for classes annotated with         
@ControllerAdvice(annotations = RestController.class)
@Order(1) // NOTE: order 1 here
public class RestControllerExceptionHandler 


  @ExceptionHandler(Exception.class)
  public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e) 

    // below object should be serialized to json
    ErrorResponse errorResponse = new ErrorResponse("asdasd"); 

    return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
  

// should handle exceptions for all the other controllers
@ControllerAdvice(annotations = Controller.class)
@Order(2)  // NOTE: order 2 here
public class ControllerExceptionHandler 


  @ExceptionHandler(Exception.class)
  public ResponseEntity<String> handleUnexpectedException(Exception e) 
    return new ResponseEntity<String>("Unexpected exception, HttpStatus.INTERNAL_SERVER_ERROR);
  

【讨论】:

这可能确实是一个错误,但也可能不是,它可能只是 Spring 反射设施的一个实现细节,在您的情况下不保证修复。尝试考虑订购您的建议,这可以按照this answer 中的建议进行。【参考方案2】:

发生这种情况是因为 @RestController 注释本身也是一个 @Controller,因此 Spring 正在考虑将 @ControllerAdvice 和 annotation = Controller.class 用于两者。

您可以尝试另一个method 来定义@ControllerAdvice 将生效的控制器子集,所以这里有一些解决方案:


解决方案 1

    创建一个新的注解@NotRestController:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
        public @interface NotRestController 
    
    

    用@Controller 和@NotRestController 标记不是@RestController 的控制器:

    @Controller
    @NotRestController
    @RequestMapping("/controller")
    public class SampleController 
    
    

    ControllerExceptionHandler 上使用NotRestController.class

    @ControllerAdvice(annotations = NotRestController.class)
    public class ControllerExceptionHandler 
    

我用Solution 1 创建了一个示例项目并在 Github 上分享:

https://github.com/brunocleite/springexceptionhandlertest


解决方案 2

    将您的@RestController 类移动到包foo.bar 中,将您的@Controller 类移动到另一个包foo.nuk

    使用@ControllerAdvicebasePackages 属性而不是annotations

    @ControllerAdvice(basePackages="foo.bar")
    public class RestControllerExceptionHandler 
    
    @ControllerAdvice(basePackages="foo.nuk")
    public class ControllerExceptionHandler 
    

最好的问候!

【讨论】:

+1,但是,我知道@Controller 建议的其他选项(如解决方案 1 中所述),但我不想使用它们,因为我想利用现有的 RestController 注释。解决方案1还可以,但是我不想添加额外的不必要的注释。【参考方案3】:

这是我的解决方案。它消除了创建额外注释的需要。

    创建 2 个基本控制器。
@Controller
public class BaseController

@Controller
public class BaseRestController
    使用选定的基本控制器扩展每个控制器。
@Controller
public class UserController extends BaseController

@RestController
public class UserRestController extends BaseRestController
    创建 2 个全局控制器并在每个控制器中仅包含扩展基类。
@ControllerAdvice(assignableTypes = BaseController.class)
public class GlobalController

@ControllerAdvice(assignableTypes = BaseRestController.class)
public class GlobalRestController

【讨论】:

【参考方案4】:

进一步构建@bruno-leite 的解决方案 1:

您可以定义一个元注释,因此每次插入 2 个时您只需要 1 个注释:

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Controller;

import java.lang.annotation.*;

/**
 * Alias annotation for @Controller so that @ControllerAdvice instances can
 * target the web controllers separately from the rest controllers.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
public @interface WebController 
    @AliasFor(
            annotation = Controller.class
    )
    String value() default "";

在网络控制器上使用它:

@WebController
@RequestMapping("/customers")
public class CustomerController 

最后定义一个@ControllerAdvice,它只针对这样的网络控制器:

@ControllerAdvice(annotations = WebController.class)
public class GlobalControllerAdvice 

    @ModelAttribute("displayName")
    public String getDisplayName(@AuthenticationPrincipal KeycloakAuthenticationToken principal) 
        if (principal != null) 
            String givenName = principal.getAccount().getKeycloakSecurityContext().getToken().getGivenName();
            String familyName = principal.getAccount().getKeycloakSecurityContext().getToken().getFamilyName();
            return givenName + " " + familyName;
         else 
            return null;
        
    

(此示例确保我的 Thymeleaf 模板始终具有 displayName 属性,但也可以轻松调整异常处理程序)

【讨论】:

【参考方案5】:

使用顺序修复错误:

@Order(1)
@ControllerAdvice(annotations = RestController.class)
public class ErrorRestControllerAdvice 


@Order(2)
@ControllerAdvice(annotations = Controller.class)
public class ErrorControllerAdvice 

【讨论】:

我已经发布了这个解决方案。这个答案并没有给这个话题带来任何新的东西

以上是关于Spring:RestController 和 Controller 的不同异常处理程序的主要内容,如果未能解决你的问题,请参考以下文章

Spring中@Controller和@RestController之间的区别

spring BootSpring中@Controller和@RestController之间的区别

Spring:RestController 和 Controller 的不同异常处理程序

Spring 的@Controller 和@RestController的区别

Spring RestController 如何同时接受 JSON 和 XML?

Spring 注解中@RestController与@Controller的区别