SpringBoot 统一异常处理(附核心工具类-ErrorInfoBuilder) 1

简介: SpringBoot 统一异常处理(附核心工具类-ErrorInfoBuilder)

# 序言


此前,我们主要通过在控制层(Controller)中手动捕捉异常(TryCatch)和处理错误,在SpringBoot 统一异常处理的做法主要有两种:一是基于注解ExceptionHandler,二是基于接口ErrorController,两者都可以让控制器层代码快速“瘦身”,让业务逻辑看起来更加清晰明朗!


一. 默认错误处理


SpringBoot 默认为我们提供了BasicErrorController 来处理全局错误/异常,并在Servlet容器中注册error为全局错误页。所以在浏览器端访问,发生错误时,我们能及时看到错误/异常信息和HTTP状态等反馈。工作原理如下:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    // 统一异常处理(View)
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
    }



例如下面这两个错误,对于日常开发而言,再熟悉不过了。


404 Not Found


500 服务器错误


二. 统一异常处理


默认的英文空白页,显然不能够满足我们复杂多变的需求,因此我们可以通过专门的类来收集和管理这些异常信息,这样做不仅可以减少控制层的代码量,还有利于线上故障排查和紧急短信通知等。


具体步骤


为了让小伙伴少走一些弯路,楼主根据官方源码和具体实践,提炼这些核心工具类:


  • ErrorInfo  错误信息
  • ErrorInfoBuilder 错误信息的构建工具

注:在CSDN和大牛博客中,不乏关于Web应用的统一异常处理的教程,但更多的是基础学习使用,并不能投入实际项目使用,为了让大家少走一些弯路和快速投入生产,楼主根据官方源码和项目实践,提炼出了核心工具类(ErrorInfoBuilder ),将构建异常信息的逻辑从异常处理器/控制器中抽离出来,让大家通过短短几行代码就能获取丰富的异常信息,更专注于业务开发!!


1.  统一异常处理器(GlobalErrorHandler)


  • @ControllerAdvice  限定范围 例如扫描某个控制层的包
  • @ExceptionHandler  指定异常  例如指定处理运行异常。

具体如下:

package com.hehe.error;
public class ErrorInfo {
    private String time;//发生时间
    private String url;//访问Url
    private String error;//错误类型
    String stackTrace;//错误的堆栈轨迹
    private int statusCode;//状态码
    private String reasonPhrase;//状态码
    //Getters And Setters
       ...
}


2. 统一异常信息(ErrorInfo)


虽然官方提供了ErrorAttributes来存储错误信息,但其返回的是存储结构是Map<String,Object>,为了更好的服务统一异常,这里我们统一采用标准的ErrroInfo来记载错误信息。

