Fork me on GitHub

BiuBiu

记录、学习、生活

0%

SpringBoot系列(三)-RestFul-API Swagger 统一接口返回 全局异常处理

随着前后端分离的发展,各端分工越来越精细化,前后端耦合性大大降低,后端接口需要文档,统一格式返回等等,今天一一分析分析……

准备
新建一个项目,springboot-restful-api-demo 引入web模块。

统一接口返回

前后端分离:前端负责数据的展示,后端负责数据的处理,前后端交互变得非常重要!

前后端分离架构:

1
2
3
4
5
6
7
前端:iOS Android 小程序 web m 等等
调用接口(Http)
后端:Nginx (负载均衡,流量分发,静态资源处理,反向代理等等)
后端:聚合服务
后端:微服务(分布式部署的各种服务)
处理数据
后端:DB,Redis,MongoDB,MQ等

一套系统适应多端(iOS App, Android App, 小程序,m站,pc站等),所以API最好返回统一的数据格式 如下模板,code-状态码 message-消息提示,错误消息等 data-真正的数据

1
2
3
4
5
6
7
8
{
"code": 200,
"message": "OK",
"data": {
"memo": "",
"dataState": 1
}
}

开始撸起来,响应实体Result

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
48
49
50
51
52
53
54
55
56
package com.biubiu.api.vo;

import lombok.Getter;

import java.io.Serializable;

/**
* <p>
* Result
* </p>
*
* @author wbbaijq
* @since 2021/8/23
*/
@Getter
public class Result<T> implements Serializable {
private static final long serialVersionUID = 7491166533026088331L;

private int code;
private String message;
private T data;

public Result() {
}

private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}

public static <T> Result<T> success(T data) {
return new Result<>(200, "ok", data);
}

public static <T> Result<T> success() {
return success(null);
}

public static <T> Result<T> error(ResultCode resultCode) {
return error(resultCode.getCode(), resultCode.getMessage());
}

public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}

public static <T> Result<T> error(String message) {
return error(-1, message);
}

public static <T> Result<T> error() {
return error("error");
}

}

统一错误状态码ResultCode

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
package com.biubiu.api.vo;

/**
* <p>
* ResultCode
* </p>
*
* @author wbbaijq
* @since 2021/8/23
*/
public enum ResultCode {
//异常声明
SUCCESS(0, "ok"),
ERROR(-1, "error"),

//参数异常
ERROR_SYSTEM_EXCEPTION(1001, "系统异常"),

ERROR_PARAM_IS_BLANK(1002, "参数为空"),
ERROR_PARAM_TYPE_BIND_ERROR(1003, "参数类形错误"),

//用户异常
ERROR_USER_NOT_LOGGED_IN(104001, "用户未登录,需要验证,请登录"),
ERROR_USER_LOGIN_ERROR(104002, "账号不存在或密码错误"),
ERROR_USER_NOT_EXIST(104003, "用户不存在"),
ERROR_USER_HAS_EXISTED(104004, "用户已存在");//末尾分号,勿删

private final int code;
private final String message;

ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return this.code;
}

public String getMessage() {
return this.message;
}
}

测试 DemoController

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
package com.biubiu.api.controller;

import com.biubiu.api.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
* <p>
* DemoController
* </p>
*
* @author wbbaijq
* @since 2021/8/23
*/
@RestController
@RequestMapping("/api")
public class DemoController {

@RequestMapping("/demo")
public Result<Map<String, Object>> demo() {
Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
map.put("id", 10001);
map.put("hobby", new ArrayList<>(Arrays.asList("篮球", "网球")));
map.put("city", "上海");
return Result.success(map);
}

@RequestMapping("/test")
public Result<String> demos() {
return Result.success("测字符串");
}
}

h94vSP.png]

