近年来移动互联网的发展,前端设备层出不穷(手机、平板、桌面电脑、其他专用设备…),因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信,于是 RESTful 火了,它可以通过一套统一的接口为 Web,iOS和Android提供服务
image.png

1. RESTful API 规范

协议

API 与用户的通信协议,总是使用 https 协议。

域名

应该尽量将API部署在专用域名之下。
https://api.example.com
如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。
https://example.org/api/

版本

将 API 的版本号放入 URL。https://api.example.com/v1/

路径

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,API中的名词建议使用复数。
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

下面是一些例子。

  • GET /v1/zoos:列出所有动物园
  • POST /v1/zoos:新建一个动物园
  • GET /v1/zoos/id:获取某个指定动物园的信息
  • PUT /v1/zoos/id:更新某个指定动物园的信息(提供该动物园的全部信息)
  • DELETE /v1/zoos/id:删除某个动物园
  • GET /v1/zoos/id/animals:列出某个指定动物园的所有动物
  • DELETE /v1/zoos/id/animals/id:删除某个指定动物园的指定动物

    过滤信息

    如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
    下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量

  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。
比如,GET /v1/zoo/id/animals 与 GET /animals?zoo_id=id的含义是相同的。

状态码

服务器向用户返回的状态码和提示信息,常见的有以下:

  • 200 OK - [GET]:服务器成功返回用户请求的数据。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误。

    错误处理

    如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
    { error: “Invalid API key” }

    2. 公共接口规范

    公共请求头

    | 名称 | 必填 | 说明 | | —- | —- | —- | | Authorization | 否 | 用于验证请求合法性的认证信息。
    使用场景:非匿名请求 |

公共响应头

头部名称 必填 说明
Content-Type 正常情况下该值将被设为 application/json,表示返回 JSON 格式的文本信息。

成功响应

成功响应格式

  1. HTTP/1.1 200 OK
  2. Content-Type: application/json
  3. {
  4. "code": 200,
  5. "msg": "",
  6. "data":
  7. }

错误响应

当您请求出现错误时,响应头部信息包括:

  • Content-Type: application/json
  • 一个合适的 3xx,4xx,或者 5xx 的 HTTP 状态码

错误响应格式:

  1. HTTP/1.1 状态码 信息
  2. Content-Type: application/json
  3. {
  4. "code": <httpCode int>,
  5. "error": "<errMsg string>"
  6. }

响应状态码

HTTP状态码 说明
200 请求成功
401 认证授权失败
403 权限不足,拒绝访问。
404 资源不存在
包括空间资源不存在;镜像源资源不存在。
405 请求方式错误
主要指非预期的请求方式。

自定义的其它错误码….

设计一组 RESTful API 完成用户信息的维护。

请求类型 URL 说明
GET /api/sys/users 查询用户列表
POST /api/sys/users 创建用户
GET /api/sys/users/{id} 根据 id 查询用户
PUT /api/sys/users/{id} 根据 id 更新用户
DELETE /api/sys/users/{id} 根据 id 删除用户

3. 查询用户接口

描述

根据用户名、邮箱分页查询用户信息

请求

请求语法

  1. GET /api/sys/users HTTP/1.1
  2. Content-Type: application/x-www-form-urlencoded
  3. Authorization: <AccessToken>

请求参数

参数名称 必填 说明
name 根据用户名模糊查询用户
email 根据用户邮箱模糊查询用户
pageNum 当前页
pageSize 一页显示多少条

请求头

该请求操作的实现使用了所有操作的公共请求头。

请求内容

该请求操作的请求体为空。

响应

响应语法

HTTP/1.1 200 OK
Content-Type: application/json
{
  "code": 200, 
  "msg": "查询用户列表成功",
  "data": {}
}

响应头

该请求操作的实现使用了所有操作的公共响应头。

响应内容

名称 必填 说明
id 用户id
name 用户名
age 年龄
email 邮箱

响应状态码

该操作的实现不会返回特殊错误。有关错误和错误代码列表的一般信息。

示例

请求示例

GET /api/sys/users?name=jack&pageSize=10&pageNum=1 HTTP/1.1
Authorization: j853F3bLkWl59I5BOkWm6q1Z1mZClpr9Z9CLfDE0

响应示例

