SpringBoot - 统一格式封装及高阶全局异常处理

简介: SpringBoot - 统一格式封装及高阶全局异常处理


Pre

Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler实现自定义全局异常


演进过程

我们搞个boot工程 ,来看下为什么以及如何来实现统一格式封装及高阶全局异常处理

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>
    </dependencies>



版本V1

@RestController  // 返回JSON 
@RequestMapping("/v1")
public class ArtisanV1Controller {
    /**
     * 返回字符串
     *
     * @return
     */
    @GetMapping("/getString")
    public String getStr() {
        return "OOOOOOOK";
    }
    /**
     * 返回自定义对象
     *
     * @return
     */
    @GetMapping("/getArtisan")
    public Artisan getArt() {
        Artisan artisan = new Artisan();
        artisan.setJob("ArtisanJob");
        artisan.setAge(18);
        return artisan;
    }
    /**
     * 接口异常
     *
     * @return
     */
    @GetMapping("/getMockError")
    public int getMockError() {
        int i = 1 / 0;
        return i;
    }
}

分别测试下

这混乱的格式, 前端同学怎么想



版本2

Step1 约定统一返回格式

一个合格的标准的返回格式至少包含3部分:

  • status 状态值

由后端统一定义各种返回结果的状态码

  • message 描述

本次接口调用的结果描述

  • data 数据

本次接口返回的数据

如果需要可以加入其他节点,比如在返回对象中添加了接口调用时间 (timestamp: 接口调用时间)

Step2 开发统一返回对象

package com.artisan.resp;
import lombok.Data;
/**
 * @author 小工匠
 * @version 1.0
 * @description: 公共结果 
 * @mark: show me the code , change the world
 */
@Data
public class ResponseData<T> {
    /**
     * 结果状态 ,具体状态码参见ResponseCode
     */
    private int status;
    /**
     * 响应消息
     **/
    private String message;
    /**
     * 响应数据
     **/
    private T data;
    /**
     * 接口请求时间
     **/
    private long timestamp;
    /**
     * 初始化,增加接口请求事件
     */
    public ResponseData() {
        this.timestamp = System.currentTimeMillis();
    }
    /**
     * 成功
     *
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success() {
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC100.getCode());
        resultData.setMessage(ResponseCode.RC100.getMessage());
        return resultData;
    }
    /**
     * 成功
     *
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success(String message) {
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC100.getCode());
        resultData.setMessage(message);
        return resultData;
    }
    /**
     * 成功
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success(T data) {
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC100.getCode());
        resultData.setMessage(ResponseCode.RC100.getMessage());
        resultData.setData(data);
        return resultData;
    }
    /**
     * 失败
     *
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> fail(String message) {
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC999.getCode());
        resultData.setMessage(message);
        return resultData;
    }
    /**
     * 失败
     *
     * @param code
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> fail(int code, String message) {
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(code);
        resultData.setMessage(message);
        return resultData;
    }
    /**
     * 失败
     *
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> fail() {
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC999.getCode());
        resultData.setMessage(ResponseCode.RC999.getMessage());
        return resultData;

Step3 约定接口状态码

package com.artisan.resp;
import lombok.Getter;
/**
 * @author 小工匠
 * @version 1.0
 * @description: 状态码集合
 * @mark: show me the code , change the world
 */
public enum ResponseCode {
    /**
     * 操作成功
     **/
    RC100(100, "操作成功"),
    /**
     * 操作失败
     **/
    RC999(999, "操作失败"),
    /**
     * access_denied
     **/
    RC403(403, "无访问权限,请联系管理员授予权限"),
    /**
     * access_denied
     **/
    RC401(401, "匿名用户访问无权限资源时的异常"),
    /**
     * 服务异常
     **/
    RC500(500, "系统异常,请稍后重试"),
    ILLEGAL_ARGUMENT(3001, "非法参数"),
    INVALID_TOKEN(2001, "访问令牌不合法"),
    ACCESS_DENIED(2003, "没有权限访问该资源"),
    CLIENT_AUTHENTICATION_FAILED(1001, "客户端认证失败"),
    USERNAME_OR_PASSWORD_NOTMATCH(1002, "用户名或密码错误"); 
    /**
     * 自定义状态码
     **/
    @Getter
    private final int code;
    /**
     * 自定义描述
     **/
    @Getter
    private final String message;
    ResponseCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
}

Step4 验证

package com.artisan.controller;
import com.artisan.entity.Artisan;
import com.artisan.resp.ResponseData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author 小工匠
 * @version 1.0
 * @description: 版本2
 * @mark: show me the code , change the world
 */
@RestController
@RequestMapping("/v2")
public class ArtisanV2Controller {
    @GetMapping("/getString")
    public ResponseData<String> getStr() {
        return ResponseData.success("OOOOOOK");
    }
    @GetMapping("/getArtisan")
    public ResponseData<Artisan> getArt() {
        Artisan artisan = new Artisan();
        artisan.setJob("CodeMonkey");
        artisan.setAge(18);
        return ResponseData.success(artisan);
    }
    @GetMapping("/getMockError")
    public ResponseData<Integer> getMockError() {
        int i = 1 / 0;
        return ResponseData.success(i);
    }
}

好像部分实现了统一格式返回,确实也是有很多项目在Controller层通过ResponseData.success()对返回结果进行包装后返回给前端。

但是这个抛异常的这么玩还是不行呀? ------------------------> 全局异常处理


Step5 完善全局异常处理 @RestControllerAdvice + @ExceptionHandler

package com.artisan.resp;
import com.artisan.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.stream.Collectors;
/**
 * @author 小工匠
 * @version 1.0
 * @description: 全局异常处理
 * @mark: show me the code , change the world
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 默认全局异常处理。
     *
     * @param e e
     * @return ResponseData
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseData<String> exception(Exception e) {
        log.error("兜底异常信息 ex={}", e.getMessage());
        return ResponseData.fail(ResponseCode.RC500.getCode(), e.getMessage());
    }
    /**
     * Assert异常
     */
    @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseData<String> exception(IllegalArgumentException e) {
        return ResponseData.fail(ResponseCode.ILLEGAL_ARGUMENT.getCode(), e.getMessage());
    }
    /**
     * 抓取自定义异常  BaseException
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseData<String> exception(BaseException e) {
        return ResponseData.fail(e.getErrorCode(), e.getMessage());
    }
}
  • @ExceptionHandler,统一处理某一类异常, 减少代码重复率和复杂度,比如要捕获自定义异常可以@ExceptionHandler(BusinessException.class)
  • @ResponseStatus指定客户端收到的http状态码

重新验证下


全局异常处理器的必要行

  1. 避免try…catch,由全局异常处理器统一捕获
  2. 自定义异常,只能通过全局异常处理器来处理
  3. Validator参数校验器的时候,参数校验不通过会抛出异常,无法用try…catch捕获,只能使用全局异常处理器。


版本3 (ResponseBodyAdvice)

V2版本有缺陷么?

我们不难发现每写一个接口都需要调用ResponseData.success()对结果进行包装 ,程序猿懒啊, 能不写吗



Step1 自定义ResponseBodyAdvice接口实现类

ResponseBodyAdvice的作用一般是用于拦截Controller方法的返回值,统一处理返回值/响应体, 加解密,签名等

package com.artisan.resp.v3;
import com.artisan.resp.ResponseData;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
 * @author 小工匠
 * @version 1.0
 * @description: 拦截Controller方法的返回值,统一处理返回值/响应体
 * @mark: show me the code , change the world
 */
@RestControllerAdvice
public class CustomResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        // 处理String类型 
        if (o instanceof String) {
            return objectMapper.writeValueAsString(ResponseData.success(o));
        }
        // 若是统一返回类型,则不用再此封装
//        if (o instanceof ResponseData) {
//            return o;
//        }
        return ResponseData.success(o);
    }
}
  • @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器
  • 有一个地方对String做了特殊处理,因为如果Controller直接返回String ,SpringBoot是直接返回,所以我们需要手动转换成json

