springboot controller层异常处理 (spring controller)

背景

最近在Review Code时发现开发人员对接口开发的返回结果参差不齐,对后端服务的错误消息输出也是原生态的错误输出。对前端开发人员开发不友好,对用户使用感官也很差。对此寻求解决方案。

方案

使用Spring @RestControllerAdvice(返回增强器),统一处理控制层中未捕获异常处理的异常信息。

@RestControllerAdvice是Spring框架中一个用于统一处理控制器异常和返回结果的注解,可以捕获整个应用程序中抛出的异常,并对它们进行处理。它是@ControllerAdvice注解的特殊版本,用于RESTful风格的应用程序。

@RestControllerAdvice注解实际上是@ControllerAdvice和@ResponseBody注解的组合,这意味着当使用@RestControllerAdvice注解时,异常处理方法的返回值将自动转换为HTTP响应的主体,返回的数据是JSON格式的。因此,@RestControllerAdvice常用于全局异常的捕获处理和请求参数的增强,适用于所有使用@RequestMapping方法的情况。

springcontroller,springcontroller线程

实操

创建统一返回报文体

创建统一返回报文体 ResponseBody。 其中返回数据类型使用泛型进行接收,便于兼容多种类型数据的输出。

public class ResponseBody<T> {

    /**
     * 正确响应状态
     */
    public static final int OK = 200;
    /**
     * 错误响应状态
     */
    public static final int ERROR = 100;

    public static <T> ResponseBody<T> successResponse(T data){
        ResponseBody<T> response = new ResponseBody<>();
        response.setStatus(ResponseBody.OK);
        response.setData(data);
        return response;
    }

    /**
     * 响应数据
     */
    private T data;

    /**
     * 状态值
     */
    private int status;

    /**
     * 描述信息
     */
    private String description;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

}

创建业务异常类

创建 DiyException 业务异常类,使已知业务异常信息可使用。

public class DiyException extends RuntimeException {

    private String msg;

    public DiyException(String msg) {
        super(msg);
        this.msg = msg;
    }

    /**
     * 生成业务异常
     */
    public static RuntimeException getDiyException(String msg) {
        return new DiyException(msg);
    }
}

统一异常拦截器

使用RestControllerAdvice创建统一异常拦截器,统一处理控制层中未捕获异常处理的异常信息。

@RestControllerAdvice(value = "com.loop.helloworld")//设置拦截范围
@Order(Ordered.HIGHEST_PRECEDENCE) //使用Oracle 驱使本拦截在最前避免与其他拦截器冲突
public class DemoGlobalExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(DemoGlobalExceptionHandler.class);

    /**
     * @方法名称 ValidData 校验报错拦截
     * @功能描述 <pre> ValidData 校验报错统一拦截返回 <pre>
     * @作者 loop 猿小哥
     * @创建时间 2022/3/29 17:46
     */
    @ExceptionHandler({RuntimeException.class})
    @ResponseStatus(HttpStatus.OK)
    public ResponseBody<Object> handlerMethodArgumentNotValidException(Exception exception) {
        String message = exception.getMessage();
        this.logger.error(message, exception);
        ResponseBody<Object> response = new ResponseBody<Object>();
        response.setStatus(ResponseBody.ERROR);
        if (StringUtils.isBlank(message)) {
            message = "系统发生未知异常,请查看日志信息";
        }
        response.setStatus(ResponseBody.ERROR);
        response.setDescription(message);
        return response;
    }

    /**
     * @方法名称 数据转换错误错误统一拦截返回
     * @功能描述 <pre> 数据转换错误错误统一拦截返回 <pre>
     * @作者 loop 猿小哥
     * @创建时间 2022/3/29 17:46
     */
    @ExceptionHandler({DiyException.class})
    @ResponseStatus(HttpStatus.OK)
    public ResponseBody<Object> handlerInvalidFormatException(DiyException exception) {
        logger.debug("business exception:", exception);
        logger.error(exception.getMessage(), exception);
        ResponseBody<Object> response = new ResponseBody<Object>();
        response.setStatus(ResponseBody.ERROR);
        response.setDescription(exception.getMessage());
        return response;
    }
}

测试验证

创建Controller接口测试类

@RestController
@RequestMapping("/demo")
public class DemoController {

    @PostMapping("/get")
    public ResponseBody<Map> get(@RequestBody Map<String, Object> reqParams) {

        Map<String, Object> responseVo = new HashMap<>();

        if (reqParams.get("id").equals("0")){
            throw new RuntimeException("数据错误");
        }

        if (reqParams.get("id").equals("1")){
            if (reqParams.get("name").equals("验证未知的错误")){
                System.out.println();
            }
        }
        if (reqParams.get("id").equals("2")){
            throw DiyException.getDiyException("业务错误");
        }

        return ResponseBody.successResponse(responseVo);
    }
}

未开启增强,接口测试

springcontroller,springcontroller线程

springcontroller,springcontroller线程

springcontroller,springcontroller线程

测试未拦截情况可看出,接口http状态是500错误,返回报文信息不友好。

开启增强,接口测试

springcontroller,springcontroller线程

springcontroller,springcontroller线程

springcontroller,springcontroller线程

通过上面拦截测试情况,得出接口http状态是200错误,返回报文信息经过统一处理。系统发生未知错误时,也拦截进行输出友好的错误消息。

总结

通过上面案例可以看出@RestControllerAdvice使得异常处理方法的返回值自动转换为HTTP响应的主体,返回的数据是JSON格式的。使用@RestControllerAdvice可以统一处理控制器异常和返回结果,提高了代码的可读性和可维护性。

下面对ExceptionHandler 统一异常拦截器进行一些要点总结:

  • @RestControllerAdvice注解中的value值可进行设置Controller拦截范围
  • @Order 注解可设置拦截器级别(Ordered.HIGHEST_PRECEDENCE 为最高级)。
  • 方法上的@ExceptionHandler 注解设置需要处理的异常类型,方法入参为需处理的异常类。
  • 方法上的@ResponseStatus注解可进行设置接口 http响应状态。