springboot之全局处理统一返回

44次阅读

共计 7319 个字符,预计需要花费 19 分钟才能阅读完成。

springboot 之全局处理统一返回

简介

在 REST 风格的开发中,避免通常会告知前台返回是否成功以及状态码等信息。这里我们通常返回的时候做一次 util 的包装处理工作,如:Result类似的类,里面包含 succcodemsgdata 等字段。

接口调用返回类似如下:

{
  "succ": false,        // 是否成功
  "ts": 1566467628851,  // 时间戳
  "data": null,         // 数据
  "code": "CLOUD800",   // 错误类型
  "msg": "业务异常",    // 错误描述
  "fail": true
}

当然在每个接口里返回要通过 Result 的工具类将这些信息给封装一下,这样导致业务和技术类的代码耦合在一起。

接口调用处理类似如下:

  @GetMapping("hello")
  public Result list(){return Result.ofSuccess("hello");
  }

结果:

{
  "succ": ture,         // 是否成功
  "ts": 1566467628851,  // 时间戳
  "data": "hello",      // 数据
  "code": null,         // 错误类型
  "msg": null,          // 错误描述
  "fail": true
}

我们将这些操抽出一个公共 starter 包,各个服务依赖即可,做一层统一拦截处理的工作,进行技术解耦。

配置

unified-dispose-springboot-starter

这个模块里包含异常处理以及全局返回封装等功能,下面。

完整目录结构如下:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── purgetiem
│   │   │           └── starter
│   │   │               └── dispose
│   │   │                   ├── GlobalDefaultConfiguration.java
│   │   │                   ├── GlobalDefaultProperties.java
│   │   │                   ├── Interceptors.java
│   │   │                   ├── Result.java
│   │   │                   ├── advice
│   │   │                   │   └── CommonResponseDataAdvice.java
│   │   │                   ├── annotation
│   │   │                   │   ├── EnableGlobalDispose.java
│   │   │                   │   └── IgnorReponseAdvice.java
│   │   │                   └── exception
│   │   │                       ├── GlobalDefaultExceptionHandler.java
│   │   │                       ├── category
│   │   │                       │   └── BusinessException.java
│   │   │                       └── error
│   │   │                           ├── CommonErrorCode.java
│   │   │                           └── details
│   │   │                               └── BusinessErrorCode.java
│   │   └── resources
│   │       ├── META-INF
│   │       │   └── spring.factories
│   │       └── dispose.properties
│   └── test
│       └── java

统一返回处理

按照一般的模式,我们都需要创建一个可以进行处理包装的工具类以及一个返回对象。

Result(返回类):

创建 Result<T> Tdata的数据类型,这个类包含了前端常用的字段,还有一些常用的静态初始化 Result 对象的方法。

/**
 * 返回统一数据结构
 *
 * @author purgeyao
 * @since 1.0
 */
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {

  /**
   * 是否成功
   */
  private Boolean succ;

  /**
   * 服务器当前时间戳
   */
  private Long ts = System.currentTimeMillis();

  /**
   * 成功数据
   */
  private T data;

  /**
   * 错误码
   */
  private String code;

  /**
   * 错误描述
   */
  private String msg;

  public static Result ofSuccess() {Result result = new Result();
    result.succ = true;
    return result;
  }

  public static Result ofSuccess(Object data) {Result result = new Result();
    result.succ = true;
    result.setData(data);
    return result;
  }

  public static Result ofFail(String code, String msg) {Result result = new Result();
    result.succ = false;
    result.code = code;
    result.msg = msg;
    return result;
  }

  public static Result ofFail(String code, String msg, Object data) {Result result = new Result();
    result.succ = false;
    result.code = code;
    result.msg = msg;
    result.setData(data);
    return result;
  }

  public static Result ofFail(CommonErrorCode resultEnum) {Result result = new Result();
    result.succ = false;
    result.code = resultEnum.getCode();
    result.msg = resultEnum.getMessage();
    return result;
  }

  /**
   * 获取 json
   */
  public String buildResultJson(){JSONObject jsonObject = new JSONObject();
    jsonObject.put("succ", this.succ);
    jsonObject.put("code", this.code);
    jsonObject.put("ts", this.ts);
    jsonObject.put("msg", this.msg);
    jsonObject.put("data", this.data);
    return JSON.toJSONString(jsonObject, SerializerFeature.DisableCircularReferenceDetect);
  }
}

这样已经满足一般返回处理的需求了,在接口可以这样使用:

  @GetMapping("hello")
  public Result list(){return Result.ofSuccess("hello");
  }

当然这样是耦合的使用,每次都需要调用 Result 里的包装方法。


ResponseBodyAdvice 返回统一拦截处理

ResponseBodyAdvice在 spring 4.1 新加入的一个接口,在消息体被 HttpMessageConverter 写入之前允许 Controller@ResponseBody 修饰的方法或 ResponseEntity 调整响应中的内容,比如做一些返回处理。

ResponseBodyAdvice接口里一共包含了两个方法

  • supports: 该组件是否支持给定的控制器方法返回类型和选择的 {@code HttpMessageConverter} 类型
  • beforeBodyWrite: 在选择 {@code HttpMessageConverter} 之后调用,在调用其写方法之前调用。

那么我们就可以在这两个方法做一些手脚。

  • supports用于判断是否需要做处理。
  • beforeBodyWrite用于做返回处理。

CommonResponseDataAdvice类实现 ResponseBodyAdvice 两个方法。

filter(MethodParameter methodParameter) 私有方法里进行判断是否要进行拦截统一返回处理。

