聚合统一,SpringBoot实现全局响应和全局异常处理

目录

前言

全局响应

数据规范

状态码(错误码)

全局响应类

使用

优化

全局异常处理

为什么需要全局异常处理

业务异常类

全局捕获

使用

优化

总结


前言

        在悦享校园1.0版本中的数据返回采用了以Map对象返回的方式,虽然较为便捷但也带来一些问题。一是在Controller中所有方法均需要实例化一个Map对象。二是当返回数据较多时使用put方式添加信息会容易出现遗漏的问题。在异常处理方面,虽然该版本中对所有异常通过继承RuntimeException的方式来进行封装,但业务异常较多时这一操作就显得冗余,且需要使用上述提到的Map对象包装异常信息。对于以上问题在2.0版本中通过结合SpringBoot来进行优雅的解决。

全局响应

数据规范

一般来讲我们提供给前端接口调用的返回值为如下的JSON格式,其包含结果状态,状态码,响应信息和响应数据。通常会使用@ResponseBody注解配合一个响应类来实现这一功能。但需要注意的是,当方法返回值为String类型时,@ResponseBody注解并不会将其转为JSON格式,需要手动进行转换。

{
    "success": true,
    "code": 0,
    "message": "操作成功",
    "data": "Hello"
}

状态码(错误码)

通过第一步数据规范可知,当接口被调用后会返回对应信息,若调用成功时返回固定的状态码即可,但调用失败时则需要不同的状态码来标识。为解决这个问题这里使用枚举的方式来定义出现异常时的错误信息。(此处的枚举对象名称可以自定义)

 块的错误。(此处的枚举对象名称可以自定义)

@AllArgsConstructor
@Getter
public enum ExceptionCodeEnum {
    // 操作成功
    EC0(0,"操作成功"),
    // 通用模块错误
    EC10000(10000,"系统内部错误"),
    EC10001(10001,"参数错误"),
    EC10002(10002,"资源不存在"),
    
    // 用户模块错误
    EC20000(20000,"用户名已被占用"),
    EC20001(20001,"用户不存在"),
    EC20002(20002,"用户名或密码错误"),
    
    // 其它模块....

    /**
     * 异常代码
     */
    private Integer code;
    /**
     * 描述信息
     */
    private String message;
}

全局响应类

此处创建一个泛型类来实现全局返回信息的格式统一,并且提供返回结果不同状态下的构造方法。

@Data
@Builder
@AllArgsConstructor
public class ResultDataVO {
    /**
     * 调用结果状态
     */
    private Boolean success;
    /**
     * 响应代码
     */
    private Integer code;
    /**
     * 详细信息
     */
    private String message;
    /**
     * 返回数据,数据为空则不返回
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;


    /**
     * 操作成功时返回的数据
     * @param result
     * @param 
     * @return
     */
    public static  ResultDataVO success(T result) {

        return ResultDataVO.builder()
                .success(true)
                .code(ExceptionCodeEnum.EC0.getCode())
                .message(ExceptionCodeEnum.EC0.getMessage())
                .data(result)
                .build();
    }

    /**
     * 操作失败
     * @param 
     * @param exceptionCodeEnum 错误类型枚举
     * @return
     */
    public static  ResultDataVO failure(ExceptionCodeEnum exceptionCodeEnum){

        return ResultDataVO.builder()
                .success(false)
                .code(exceptionCodeEnum.getCode())
                .message(exceptionCodeEnum.getMessage())
                .data(null)
                .build();
    }

    /**
     * 操作失败,返回信息
     * @param exceptionCodeEnum 错误信息列表
     * @param result 对应失败信息对象
     * @param 
     * @return
     */
    public static  ResultDataVO failure(ExceptionCodeEnum exceptionCodeEnum, T result){

        return ResultDataVO.builder()
                .success(false)
                .code(exceptionCodeEnum.getCode())
                .message(exceptionCodeEnum.getMessage())
                .data(result)
                .build();
    }
}

使用

通过以上操作已经实现了一个基础的全局数据响应处理,可以通过如下方式来使用。

@GetMapping("/{id}")
    public ResultDataVO getMsg(@RequestParam(required = false) String name,
                               @Max(value = 10,message = "最大值不能超过10")
                               @PathVariable(name = "id") int uid) {
        String result = "Hello,"+name+" id "+uid;

        return ResultDataVO.success(result);
    }

聚合统一,SpringBoot实现全局响应和全局异常处理_第1张图片

优化

虽然到这里我们已经基本实现了全局响应,但如果不想要在每个方法中调用ResultDataVO的success方法,可以通过如下方式解决。这里新建处理类实现了ResponseBodyAdvice接口,该接口包含三个方法,supports、beforeBodyWrite、handleEmptyBody。

supports用于指明方法是否需要对进入的方法进行后续包装处理,默认返回true,即对所有方法处理。

beforeBodyWrite用于在控制器方法返回结果后,但在响应体写入之前调用。可以在此方法中修改body对象,如包装、添加元数据等。在该方法中将使用ResultDataVO的success方法进行包装,由此将可以省去在Controller方法中重复调用success方法。

