结合springboot 2.2.2和springboot2.4.4源码分析如何基于BasicErrorController实现全局错误异常处理

问题背景

在项目开发过程中,发现一个问题,不同springboot版本(2.2.2和2.3以上版本)在做统一错误处理的时候,高版本的无法获取到设置的错误信息,postman测试结果如下:

//2.2.2 版本
{
    "code": 401,
    "message": "请登录后操作",
    "messsageCode": null,
    "data": null
}

//2.4.4 版本
{
    "code": 401,
    "message": "",
    "messsageCode": null,
    "data": null
}

通过测试结果可见,2.4.4版本message值为空;接下来我就结合源码来分析下产生这个的原因。

一、实现逻辑分析

springboot通过设置统一异常处理的路口,对异常内容进行分析,将处理后的结果返回给客户端进行呈现。要实现自定义异常处理类,有以下方式:
1、自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。
2、自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。
3、自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
4、继承AbstractErrorController

总的来说就是实现ErrorController接口, BasicErrorController类继承AbstractErrorController,AbstractErrorController实现了ErrorController接口,我们只需要继承BasicErrorController,然后重写我们自己定义的结果返回方法即可。

二、BasicErrorController源码分析

2.1 先看类头部注解
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
//内容部分后面分析
}

这个RequestMapping地址,你可以理解为从左到右的选择关系,如果你在配置文件配置了server.error.path的话,就会使用你配置的异常处理地址,如果没有就会使用你配置的error.path路径地址,如果还是没有,默认使用/error来作为发生异常的处理地址。


2.2 异常处理路口

springboot版本2.2.2

@RequestMapping(
	        produces = {"text/html"}
	    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = this.getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity(status);
        } else {
            Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));//注意这行代码
            return new ResponseEntity(body, status);
        }

springboot版本2.4.4

	 @RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = this.getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity(status);
        } else {
            Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));//与2.2.2版本不一致地方
            return new ResponseEntity(body, status);
        }
    }

它提供了两个接口处理方法,上面一个标注了produces为text/html,当你是网页请求的时候返回的网页数据,下面的接口是当你的请求为其他的时候,返回的是ResponseEntity对象(json数据或者其他,取决与你的返回数据类型配置)。

我们看到第一个接口返回了一个error页面,如果你的项目静态页面下刚好存在一个error所对应的页面,那么Spring Boot会得到你本地的页面。
我在这主要分析的事第二个处理方式,实现自定义返回结果。通过两个版本代码对比,发现其中的区别之处,上面代码有标识。


2.2.1首先来看springboot 2.2.2版本

设置相应结果方法:
其中includeStackTrace因为没有设置erroProperties参数includeStackTrace为ALWAYS所以其值为false

public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, webRequest);
        this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);//此步骤添加相应信息
        this.addPath(errorAttributes, webRequest);
        return errorAttributes;
    }

再看方法addErrorDetails

private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace) {
        Throwable error = this.getError(webRequest);
        if (error != null) {
            while(true) {
                if (!(error instanceof ServletException) || error.getCause() == null) {
                    if (this.includeException) {
                        errorAttributes.put("exception", error.getClass().getName());
                    }

                    this.addErrorMessage(errorAttributes, error);//将error信息放到相应信息中
                    if (includeStackTrace) {
                        this.addStackTrace(errorAttributes, error);
                    }
                    break;
                }

                error = error.getCause();
            }
        }

通过断点得到error值如下图:
在这里插入图片描述
通过断点可以看到将error的detailMessage值放入到响应中,如下图所示:
在这里插入图片描述

2.2.2 springboot 2.4.4版本

注意:以下代码是沿着出现问题如何去分析解决的思路顺序,带着问题一步步去探索,大家可以打开源码跟着我的思路一步步去看,去理解很快可以看明白。

1. 沿着源码我们先找到方法getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options)

public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
        if (Boolean.TRUE.equals(this.includeException)) {
            options = options.including(new Include[]{Include.EXCEPTION});
        }

        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }

        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }

        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.put("message", "");
        }

        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }

        return errorAttributes;
    }

通过上面代码,我们可以知道errorAttributes的取值取决于option的值,所以接下来我们看option取值来源。

2. 方法getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType)

    protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
        ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
        if (this.errorProperties.isIncludeException()) {
            options = options.including(new Include[]{Include.EXCEPTION});
        }

        if (this.isIncludeStackTrace(request, mediaType)) {
            options = options.including(new Include[]{Include.STACK_TRACE});
        }

        if (this.isIncludeMessage(request, mediaType)) {
            options = options.including(new Include[]{Include.MESSAGE});
        }

        if (this.isIncludeBindingErrors(request, mediaType)) {
            options = options.including(new Include[]{Include.BINDING_ERRORS});
        }

        return options;
    }

代码中可以看出,ErrorAttributeOptions取值又取决于errorProperties的值,所以我们来先看下ErrorProperties这个类。

3. ErrorProperties这个类

   public class ErrorProperties {
    @Value("${error.path:/error}")
    private String path = "/error";
    private boolean includeException;
    private ErrorProperties.IncludeStacktrace includeStacktrace;
    private ErrorProperties.IncludeAttribute includeMessage;
    private ErrorProperties.IncludeAttribute includeBindingErrors;
    private final ErrorProperties.Whitelabel whitelabel;

    public ErrorProperties() {
        this.includeStacktrace = ErrorProperties.IncludeStacktrace.NEVER;
        this.includeMessage = ErrorProperties.IncludeAttribute.NEVER;
        this.includeBindingErrors = ErrorProperties.IncludeAttribute.NEVER;
        this.whitelabel = new ErrorProperties.Whitelabel();
    }
    //后面代码我们不看,只看这个构造器
 }