{
  "msg": "请求成功",
  "current": 1,
  "total": 7,
  "code": 200,
  "size": 10,
  "data": [
    {
      "id": 1,
      "name": "jack",
      "age": 18,
      "email": "jack@126.com"
    },
    ....
  ]
}

4. 新增用户接口

描述

请求

请求语法

POST /api/sys/users HTTP/1.1
Content-Type: application/x-www-form-urlencoded 
Authorization: <AccessToken>

请求参数

参数名称 必填 说明
name 用户名
email 邮箱
age 年龄

请求头

该请求操作的实现使用了所有操作的公共请求头。

请求内容

该请求操作的请求体为空。

响应

响应语法

HTTP/1.1 200 OK
Content-Type: application/json
{
  "code": 200, 
  "msg": "新增用户成功"
}

响应头

该请求操作的实现使用了所有操作的公共响应头。

响应内容

参考公共响应内容

响应状态码

该操作的实现不会返回特殊错误。有关错误和错误代码列表的一般信息。

示例

请求示例

POST /api/sys/users HTTP/1.1
Authorization: j853F3bLkWl59I5BOkWm6q1Z1mZClpr9Z9CLfDE0

响应示例

HTTP/1.1 200 OK
Content-Type: application/json
{
  "code": 200, 
  "msg": "新增用户成功",
  "data": {}
}

5. 实现接口

引入依赖

<!-- springmvc 、servlet 、tomcat -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

公共类库

image.png

响应请求对象

package com.imcode.common.vo;

import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.http.HttpStatus;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 响应对象
 */
public class Result extends ConcurrentHashMap<String, Object> {

    public Result() {
        this.put("code", HttpStatus.OK.value());
        this.put("msg", "请求成功");
    }

    public static Result ok() {
        return new Result();
    }

    public static Result ok(String msg) {
        return Result.ok().put("msg", msg);
    }

    public static Result ok(Object data) {
        return Result.ok().put("data", data);
    }

    public static Result ok(IPage<?> page) {
        return Result.ok()
                // 当前页数据
                .put("data", page.getRecords())
                // 当前页面码
                .put("current", page.getCurrent())
                // 一页显示多少条
                .put("size", page.getSize())
                // 满足查询条件的总记录数
                .put("total", page.getTotal());
    }

    public static Result ok(String msg, Object data) {
        return Result.ok(msg).put("data", data);
    }

    public static Result error(HttpStatus status, String error) {
        return Result.ok()
                .put("code", status)
                .put("error", error)
                .put("msg", "");
    }

    @Override
    public Result put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}

分页对象

package com.imcode.common.vo;
import lombok.Data;

@Data
public class PageVO {
    // 当前页码
    private Integer current = 1;
    // 每页显示多少条
    private Integer size = 10;
}

用户接口

查询条件对象

image.png

package com.imcode.sys.vo;

import com.imcode.common.vo.PageVO;
import lombok.Data;

@Data
public class UserQuery extends PageVO {
    private String name;
    private String email;
}

实现查询接口

image.png

package com.imcode.sys.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.imcode.common.vo.Result;
import com.imcode.sys.entity.User;
import com.imcode.sys.service.UserService;
import com.imcode.sys.vo.UserQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author jack
 * @since 2022-07-02
 */
@RestController
@RequestMapping("/api/sys")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public Result list(UserQuery param) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
         wrapper.like(StringUtils.hasLength(query.getName()), User::getName, query.getName());
        wrapper.like(StringUtils.hasLength(query.getEmail()), User::getEmail, query.getEmail());
        IPage<User> page = 
            userService.page(new Page<>(param.getCurrent(), param.getSize()), wrapper);
        return Result.ok(page);
    }

    @PostMapping("/users")
    public Result save(User user) {
        userService.save(user);
        return Result.ok("新增用户成功");
    }

}

没有前端,如何测试?API文档和测试工具

6. Swagger 构建 API 文档

Swagger 介绍

  • 轻松构建 RESTful API 文档。
  • 文档和代码融合,修改代码逻辑的同时方便更新文档说明。
  • 提供 API 接口调试功能。

官方界面有点丑,移步knife4j:https://doc.xiaominfo.com/
image.png

引入依赖

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.9</version>
</dependency>

配置类

image.png