package com.hehe.error;
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class ErrorInfoBuilder implements HandlerExceptionResolver, Ordered {
    /**
     * 错误KEY
     */
    private final static String ERROR_NAME = "hehe.error";
    /**
     * 错误配置(ErrorConfiguration)
     */
    private ErrorProperties errorProperties;
    public ErrorProperties getErrorProperties() {
        return errorProperties;
    }
    public void setErrorProperties(ErrorProperties errorProperties) {
        this.errorProperties = errorProperties;
    }
    /**
     * 错误构造器 (Constructor) 传递配置属性:server.xx -> server.error.xx
     */
    public ErrorInfoBuilder(ServerProperties serverProperties) {
        this.errorProperties = serverProperties.getError();
    }
    /**
     * 构建错误信息.(ErrorInfo)
     */
    public ErrorInfo getErrorInfo(HttpServletRequest request) {
        return getErrorInfo(request, getError(request));
    }
    /**
     * 构建错误信息.(ErrorInfo)
     */
    public ErrorInfo getErrorInfo(HttpServletRequest request, Throwable error) {
        ErrorInfo errorInfo = new ErrorInfo();
        errorInfo.setTime(LocalDateTime.now().toString());
        errorInfo.setUrl(request.getRequestURL().toString());
        errorInfo.setError(error.toString());
        errorInfo.setStatusCode(getHttpStatus(request).value());
        errorInfo.setReasonPhrase(getHttpStatus(request).getReasonPhrase());
        errorInfo.setStackTrace(getStackTraceInfo(error, isIncludeStackTrace(request)));
        return errorInfo;
    }
    /**
     * 获取错误.(Error/Exception)
     *
     * @see DefaultErrorAttributes #addErrorDetails
     */
    public Throwable getError(HttpServletRequest request) {
        //根据HandlerExceptionResolver接口方法来获取错误.
        Throwable error = (Throwable) request.getAttribute(ERROR_NAME);
        //根据Request对象获取错误.
        if (error == null) {
            error = (Throwable) request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE);
        }
        //当获取错误非空,取出RootCause.
        if (error != null) {
            while (error instanceof ServletException && error.getCause() != null) {
                error = error.getCause();
            }
        }//当获取错误为null,此时我们设置错误信息即可.
        else {
            String message = (String) request.getAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE);
            if (StringUtils.isEmpty(message)) {
                HttpStatus status = getHttpStatus(request);
                message = "Unknown Exception But " + status.value() + " " + status.getReasonPhrase();
            }
            error = new Exception(message);
        }
        return error;
    }
    /**
     * 获取通信状态(HttpStatus)
     *
     * @see AbstractErrorController #getStatus
     */
    public HttpStatus getHttpStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute(WebUtils.ERROR_STATUS_CODE_ATTRIBUTE);
        try {
            return statusCode != null ? HttpStatus.valueOf(statusCode) : HttpStatus.INTERNAL_SERVER_ERROR;
        } catch (Exception ex) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }
    /**
     * 获取堆栈轨迹(StackTrace)
     *
     * @see DefaultErrorAttributes  #addStackTrace
     */
    public String getStackTraceInfo(Throwable error, boolean flag) {
        if (!flag) {
            return "omitted";
        }
        StringWriter stackTrace = new StringWriter();
        error.printStackTrace(new PrintWriter(stackTrace));
        stackTrace.flush();
        return stackTrace.toString();
    }
    /**
     * 判断是否包含堆栈轨迹.(isIncludeStackTrace)
     *
     * @see BasicErrorController #isIncludeStackTrace
     */
    public boolean isIncludeStackTrace(HttpServletRequest request) {
        //读取错误配置(server.error.include-stacktrace=NEVER)
        IncludeStacktrace includeStacktrace = errorProperties.getIncludeStacktrace();
        //情况1:若IncludeStacktrace为ALWAYS
        if (includeStacktrace == IncludeStacktrace.ALWAYS) {
            return true;
        }
        //情况2:若请求参数含有trace
        if (includeStacktrace == IncludeStacktrace.ON_TRACE_PARAM) {
            String parameter = request.getParameter("trace");
            return parameter != null && !"false".equals(parameter.toLowerCase());
        }
        //情况3:其它情况
        return false;
    }
    /**
     * 保存错误/异常.
     *
     * @see DispatcherServlet #processHandlerException 进行选举HandlerExceptionResolver
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
        request.setAttribute(ERROR_NAME, ex);
        return null;
    }
    /**
     * 提供优先级 或用于排序
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

3. 统一异常信息的构建工具(ErrorInfoBuilder)


ErrorInfoBuilder 作为核心工具类,其意义不言而喻,重点API:


  • 获取错误/异常
  • 通信状态
  • 堆栈轨迹


注:正确使用ErrorInfoBuilder,可以让处理器减少80%的代码。总而言之,ErrorInfoBuilder是个好东西,值得大家细细琢磨



  package com.hehe;
@SpringBootApplication
@RestController
public class ErrorHandlerApplication {
    /**
     * 随机抛出异常
     */
    private void randomException() throws Exception {
        Exception[] exceptions = { //异常集合
                new NullPointerException(),
                new ArrayIndexOutOfBoundsException(),
                new NumberFormatException(),
                new SQLException()};
        //发生概率
        double probability = 0.75;
        if (Math.random() < probability) {
            //情况1:要么抛出异常
            throw exceptions[(int) (Math.random() * exceptions.length)];
        } else {
            //情况2:要么继续运行
        }
    }
    /**
     * 模拟用户数据访问
     */
    @GetMapping("/")
    public List index() throws Exception {
        randomException();
        return Arrays.asList("正常用户数据1!", "正常用户数据2! 请按F5刷新!!");
    }
    public static void main(String[] args) {
        SpringApplication.run(ErrorHandlerApplication.class, args);
    }
}


