相关推荐recommended
【SpringBoot3】统一数据响应,全局异常处理以及通用响应处理
作者:mmseoamin日期:2024-04-01

一、设计一个优秀的异常处理机制

在Spring Boot中设计一个优秀的异常处理机制,可以确保应用程序在遇到错误时提供清晰、一致的响应,同时提高系统的健壮性和可维护性。

以下是一个关于如何设计Spring Boot异常处理机制的步骤和建议:

1)定义自定义异常类

  • 创建自定义异常类来代表应用程序中可能发生的特定错误情况。
  • 自定义异常类应该扩展标准的RuntimeException或Exception类,并提供一个明确的消息来描述错误。

    2)创建全局异常处理器

    • 使用@ControllerAdvice注解创建一个全局异常处理器类。
    • 在该类中,使用@ExceptionHandler注解来指定处理特定异常的方法。

      3)定义统一的异常响应格式

      • 定义一个统一的异常响应格式,比如一个包含状态码、错误消息和可能还有其他相关信息的JSON对象。
      • 在全局异常处理器中,确保所有捕获的异常都被转换成这种统一的格式。

        4)异常分层

        • 根据错误的严重程度和影响范围,对异常进行分层。
        • 可以定义不同的异常类来表示不同的错误级别,例如:业务逻辑错误、系统错误、安全错误等。

          5)日志记录

          • 在异常处理过程中,确保记录适当的日志信息。
          • 使用Spring Boot的日志框架(如Logback或Log4j)来记录异常的堆栈跟踪和其他重要信息。

            6)测试

            • 编写单元测试来验证异常处理机制的行为。
            • 确保在出现预期异常时,处理程序能够正确捕获并记录异常,同时返回正确的响应格式。

              7)提供友好的用户错误信息

              • 不要在前端界面上直接显示原始异常消息或堆栈跟踪。
              • 提供给用户友好、易于理解的错误信息和建议。

                8)处理全局异常

                • 使用@ExceptionHandler(Exception.class)来处理所有未被其他处理器捕获的异常。
                • 这可以作为一个“兜底”处理器,确保所有异常都被处理。

                  9)考虑国际化

                  • 如果你的应用程序需要支持多种语言,确保异常消息是可以国际化的。
                  • 使用Spring的消息源(MessageSource)来支持多语言错误消息。

                    10)优雅地处理资源不足

                    • 当应用程序遇到资源不足(如数据库连接池耗尽)等问题时,应确保异常得到妥善处理,避免应用崩溃。
                    • 考虑实现资源耗尽时的回退策略,如优雅地降级服务。

                      11)使用AspectJ进行切面编程

                      • 可以使用AspectJ的切面编程来在方法执行前后进行异常处理,从而避免在每个控制器中重复编写异常处理逻辑。

                        通过遵循这些步骤,你可以设计一个强大而灵活的异常处理机制,提高Spring Boot应用程序的健壮性和用户体验。

                        二、统一异常处理实现步骤

                        1、统一数据响应

                        我们必须为所有的接口定义统一的数据响应格式,创建统一数据响应类

                        @Data
                        public class AjaxResponse {
                            private String message;
                            private Integer code;
                            private T data;
                            public static AjaxResponse error(CustomException e) {
                                AjaxResponse response = new AjaxResponse<>();
                                response.setCode(e.getCode());
                                response.setMessage(e.getMsg());
                                return response;
                            }
                            public static AjaxResponse success() {
                                AjaxResponse response = new AjaxResponse<>();
                                response.setCode(ErrorCode.SUCCESS.getCode());
                                response.setMessage(ErrorCode.SUCCESS.getMsg());
                                return response;
                            }
                            public static  AjaxResponse success(T data) {
                                AjaxResponse response = new AjaxResponse<>();
                                response.setCode(ErrorCode.SUCCESS.getCode());
                                response.setMessage(ErrorCode.SUCCESS.getMsg());
                                response.setData(data);
                                return response;
                            }
                        }
                        

                        2、定义统一的异常状态码

                        建议直接使用Http状态码

                        @Getter
                        public enum ErrorCode {
                            SUCCESS(200, "成功"),
                            BAD_REQUEST(400, "请求参数不正确"),
                            SERVER_ERROR(500, "系统异常"),
                            UNKNOWN(999, "未知错误");
                            private Integer code;
                            private String msg;
                            ErrorCode(Integer code, String message) {
                                this.code = code;
                                this.msg = message;
                            }
                        }
                        

                        3、创建自定义异常类

                        创建自定义异常CustomException,使用统一的异常枚举类ErrorCode作为参数

                        @Getter
                        public class CustomException extends RuntimeException {
                            private Integer code;
                            private String msg;
                            public CustomException() {
                                super();
                            }
                            public CustomException(ErrorCode errorCode) {
                                this.code = errorCode.getCode();
                                this.msg = errorCode.getMsg();
                            }
                            public CustomException(ErrorCode errorCode, String msg) {
                                this.code = errorCode.getCode();
                                this.msg = msg;
                            }
                        }
                        

                        4、创建全局异常处理类

                        创建全局异常处理类 WebExceptionHandler ,并使用注解 @ControllerAdvice 声明。

                        拦截异常,并封装成统一的数据返回格式AjaxResponse

                        @ControllerAdvice
                        public class WebExceptionHandler {
                            @ExceptionHandler(CustomException.class)
                            @ResponseBody
                            public AjaxResponse customerException(CustomException e) {
                                return AjaxResponse.error(e);
                            }
                            @ExceptionHandler(Exception.class)
                            @ResponseBody
                            public AjaxResponse exception(Exception e) {
                                return AjaxResponse.error(new CustomException(ErrorCode.UNKNOWN));
                            }
                        }
                        

                        5、创建通用响应处理类

                        创建通用返回处理类 GlobalResponseHandler ,拦截所有的接口返回数据,将接口请求的HttpCode,设置为AjaxResponse的Code,保证在异常抛出时,前端能够感知到是异常请求。

                        @ControllerAdvice
                        public class GlobalResponseHandler implements ResponseBodyAdvice {
                            @Override
                            public boolean supports(MethodParameter returnType, Class converterType) {
                                if (returnType.getMethod() == null) {
                                    return false;
                                }
                                ResponseBody responseBody = returnType.getMethod().getAnnotation(ResponseBody.class);
                                if (responseBody != null) {
                                    return true;
                                }
                                // 只拦截返回结果为 AjaxResponse 类型
                                return returnType.getMethod().getReturnType() == AjaxResponse.class;
                            }
                            @Override
                            public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                                          MediaType selectedContentType,
                                                          Class selectedConverterType,
                                                          ServerHttpRequest request,
                                                          ServerHttpResponse response) {
                                ResponseBody responseBody = returnType.getMethod().getAnnotation(ResponseBody.class);
                                if (responseBody != null || selectedContentType.equalsTypeAndSubtype(MediaType.APPLICATION_JSON)) {
                                    if (body instanceof AjaxResponse ajaxBody) {
                                        if (!Objects.equals(ajaxBody.getCode(), ErrorCode.UNKNOWN.getCode())) {
                                            response.setStatusCode(HttpStatus.valueOf(ajaxBody.getCode()));
                                        }
                                    } else {
                                        return AjaxResponse.success(body);
                                    }
                                }
                                return body;
                            }
                        }
                        

                        6、创建测试类,模拟抛出异常

                        1)创建测试异常Service

                        @Service
                        public class ExceptionService {
                            public void serverError() {
                                try {
                                    Class.forName("com.mysql.jdbc.xxx.Driver");
                                } catch (Exception e) {
                                    throw new CustomException(ErrorCode.SERVER_ERROR, "数据库驱动加载异常,出现ClassNotFoundException,请联系管理员");
                                }
                            }
                            public void badRequest() {
                                throw new CustomException(ErrorCode.BAD_REQUEST, "您输入的数据不符合业务逻辑,请确认后重新输入!");
                            }
                        }
                        

                        2)创建测试Controller方法

                        @GetMapping("/user")
                        @ResponseBody
                        public User user(User user) {
                            Assert.isTrue(user.getName().equals("jackson"), "User must be a jackson");
                            if (user.getAge() < 18 & user.getAge() >= 0) {
                                exceptionService.badRequest();
                            } else if (user.getAge() < 0) {
                                exceptionService.serverError();
                            }
                            return user;
                        }
                        

                        可以使用postman进行测试,传入不同的参数抛出不同异常

                        三、 @ControllerAdvice 特别说明

                        @ControllerAdvice是Spring 3.2及以后版本中引入的一个注解,它用于全局地处理控制器层的异常和其他跨切面的关注点。该注解提供了一种集中的方式,使得开发者可以在单个位置定义并管理多个控制器中可能遇到的通用逻辑。

                        具体来说,@ControllerAdvice的作用主要体现在以下几个方面:

                        1. 全局异常处理:通过结合@ExceptionHandler注解,开发者可以定义全局级别的异常处理逻辑。这避免了在每个控制器中重复编写相同的异常处理代码。当控制器中抛出异常时,Spring MVC会查找带有@ControllerAdvice注解的类,并尝试调用匹配的@ExceptionHandler方法来处理异常。
                        2. 数据绑定:通过结合@InitBinder注解,开发者可以在多个控制器之间配置通用的WebDataBinder设置。这对于自定义请求参数的绑定和格式化非常有用。例如,开发者可以定义全局的日期格式、数字格式等。
                        3. 模型增强:通过结合@ModelAttribute注解,开发者可以在多个控制器间添加公共的模型属性。这对于添加那些需要在多个控制器或视图中使用的数据非常方便。例如,开发者可以在一个带有@ControllerAdvice注解的类中定义一个方法,该方法会在每个控制器方法执行之前执行,从而向模型中添加一些公共属性。

                        总的来说,@ControllerAdvice注解提供了一个强大的机制,使得开发者能够以一种集中和模块化的方式处理控制器层的异常和其他跨切面的关注点。这不仅提高了代码的可维护性和可重用性,还使得异常处理和数据绑定等逻辑更加清晰和易于管理。

                        四、Http状态码

                        状态码类别原因短语描述
                        100信息性响应Continue请求已收到,请继续发送
                        101信息性响应Switching Protocols切换协议
                        102信息性响应Processing请求正在处理中
                        200成功OK请求成功
                        201成功Created请求成功且资源已创建
                        202成功Accepted请求已接受,处理中
                        203成功Non-Authoritative Information非授权信息
                        204成功No Content无内容
                        205成功Reset Content重置内容
                        206成功Partial Content部分内容
                        300重定向Multiple Choices多种选择
                        301重定向Moved Permanently永久移动
                        302重定向Found临时移动
                        303重定向See Other查看其他位置
                        304重定向Not Modified未修改
                        307重定向Temporary Redirect临时重定向
                        400客户端错误Bad Request错误请求
                        401客户端错误Unauthorized未授权
                        402客户端错误Payment Required需要付款
                        403客户端错误Forbidden禁止访问
                        404客户端错误Not Found未找到资源
                        405客户端错误Method Not Allowed方法不允许
                        406客户端错误Not Acceptable不可接受
                        407客户端错误Proxy Authentication Required需要代理身份验证
                        408客户端错误Request Timeout请求超时
                        409客户端错误Conflict冲突
                        410客户端错误Gone资源已消失
                        500服务器错误Internal Server Error服务器内部错误
                        501服务器错误Not Implemented未实现功能
                        502服务器错误Bad Gateway错误的网关
                        503服务器错误Service Unavailable服务不可用
                        504服务器错误Gateway Timeout网关超时
                        505服务器错误HTTP Version Not Supported不支持的HTTP版本
                        506服务器错误Variant Also Negotiates协商变体也存在
                        507服务器错误Insufficient Storage存储不足
                        508服务器错误Loop Detected检测到循环

                        参考

                        • https://docs.spring.io/spring-boot/docs/3.2.3/reference/htmlsingle/#web.servlet.spring-mvc.error-handling