package com.imcode.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.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {

    @Bean
    public Docket defaultApi() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        .title("XX系统API接口")
                        .description("在线API文档")
                        .version("1.0")
                        .build())
                .select()
                //.apis(RequestHandlerSelectors.basePackage("com.imcode"))
                .apis(RequestHandlerSelectors
                        .withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}

重启应用,访问:http://localhost:8080/doc.html
image.png
如果报错,添加如下配置:

spring:
  mvc:
    pathmatch:
    matching-strategy: ant_path_matcher

添加文档内容

image.png

控制器

@Api(tags = "用户模块")
@RestController
@RequestMapping("/api/sys")
public class UserController {
    @Autowired
    private UserService userService;

    @ApiOperation(value = "分页查询")
    @GetMapping("/users")
    public Result list(UserQuery param) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(param.getName() != null, User::getName, param.getName());
        wrapper.like(param.getEmail() != null, User::getEmail, param.getEmail());
        IPage<User> page = userService.page(new Page<>(param.getCurrent(), param.getSize()), wrapper);
        return Result.ok(page);
    }

    @ApiOperation(value = "新增用户")
    @PostMapping("/users")
    public Result save(User user) {
        userService.save(user);
        return Result.ok("新增用户成功");
    }

}

实体类

image.png

@Getter
@Setter
@TableName("t_user")
@ApiModel(description="用户信息")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键")
    private Integer id;

    @ApiModelProperty("姓名")
    private String name;

    @ApiModelProperty("年龄")
    private Integer age;

    @ApiModelProperty("邮箱")
    private String email;

}

VO类

image.png

@Data
@ApiModel(description="用户查询条件")
public class UserQuery extends PageVO {

    @ApiModelProperty("姓名")
    private String name;

    @ApiModelProperty("邮箱")
    private String email;
}

image.png

@Data
@ApiModel(description="分页信息")
public class PageVO {
    // 当前页码
    @ApiModelProperty("页码")
    private Integer current = 1;
    // 一页显示多少条
    @ApiModelProperty("一页显示多少条")
    private Integer size = 10;
}

常用注解

  • @Api:修饰整个类,描述Controller的作用;
  • @ApiOperation:描述一个类的一个方法,或者说一个接口;
  • @ApiParam:单个参数描述;用在方法参数上
  • @ApiModel:用对象来接收参数;
  • @ApiProperty:用对象接收参数时,描述对象的一个字段;
  • @ApiIgnore:使用该注解忽略这个API;
  • @ApiImplicitParam:一个请求参数;用在方法上
  • @ApiImplicitParams:多个请求参数。用在方法上
  • ……

    参考示例

    @Api(value = "MemberApi", description = "用户登录、注册、账户合法性校验")
    @RestController
    @RequestMapping("/api/member")
    public class MemberApi {
    
      /**
       * 会员注册
       * @param
       * @return
       */
      @ApiOperation(value = "会员注册", notes = "网站会员注册")
      @ApiImplicitParam(name = "member", value = "会员详细信息", dataType = "Member")
      @PostMapping("/register")
      public R register(@RequestBody Member member) {
          try {
              return R.success();
          } catch (Exception e) {
              e.printStackTrace();
              return R.error(e.getMessage());
          }
      }
      /**
       * 登录
       * @param
       * @return
       */
      @ApiOperation(value = "会员登录", notes = "网站会员登录")
      @ApiImplicitParams({
              @ApiImplicitParam(name = "account", value = "会员帐号",
                                required = true, paramType = "query", 
                                dataType = "String"),
              @ApiImplicitParam(name = "password", value = "会员密码", 
                                required = true, paramType = "query",
                                dataType = "String")
      })
      @PostMapping("/login")
      public R login(String account, String password) {
          try {
              return R.success();
          } catch (Exception e) {
              e.printStackTrace();
              return R.error(e.getMessage());
          }
      }
      /**
       * 检查帐号是否存在
       *
       * @param
       * @return
       */
      @ApiOperation(value = "检查帐号是否存在", notes = "根检查帐号是否存在")
      @GetMapping("/checkaccount/{account}")
      public R checkAccount(@ApiParam(name = "account",required = true,value = "用户帐号") @PathVariable String account) {
          try {
              return R.success();
          } catch (Exception e) {
              e.printStackTrace();
              return R.error(e.getMessage());
          }
      }
    }
    

    7. ApiPost

    image.png
    官方文档地址:https://wiki.apipost.cn/
    image.png