注:ErrorBuilder之所以使用@Order注解和实现HandlerExceptionResolver接口是为了获取错误/异常,通常情况下@ExceptionHandler并不需要这么做,因为在映射方法注入Throwable就可以获得错误/异常,这是主要是为了ErrorController根据Request对象快速获取错误/异常。


4. 控制层代码(Controller)


上述,错误/异常处理器、错误信息、错误信息构建工具全部完成,我们编写控制层代码来测试相关效果。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>GlobalError</title>
</head>
<h1>统一祖国 振兴中华</h1>
<h2>服务异常,请稍后再试。</h2>
<div th:object="${errorInfo}">
    <h3 th:text="*{'发生时间:'+time}"></h3>
    <h3 th:text="*{'访问地址:'+url}"></h3>
    <h3 th:text="*{'问题类型:'+error}"></h3>
    <h3 th:text="*{'通信状态:'+statusCode+','+reasonPhrase}"></h3>
    <h3 th:text="*{'堆栈信息:'+stackTrace}"></h3>
</div>
</body>
</html>


相关文章
|
5月前
|
Java 数据格式 Spring
SpringBoot统一功能处理(统⼀⽤户登录权限验证、统⼀异常处理、统⼀数据格式封装)
SpringBoot统一功能处理(统⼀⽤户登录权限验证、统⼀异常处理、统⼀数据格式封装)
|
10月前
|
前端开发 JavaScript Java
SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回
本篇将要学习 Spring Boot 统一功能处理模块,这也是 AOP 的实战环节 用户登录权限的校验实现接口 HandlerInterceptor + WebMvcConfigurer 异常处理使用注解 @RestControllerAdvice + @ExceptionHandler 数据格式返回使用注解 @ControllerAdvice 并且实现接口 @ResponseBodyAdvice
333 0
|
10月前
|
JSON 前端开发 JavaScript
SpringBoot整合【全局异常处理+错误码枚举+JSR303校验】
本文介绍了如何使用Spring Boot整合全局异常处理、错误码枚举和JSR 303校验。全局异常处理通过@ControllerAdvice和@ExceptionHandler注解实现,提供了统一处理异常的机制。错误码枚举用于管理和传递错误信息,将错误信息与错误码进行映射,提高异常情况的定义和管理。JSR 303校验通过注解定义验证规则,并使用@Valid注解进行数据校验,确保请求数据的有效性。通过这些技术的应用,能够提升应用程序的异常处理和数据验证能力,提高应用程序的健壮性和用户体验。
439 2
|
10月前
|
JSON 前端开发 Java
SpringBoot 统一异常处理(附核心工具类-ErrorInfoBuilder) 2
SpringBoot 统一异常处理(附核心工具类-ErrorInfoBuilder)
|
12月前
|
消息中间件 JavaScript 小程序
SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回 下
SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回 下
SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回 下
|
12月前
|
JavaScript 前端开发 小程序
SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回 上
SpringBoot 统一功能处理:用户登录权限校验-拦截器、异常处理、数据格式返回 上
|
JSON 数据安全/隐私保护 数据格式
SpringBoot中的异常处理与参数校验_2
  兄弟们好,这次来跟老铁交流两个问题,异常和参数校验,在说参数校验之前我们先来说异常处理吧,因为后面参数的校验会牵扯到异常处理这块的内容。   说到异常处理,我不知道大家有没有写过或者遇到过如下的写法。
207 0
|
JSON Java 应用服务中间件
【SpringBoot 2】(九)异常处理 web原生组件的注入
【SpringBoot 2】(九)异常处理 web原生组件的注入
109 0
【SpringBoot 2】(九)异常处理 web原生组件的注入
|
Java 计算机视觉 前端开发
Springboot三部曲之Controller统一异常处理
SpringBoot整理的最后一块内容,Controller统一异常处理。 Controller的异常处理应该由开发组长来定义,这样再遇到问题的时候,不需要再使用if或者try等模块来对代码进行返回规范和日志记录,这类公共内容和经常进行CV编程的代码,应该统一起来,让开发人员随时随地,遇到业务无法执行的时候抛出业务异常即可,无需再次编写返回实体,这里就体现出Controller统一返回的好处了,要是不统一返回,那么异常处理的时候也没办法保证返回的内容是前端同事可以看懂的。
6833 0
|
Java
SpringBoot 统一异常处理\统一响应报文\自定义异常
SpringBoot 统一异常处理\统一响应报文\自定义异常
286 0