统一接口返回数据格式(一)—搭建公共服务

1 整体说明

在前后端分离的Web开发中,为便于开发,有必要统一后端接口的返回结果格式以及统一处理全局异常。常见的统一格式如下:

1
2
3
4
5
json复制代码{
 "status":"100",
 "message":"操作成功",
 "data":"hello,world"
}

为了避免各个微服务都自行实现返回结果的封装和全局异常的处理,可将上述功能抽取为公共服务,其他服务引入公共服务直接使用。

通过@RestControllerAdvice来拦截所有的@RestController@RestControllerAdvice注解搭配ResponseBodyAdvice接口可以实现在接口调用正常时的封装返回数据,@RestControllerAdvice注解搭配@ExceptionHandler注解可以实现在接口调用异常时封装指定异常类型的结果。本文就逐步说明如何搭建公共服务,并在其他服务中引用该公共服务。

2 公共服务搭建

2.1 服务自建自用

参考资料:SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!

2.1.1 定义返回结果格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
arduino复制代码@Data
public class ResultData<T> {
   private int status;
   private String message;
   private T data;
   private long timestamp ;

   public ResultData (){
       this.timestamp = System.currentTimeMillis();
  }

   public static <T> ResultData<T> success(T data) {
       ResultData<T> resultData = new ResultData<>();
       resultData.setStatus(ReturnCode.RC100.getCode());
       resultData.setMessage(ReturnCode.RC100.getMessage());
       resultData.setData(data);
       return resultData;
  }

   public static <T> ResultData<T> fail(int code, String message) {
       ResultData<T> resultData = new ResultData<>();
       resultData.setStatus(code);
       resultData.setMessage(message);
       return resultData;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
scss复制代码/* 定义状态码 */
public enum ReturnCode {
  /**操作成功**/
  RC100(100,"操作成功"),
  /**操作失败**/
  RC999(999,"操作失败"),
  /**服务限流**/
  RC200(200,"服务开启限流保护,请稍后再试!"),
  /**服务降级**/
  RC201(201,"服务开启降级保护,请稍后再试!"),
  /**热点参数限流**/
  RC202(202,"热点参数限流,请稍后再试!"),
  /**系统规则不满足**/
  RC203(203,"系统规则不满足要求,请稍后再试!"),
  /**授权规则不通过**/
  RC204(204,"授权规则不通过,请稍后再试!"),
  /**access_denied**/
  RC403(403,"无访问权限,请联系管理员授予权限"),
  /**access_denied**/
  RC401(401,"匿名用户访问无权限资源时的异常"),
  /**服务异常**/
  RC500(500,"系统异常,请稍后重试"),

  INVALID_TOKEN(2001,"访问令牌不合法"),
  ACCESS_DENIED(2003,"没有权限访问该资源"),
  CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),
  USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),
  UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式");

  /**自定义状态码**/
  private final int code;
  /**自定义描述**/
  private final String message;

  ReturnCode(int code, String message){
      this.code = code;
      this.message = message;
  }

  public int getCode() {
      return code;
  }

  public String getMessage() {
      return message;
  }
}

2.1.2 封装接口异常的返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
   /**
    * 默认全局异常处理。
    */
   @ExceptionHandler(Exception.class)
   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
   public ResultData<String> exception(Exception e) {
       log.error("全局异常信息 ex={}", e.getMessage(), e);
       return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());
  }
}

2.1.3 封装接口正常的返回结果

借助@RestControllerAdvice注解和ResponseBodyAdvice接口实现。

@RestControllerAdvice@RestController注解的增强,可以实现三个方面的功能:全局异常处理、全局数据绑定、全局数据预处理。

ResponseBodyAdvice接口的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typescript复制代码@RestControllerAdvice  //拦截Controller方法的返回值, 统一处理返回值/响应体, 一般用于统一返回格式、加解密、签名等等
public class RestResponseAdvice implements ResponseBodyAdvice<Object> {
   @Autowired
   private ObjectMapper objectMapper;