handleEmptyBody用于处理null值,由于ResultDataVO类中已经对null值进行了处理,因此无需重写该方法。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionAdvice implements ResponseBodyAdvice {


    /**
     * json格式化操作
     */
    @Resource
    private ObjectMapper objectMapper;

    /**
    * 是否开启对所有方法的处理,可以在此方法中添加条件使其支持对特定方法的处理。
    */
    @Override
    public boolean supports(MethodParameter returnType, Class> converterType) {
        return true;
    }
    
    /**
    * 用于在控制器方法返回结果后,但在响应体写入之前调用。可以此处对数据进行包装等操作
    */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        /**
         * 未被捕获的错误进行拦截
         */
        if(body == null){
            log.error("未处理的异常信息,请检查错误日志");
            return ResultDataVO.failure(ExceptionCodeEnum.EC10000);
        }

        /**
         * 返回类型为String则需要手动序列化
         */
        if (body instanceof String) {
            return objectMapper.writeValueAsString(ResultDataVO.success(body));
        }
        /**
         * 已被包装为全局VO对象直接返回
         */
        if (body instanceof ResultDataVO) {
            return body;
        }
        /**
         * 判断是否为404,500等错误类型
         */
        if (body instanceof LinkedHashMap) {
            LinkedHashMap httpErrorCode = (LinkedHashMap) body;
            Integer code = (Integer) httpErrorCode.get("status");
            String message = (String) httpErrorCode.get("error");
            return new ResultDataVO(false, code, message, null);

        }

        return ResultDataVO.success(body);
    }
} 
  

全局异常处理

为什么需要全局异常处理

使用全局异常处理更加灵活和规范化, 所有错误信息会被封装后返回给前端,避免暴露业务细节。

业务异常类

由于代码在运行过程中会出现异常,通常我们会使用 try...catch 方式来捕获并处理,在此之后我们需要返回错误信息知调用者当前状况。由于我们处理的异常多为RuntimeException的子类,因此可以通过编写一个业务异常类来实现总的异常信息处理,与以往不同在这里并不会为所有的业务异常创建具体的异常类,将使用前文中的错误码来配合使用。

@Getter
public class BusinessException extends RuntimeException {

    /**
     * 错误对象枚举
     */
    private ExceptionCodeEnum codeEnum;
    /**
     * 根据传入的异常枚举解析异常相关信息。
     * @param codeEnum
     */
    public BusinessException(ExceptionCodeEnum codeEnum){
        this.codeEnum = codeEnum;
    }
}

全局捕获

由于已经定义了总的异常处理类,因此在使用时只需要通过抛出 BusinessException 对象即可。但我们需要在代码中写入大量的try-catch语句来捕获处理异常。并且对于错误信息的返回需要符合在全局响应中的数据规范,也就是说需要像全局响应一样统一调用ResultDataVO的failure方法。

在前文创建的GlobalExceptionAdvice类上有一个@RestControllerAdvice注解,该注解将使所有的异常都进入到此处被处理同时也可以用于全局的数据绑定、格式化等。

既然所有的异常都进入该类处理,那么如何处理呢?这里使用@ExceptionHandler注解,使用它可以指定当前方法处理哪种类型的异常,示例代码如下。通过在方法体内调ResultDataVO的failure方法来完成返回数据格式的规范,这里仅列举了三个异常处理,可自行添加更多的异常类。

    /**
     * 数据格式转换错误
     */
    @ExceptionHandler(DataFormatException.class)
    @ResponseBody
    public ResultDataVO dataFormatExceptionHandler(DataFormatException e) {
        log.error("捕获数据格式转换错误异常", e);
        return ResultDataVO.failure(ExceptionCodeEnum.EC10001);
    }

    /**
     * 业务异常捕获
     *
     * @param businessException
     * @return
     */
    @ExceptionHandler(value = BusinessException.class)
    public ResultDataVO handleBusinessException(BusinessException businessException) {
        log.error("捕获业务异常", businessException);
        return ResultDataVO.failure(businessException.getCodeEnum());
    }
    
    /**
     * 系统级异常
     *
     * @param throwable
     */
    @ExceptionHandler(value = Throwable.class)
    public ResultDataVO handleThrowable(Throwable throwable) {
        log.error("捕获系统级异常", throwable);
        return ResultDataVO.failure(ExceptionCodeEnum.EC10000);
    }

使用

通过上述操作可以实现一定程度上对try-catch的消除,示例代码如下

    /**
     * 统一异常处理
     * @return
     */
    @GetMapping("/error")
    public ResultDataVO getError(){
        // try...catch
        int res = 1 / 0;
        return ResultDataVO.success(res);
    }

聚合统一,SpringBoot实现全局响应和全局异常处理_第2张图片

优化

虽然采用上述方式已经实现了对异常的统一拦截处理并返回,但若异常产生的源头并非Crontroller中出现而是在系统内部时则有可能导致返回结果出现问题,该问题已经在GlobalExceptionAdvice类beforeBodyWrite方法中做了处理,当body对象为空时则仍然会调用ResultDataVO的failure方法。

        // 用于处理未被正常捕获的异常
        if(body == null){
            log.error("未处理的异常信息,请检查错误日志");
            return ResultDataVO.failure(ExceptionCodeEnum.EC10000);
        }
        
        // ......

总结

通过使用@RestControllerAdvice和@ExceptionHandler注解和错误码以及对应处理/响应类即可实现全局异常的统一处理,其中GlobalExceptionAdvice类对全局响应和异常处理做了合并,可按照业务需求自行拆分。

你可能感兴趣的:(spring,boot,后端,java)