如:

  • 添加自定义注解 @IgnorReponseAdvice 忽略拦截。
  • 判断某些类不进行拦截.
  • 判断某些包下所有类不进行拦截。如 swaggerspringfox.documentation包下的接口忽略拦截等。

filter 方法:
判断为 false 就不需要进行拦截处理。

  private Boolean filter(MethodParameter methodParameter) {Class<?> declaringClass = methodParameter.getDeclaringClass();
    // 检查过滤包路径
    long count = globalDefaultProperties.getAdviceFilterPackage().stream()
        .filter(l -> declaringClass.getName().contains(l)).count();
    if (count > 0) {return false;}
    // 检查 < 类 > 过滤列表
    if (globalDefaultProperties.getAdviceFilterClass().contains(declaringClass.getName())) {return false;}
    // 检查注解是否存在
    if (methodParameter.getDeclaringClass().isAnnotationPresent(IgnorReponseAdvice.class)) {return false;}
    if (methodParameter.getMethod().isAnnotationPresent(IgnorReponseAdvice.class)) {return false;}
    return true;
  }

CommonResponseDataAdvice类:

最核心的就在 beforeBodyWrite 方法处理里。

  1. 判断 Object o 是否为 null,为null 构建 Result 对象进行返回。
  2. 判断 Object o 是否是 Result 子类或其本身,该情况下,可能是接口返回时创建了 Result, 为了避免再次封装一次,判断是Result 子类或其本身就返回 Object o 本身。
  3. 判断 Object o 是否是为 String, 在测试的过程中发现String 的特殊情况,在这里做了一次判断操作,如果为 String 就进行 JSON.toJSON(Result.ofSuccess(o)).toString() 序列号操作。
  4. 其他情况默认返回 Result.ofSuccess(o) 进行包装处理。
/**
 * {@link IgnorReponseAdvice} 处理解析 {@link ResponseBodyAdvice} 统一返回包装器
 *
 * @author purgeyao
 * @since 1.0
 */
@RestControllerAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> {

  private GlobalDefaultProperties globalDefaultProperties;

  public CommonResponseDataAdvice(GlobalDefaultProperties globalDefaultProperties) {this.globalDefaultProperties = globalDefaultProperties;}

  @Override
  @SuppressWarnings("all")
  public boolean supports(MethodParameter methodParameter,
      Class<? extends HttpMessageConverter<?>> aClass) {return filter(methodParameter);
  }

  @Nullable
  @Override
  @SuppressWarnings("all")
  public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
      Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
      ServerHttpResponse serverHttpResponse) {

    // o is null -> return response
    if (o == null) {return Result.ofSuccess();
    }
    // o is instanceof ConmmonResponse -> return o
    if (o instanceof Result) {return (Result<Object>) o;
    }
    // string 特殊处理
    if (o instanceof String) {return JSON.toJSON(Result.ofSuccess(o)).toString();}
    return Result.ofSuccess(o);
  }

  private Boolean filter(MethodParameter methodParameter) {···略}

}

这样基本完成了核心的处理工作。当然还少了上文提到的 @IgnorReponseAdvice 注解。

@IgnorReponseAdvice:
比较简单点,只作为一个标识的作用。

/**
 * 统一返回包装标识注解
 *
 * @author purgeyao
 * @since 1.0
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnorReponseAdvice {}

加入 spring 容器

最后将 GlobalDefaultExceptionHandlerbean的方式注入 spring 容器。

@Configuration
@EnableConfigurationProperties(GlobalDefaultProperties.class)
@PropertySource(value = "classpath:dispose.properties", encoding = "UTF-8")
public class GlobalDefaultConfiguration {

  ···略
  
  @Bean
  public CommonResponseDataAdvice commonResponseDataAdvice(GlobalDefaultProperties globalDefaultProperties){return new CommonResponseDataAdvice(globalDefaultProperties);
  }

}

GlobalDefaultConfigurationresources/META-INF/spring.factories文件下加载。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.purgetime.starter.dispose.GlobalDefaultConfiguration

不过我们这次使用注解方式开启。其他项目依赖包后,需要添加 @EnableGlobalDispose 才可以将全局拦截的特性开启。

将刚刚创建的 spring.factories 注释掉,创建 EnableGlobalDispose 注解。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(GlobalDefaultConfiguration.class)
public @interface EnableGlobalDispose {}

使用 @ImportGlobalDefaultConfiguration导入即可。

使用

添加依赖

<dependency>
  <groupId>io.deepblueai</groupId>
  <artifactId>unified-dispose-deepblueai-starter</artifactId>
  <version>0.1.0.RELEASE</version>
</dependency>

启动类开启 @EnableGlobalDispose 注解即可。

  1. 业务使用

接口:

@GetMapping("test")
public String test(){return "test";}

返回

{
  "succ": true,             // 是否成功
  "ts": 1566386951005,      // 时间戳
  "data": "test",           // 数据
  "code": null,             // 错误类型
  "msg": null,              // 错误描述
  "fail": false             
}
  1. 忽略封装注解:@IgnorReponseAdvice

@IgnorReponseAdvice允许范围为:类 + 方法,标识在类上这个类下的说有方法的返回都将忽略返回封装。

接口:

@IgnorReponseAdvice // 忽略数据包装 可添加到类、方法上
@GetMapping("test")
public String test(){return "test";}

返回 test

总结

项目里很多重复的 code,我们可以通过一定的方式去简化,以达到一定目的减少开发量。

示例代码地址:unified-dispose-springboot

作者 GitHub:
Purgeyao 欢迎关注

正文完
 0