全局异常处理

  1. 自定义异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.biubiu.api.handler;

    /**
    * <p>
    * SystemException
    * </p>
    *
    * @author wbbaijq
    * @since 2021/8/23
    */
    public class SystemException extends RuntimeException {
    private static final long serialVersionUID = 1488902735359521074L;

    public SystemException(String message) {
    super(message);
    }
    }
  2. 全局异常捕获

    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    package com.biubiu.api.handler;

    import com.biubiu.api.vo.Result;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.validation.BindException;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;

    import javax.servlet.http.HttpServletRequest;

    /**
    * <p>
    * GlobalExceptionHandler
    * </p>
    *
    * @author wbbaijq
    * @since 2021/8/23
    */
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public Result<Object> exceptionHandler(Exception ex, HttpServletRequest request) {
    log.error("url:{},|errMsg:{}", request.getRequestURI(), ex.getMessage(), ex);
    return Result.error(500, ex.getMessage());
    }

    /**
    * 参数校验异常捕获
    */
    @ExceptionHandler(BindException.class)
    public Result<Object> bindExceptionHandler(BindException ex, HttpServletRequest request) {
    log.error("url:{},|errMsg:{}", request.getRequestURI(), ex.getBindingResult().getFieldError().getDefaultMessage(), ex);
    return Result.error(500, ex.getBindingResult().getFieldError().getDefaultMessage());
    }

    /**
    * 参数校验异常捕获
    */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Object> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex, HttpServletRequest request) {
    log.error("url:{},|errMsg:{}", request.getRequestURI(), ex.getBindingResult().getFieldError().getDefaultMessage(), ex);
    return Result.error(500, ex.getBindingResult().getFieldError().getDefaultMessage());
    }

    /**
    * 自定义异常
    */
    @ExceptionHandler(SystemException.class)
    public Result<Object> methodArgumentNotValidExceptionHandler(SystemException ex, HttpServletRequest request) {
    log.error("url:{},|errMsg:{}", request.getRequestURI(), ex.getMessage(), ex);
    return Result.error(500, ex.getMessage());
    }
    }
  3. 测试

    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
    /**
    * <p>
    * DemoController
    * </p>
    *
    * @author wbbaijq
    * @since 2021/8/23
    */
    @RestController
    @RequestMapping("/api")
    public class DemoController {

    @RequestMapping("/e1")
    public Result<String> e1() {
    throw new SystemException("错误测试");
    }

    @RequestMapping("/e2")
    public Result<String> e2(@Valid User user) {
    return Result.success("Hello " + user.getName());
    }

    @RequestMapping("/e3")
    public Result<String> e3() {
    int i = 3 / 0;
    return Result.success("ok");
    }
    }

    @Data
    class User {
    private int id;
    @NotBlank(message = "名字不为空")
    private String name;
    }

    返回结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "code": 500,
    "message": "错误测试",
    "data": null
    }
    {
    "code": 500,
    "message": "名字不为空",
    "data": null
    }
    {
    "code": 500,
    "message": "/ by zero",
    "data": null
    }

备注:上面有个 @Valid 注解,这是参数校验用的,使用的是Hibernate Validator处理的,下面来说 参数校验

参数校验

Hibernate Validator是SpringBoot内置的校验框架,只要集成了SpringBoot就自动集成了它,就可以在对象上面使用它提供的注解来完成参数校验。

由于SpringBoot 2.3版本默认移除了校验功能,如果想要开启的话需要添加如下依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

常用注解

  • @NotNull:被注释的属性不能为null;
  • @NotBlank:被注释的字符串不能为空字符串;
  • @NotEmpty:被注释的属性不能为空;
  • @Pattern:被注释的属性必须符合其regexp所定义的正则表达式;
  • @Email:被注释的属性必须符合邮箱格式。
  • @Min:被注释的属性必须大于等于其value值;
  • @Max:被注释的属性必须小于等于其value值;
  • @Size:被注释的属性必须在其min和max值之间;

RestFul-API

随着互联网和移动设备得发展,人们对Web应用的使用需求也增加,传统的动态页面由于低效率而渐渐被HTML+JavaScript(Ajax)的前后端分离所取代!
所以一套结构清晰、符合标准、易于理解、扩展方便让大部分人都能够理解接受的接口风格就显得越来越重要,而RESTful风格的接口(RESTful API)刚好有以上特点,就逐渐被实践应用而变得流行起来

用URL定位资源,用Http请求描述操作

HTTP请求

  • GET 从服务器取出资源(一项或多项)
  • POST 在服务器新建一个资源
  • PUT 在服务器更新资源(客户端提供完整资源数据)
  • PATCH 在服务器更新资源(客户端提供需要修改的资源数据)
  • DELETE 从服务器删除资源

URL: 统一资源定位符
HTTP/1.1:

  • 引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive
  • 加入了管道机制,在同一个TCP连接里,允许多个请求同时发送,增加了并发性,进一步改善了HTTP协议的效率
  • 新增了请求方式PUT、PATCH、OPTIONS、DELETE等

状态码

  • 200 OK - 客户端请求成功 [GET]
  • 201 CREATED 用户新建或修改数据成功 [POST/PUT/PATCH]
  • 202 Accepted 表示一个请求已经进入后台排队(异步任务)
  • 204 Accepted 用户删除数据成功 [DELETE]
  • 301 - 资源(网页等)被永久转移到其它URL
  • 302 - 临时跳转
  • 400 Bad Request - 客户端请求有语法错误,不能被服务器所理解 [POST/PUT/PATCH]
  • 401 Unauthorized - 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 表示用户没有权限(令牌、用户名、密码错误)
  • 403 表示用户得到授权(与401错误相对),但是访问是被禁止
  • 404 NOT FOUND - 请求资源不存在,可能是输入了错误的URL
  • 500 - 服务器内部发生了不可预期的错误
  • 503 Server Unavailable - 服务器当前不能处理客户端的请求,一段时间后可能恢复正常。

RestFul和非RestFul比较

API name 非 RestFul RestFul
获取dog /dogs/query/{dogid} GET /dogs/{dogid}
插入dog /dogs/add POST /dogs
更新dog /dogs/update/{dogid} PUT /dogs/{dogid}
删除dog /dogs/delete/{dogid} DELETE /dogs/{dogid}