接入@RestControllerAdvice后, Controller就正常写就可以了,不用统一格式去包装了,如下

@RestController
@RequestMapping("/v3")
public class ArtisanV3Controller {
    @GetMapping("/getString")
    public String getStr() {
        return "OOOOOOK";
    }
    @GetMapping("/getArtisan")
    public Artisan getArt() {
        Artisan artisan = new Artisan();
        artisan.setJob("CodeMonkey");
        artisan.setAge(18);
        return artisan;
    }
    @GetMapping("/getMockError")
    public int getMockError() {
        int i = 1 / 0;
        return i;
    }
}

测试下

看到问题了吧,当我们同时启用统一标准格式封装功能ResponseAdvice和RestExceptionHandler全局异常处理器时,统一格式增强功能会给返回的异常结果再次封装,所以跟前端的接口响应又迷糊了



Step2 全局异常整合到返回的标准格式

因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。

// 若是统一返回类型,则不用再此封装
        if (o instanceof ResponseData) {
            return o;
        }

如果返回的结果是ResponseData对象,直接返回即可。

重新测试 ,


源码

https://github.com/yangshangwei/boot2


相关文章
|
10天前
|
JSON Java 数据格式
springboot中表字段映射中设置JSON格式字段映射
springboot中表字段映射中设置JSON格式字段映射
22 1
|
6月前
|
Java 数据库 数据安全/隐私保护
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
618 1
|
15天前
|
Java
springboot将list封装成csv文件
springboot将list封装成csv文件
20 4
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
52 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
3月前
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
1101 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
|
3月前
|
JSON 前端开发 Java
SpringBoot3怎么做统一结果封装?
在Spring Boot应用中,统一结果封装有助于团队协作,确保一致的API响应格式,提升代码质量和维护性。主要优点包括:简化前端集成工作,减少后端重复编码,以及增强接口的可维护性。实现上,首先定义`Result`类来封装响应状态码、消息、数据及时间戳;其次,通过`ResultCode`枚举类标准化状态信息。示例代码展示了如何构建这些类,并通过一个简单的控制器方法演示了如何使用它们返回JSON格式的响应结果。
111 2
|
4月前
|
XML Java API
springboot基础及上传组件封装
springboot基础及上传组件封装
19 0
|
4月前
|
JSON Java fastjson
Spring Boot返回Json数据及数据封装
本文详细介绍了如何在Spring Boot项目中处理JSON数据的传输 Spring Boot默认使用Jackson作为JSON处理器,并通过`spring-boot-starter-web`依赖自动包含相关组件。文章还展示了如何配置Jackson处理null值,使其转换为空字符串。此外,文章比较了Jackson和FastJson的特点,并提供了FastJson的配置示例,展示了如何处理null值以适应不同应用场景。
|
5月前
|
Java
springboot封装RedisTemplate
springboot封装RedisTemplate