   /*
   * 是否支持advice功能
   * true支持, false不支持
    */
   @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) {
       if(o instanceof String){
           return objectMapper.writeValueAsString(ResultData.success(o));
      }
       if (o instanceof ResultData) {  // 对于已经是ResultData类型的结果不再封装, 直接返回
           return o;
      }
       return ResultData.success(o);
  }
}

2.1.4 在当前服务中验证

经过2.1.1~2.1.3节的处理,已经完成统一接口的返回数据格式和封装全局异常,这里直接在当前服务中创建2个接口进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@RestController
@RequestMapping("/api/demo")
public class RestDemo {

   @GetMapping("/hello")
   public String Hello() {
       return "hello, world!";
  }

   @GetMapping("/wrong")
   public int Wrong() {
       return 3/0;
  }
}

项目结构:

image-20211105000059803.png

接口调用正常的结果:

image-20211105000238234.png

接口调用异常的结果:

image-20211105000325898.png

2.2 公共服务安装

参考资料:

Maven 构建生命周期

springboot项目如何打包给其他项目引用

为了在其他服务中能以maven依赖项的方式引用2.1节搭建的公共服务,需要把公共服务安装到本地maven仓库或者部署到远程仓库。这里采用安装到本地仓库的方式进行演示。

Spring Boot打包的是Spring Boot特有格式的jar包,即可以运行的fat jar(生成jar包中源码对应的class文件在BOOT-INF目录),并不是传统的maven的JAR包,为此需要修改公共服务的pom.xml配置文件,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码    <build>
<plugins>
<!-- <plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

在IDEA的Terminal中执行maven项目安装命令:mvn clean install -Dmaven.test.skip=true

image-20211105001840547.png

3 公共服务引用

搭建另一个SpringBoot Web业务服务,在这个服务中引用第2节搭建的公共服务以实现统一接口的返回结果格式的目的。

3.1 引入公共服务依赖项

公共服务的pom.xml部分配置如下:

1
2
3
4
5
6
7
8
xml复制代码    <groupId>com.example</groupId>
<artifactId>common-advice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>common-advice</name>
<description>Public service that wraps interface results</description>
<properties>
<java.version>1.8</java.version>
</properties>

在业务服务的pom.xml中添加依赖项:

1
2
3
4
5
xml复制代码        <dependency>
<groupId>com.example</groupId>
<artifactId>common-advice</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

3.2 组件扫描路径添加公共服务

在业务服务的组件扫描路径(在服务入口增加@ComponentScan配置)中增加公共服务的包路径,否则无法使@RestControllerAdvice注解修饰的配置类生效。

查看@RestControllerAdvic源码可知,它是封装了@ControllerAdvice注解,而@ControllerAdvice封装了@Component注解,根据SpringBoot自动装配的规定,通过@ComponentScan配置可以将@RestControllerAdvic注解标记的组件加载。

1
2
3
4
5
6
7
less复制代码@ComponentScan({"com.example.demorest", "com.example.common.advice"})
@SpringBootApplication
public class DemorestApplication {
public static void main(String[] args) {
SpringApplication.run(DemorestApplication.class, args);
}
}

3.3 编写接口验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码@RestController
@RequestMapping("/api/original")
public class RestOriginal {

   @GetMapping("/success")
   public String GetSuccess() {
       return "hello, don!";
  }

   @GetMapping("/error")
   public int GetError() {
       return 6/0;
  }
}

项目结构:

image-20211105005252932.png

接口访问正常的结果:

image-20211105004944976.png

接口访问异常的结果:

image-20211105005031105.png

4 补充说明

通过调试我们可以发现,@RestControllerAdvice注解搭配的ResponseBodyAdvice接口和ExceptionHandler注解都是在controller接口方法体已经执行完成后在开始应用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%