示例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
32
33
34
35
36
37
@RestController
public class MyRestController {

@Resource
private UserLoginService userLoginService;

@GetMapping("/users")
public List<UserLogin> queryAll() {
return userLoginService.queryAll();
}

@GetMapping("/users/{id}")
public UserLogin queryById(@PathVariable("id") Integer id) {
return userLoginService.queryById(id);
}

@PostMapping("/users")
public int add(UserLogin userLogin) {
return userLoginService.add(userLogin);
}

@PutMapping("/users/{id}")
public int update(@PathVariable("id") Integer id,
@RequestParam("username") String username,
@RequestParam("password") String password) {
UserLogin user = userLoginService.queryById(id);
user.setUsername(username);
user.setPassword(password);
return userLoginService.update(user);
}

@DeleteMapping("/users/{id}")
public int delete(@PathVariable("id") Integer id) {
return userLoginService.deletebyId(id);
}

}

hCaYuV.png]

Swagger

  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- swagger 2.9.2
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
    </dependency>
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
    </dependency>
    -->

    <!--swagger3-->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
    </dependency>
  2. 配置

    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
    48
    49
    50
    51
    52
    53
    54
    package com.biubiu.common.config;

    import io.swagger.annotations.ApiOperation;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.oas.annotations.EnableOpenApi;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.Contact;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;

    /**
    * Swagger API文档配置类
    *
    * @author biubiu
    * @date 2020/11/27 23:59
    */
    @Configuration
    @EnableOpenApi
    public class Swagger3Config {

    @Bean
    public Docket createRestApi() {
    return new Docket(DocumentationType.OAS_30)
    .apiInfo(apiInfo())
    .select()
    .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
    .paths(PathSelectors.any())
    .build();
    //.globalOperationParameters(headerParameter());//全局配置
    }

    private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
    .title("Swagger3 API 文档标题")
    .description("API 文档描述")
    .contact(new Contact("biubiu", "https://www.yuque.com/biubiu-note", "baijqmail@163.com"))
    .version("1.0.0")
    .build();
    }
    private List<Parameter> headerParameter() {
    ParameterBuilder cityen = new ParameterBuilder();
    cityen.name("cityen").description("城市简写:sz,tj").modelRef(new ModelRef("string")).parameterType("header").required(false).build();

    ParameterBuilder platform = new ParameterBuilder();
    platform.name("platform").description("平台:ios,android,wap,wechat").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
    pars.add(cityen.build());
    pars.add(version.build());
    return pars;
    }
    }
  3. 访问Swagger3
    http://ip:port/swagger-ui/

  4. 常用注解

注解 描述
@Api 标记一个Controller类做为swagger文档资源
@ApiOperation 用在Controller里的方法上
@ApiModel 标记实体对象
@ApiModelProperty 标记实体对象属性
@ApiParam 用于Controller中方法的参数说明,单个参数
@ApiIgnore 用来屏蔽某些接口或参数,使其不在页面上显示

遇到的问题

错误描述

写完Result之后就基于测试,没注意检查,当我访问测试接口 http://localhost:8080/api/demo 的时候报错,状态码406,如下图

h95MTJ.png]

h95Kw4.png]

然后看看控制台,,我尼玛,这啥问题呀

1
2
3
4
5
6
7
8
2021-08-23 11:53:37.097  INFO 14532 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-08-23 11:53:37.528 INFO 14532 --- [ main] com.biubiu.api.RestFulApplication : Started RestFulApplication in 3.801 seconds (JVM running for 5.205)
2021-08-23 11:53:46.718 INFO 14532 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-08-23 11:53:46.718 INFO 14532 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-08-23 11:53:46.719 INFO 14532 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2021-08-23 11:53:46.776 WARN 14532 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation]
2021-08-23 11:53:50.096 WARN 14532 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation]
2021-08-23 11:53:53.173 WARN 14532 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation]

接着百度了下,杂七乱八的,有说包不全的,需要导入jsckson的,有说需要加context-type的,然鹅。。我这里都不管用。。。

于是乎,就仔细检查了下代码,,此时已经一万只草泥马在奔腾,原来是Result返回类少写了Getter。。。mdzz!!哎。。。一言难尽就。不说了,赶紧加上就可以了

1
2
3
4
@Getter//就这个,一定要认值
public class Result<T> implements Serializable {

}

总结分析一下吧,加深印象。。其实是给自己找点面子 😂

这里必须提到 @ResponseBody 注解,是将controller的方法返回的对象 通过适当的转换器 转换为指定的格式之后,写入到response对象的body区(响应体中),通常用来返回JSON数据

该注解用于将Controller的方法返回的对象,通过适当的 HttpMessageConverter 转换为指定格式后,写入到Response对象的body数据区

转换的时候会用到Getter方法,就报错了

老铁,想请我喝哇哈哈嘛!![笑哭]