可以看到,这个构造器给ErrorProperties的参数设置了默认值,通过代码我们知道默认设置的值堆栈跟踪,消息体,其他绑定的错误信息都是never,也就是永远不包含。

在我们做统一错误处理的时候,我们希望我们设置的错误提示能传回给前端,所以我们需要改变includeMessage的值,通过源码可以看到ErrorProperties.IncludeStacktrace的值有:

 public static enum IncludeStacktrace {
        NEVER,
        ALWAYS,
        ON_PARAM,
        /** @deprecated */
        @Deprecated
        ON_TRACE_PARAM;

        private IncludeStacktrace() {
        }
    }

通过上面2.0的方法如下:

if (this.isIncludeMessage(request, mediaType)) {
            options = options.including(new Include[]{Include.MESSAGE});
        }
        
 protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
        switch(this.getErrorProperties().getIncludeMessage()) {
        case ALWAYS:
            return true;
        case ON_PARAM:
            return this.getMessageParameter(request);
        default:
            return false;
        }
    }

我们可以知道需要设置其值为ALWAYS,这样就能获取到设置的错误message的值了。


如何设置ErrorProperties参数的值

如果直接用springboot默认的统一错误处理即用BasicErrorController的话,我们只要在配置文件中加入如下配置:

 #springboot 2.3之后,设置统一错误异常类参数配置
    #server.error.include-exception=true
    #server.error.include-stacktrace = ALWAYS
    server.error.include-message= ALWAYS
   #server.error.include-binding-errors= ALWAYS

如果是想自定义类去设置错误相应结果,可以参考如下代码:

@RestController
public class ErrorController extends BasicErrorController {

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

    /**
    * 设置ErrorProperties参数值
    */
    public static ErrorProperties initProperties(){
        ErrorProperties properties = new ErrorProperties();
        properties.setIncludeMessage(ErrorProperties.IncludeAttribute.ALWAYS);
        return properties;
    }

    
    /**
     * 引用父类构造方法,将设置的ErrorProperties传入父类
     **/
    public ErrorController() {
        super(new DefaultErrorAttributes(), initProperties());
    }


    /**
     *
     * @param request 请求
     * @return 自定义的返回实体类
     */
    @Override
    public ResponseEntity error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        logger.error("controler before exception: {}; ", body);
        return new ResponseEntity<>(BaseResponse.authError(body.get("message").toString()), status);
    }

}

总结

以上是我在编程过程中,遇到的问题,并记录了我分析解决过程,如有错误或者不足欢迎大家指正,觉得有帮助的话,麻烦大家点个赞,谢谢大家。
发现问题,探索原因,解决问题这一过程是一件挺爽的事。

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
【2021年,将Spring全家桶的课程进行Review,确保不再有课程的顺序错乱,从而导致学员看不懂。进入2022年,将Spring的课程进行整理,整理为案例精讲的系列课程,并开始加入高阶Spring Security等内容,一步步手把手教你从零开始学会应用Spring,课件将逐步进行上传,敬请期待!】 本课程是Spring全家桶系列课程的第三部分Spring Boot,Spring案例精讲课程以真实场景、项目实战为导向,循序渐进,深入浅出的讲解Java网络编程,助力您在技术工作中更进一步。 本课程聚焦Spring Boot核心知识点:整合Web(如:JSP、Thymeleaf、freemarker等的整合)的开发、全局异常处理、配置文件的配置访问、多环境的配置文件设置、日志Logback及slf4j的使用、国际化设置及使用, 并在最后以一个贯穿前后台的Spring Boot整合Mybatis的案例为终奖,使大家快速掌握Spring的核心知识,快速上手,为面试、工作都做好充足的准备。 由于本课程聚焦于案例,即直接上手操作,对于Spring的原理等不会做过多介绍,希望了解原理等内容的需要通过其他视频或者书籍去了解,建议按照该案例课程一步步做下来,之后再去进一步回顾原理,这样能够促进大家对原理有更好的理解。 【通过Spring全家桶,我们保证你能收获到以下几点】 1、掌握Spring全家桶主要部分的开发、实现2、可以使用Spring MVC、Spring Boot、Spring Cloud及Spring Data进行大部分的Spring开发3、初步了解使用微服务、了解使用Spring进行微服务的设计实现4、奠定扎实的Spring技术,具备了一定的独立开发的能力  【实力讲师】 毕业于清华大学软件学院软件工程专业,曾在Accenture、IBM等知名外企任管理及架构职位,近15年的JavaEE经验,近8年的Spring经验,一直致力于架构、设计、开发及管理工作,在电商、零售、制造业等有丰富的项目实施经验  【本课程适用人群】如果你是一定不要错过!  适合于有JavaEE基础的,如:JSP、JSTL、Java基础等的学习者没有基础的学习者跟着课程可以学习,但是需要补充相关基础知识后,才能很好的参与到相关的工作中。 【Spring全家桶课程共包含如下几门】 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值