一、目标

  1. 了解 Restful 是什么,基本概念及风格;
  2. 能使用SpringBoot 实现一套基础的 Restful 风格接口;
  3. 利用Swagger 生成清晰的接口文档。

二、Restful 入门

什么是REST 
摘自百科的定义:REST即表述性状态转移(英文:Representational State Transfer,简称REST)
是Roy Fielding博士(HTTP规范主要贡献者)在2000年的论文中提出来的一种软件架构风格。
是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。

通俗点说,REST就是一组架构约束准则;在这些准则中,有不少是利用了现有的WEB标准能力。
而最终的目的则是简化当前业务层的设计及开发工作。

Restful API 则是指符合REST架构约束的API,关于这个词在早年前其实已经非常流行,但大多数开发者对其仍然
处于观望状态,并不一定会立即采用。这个相信与当时技术社区的成熟度及氛围是密切相关。
无论如何,在微服务架构如此流行的今天,Restful API已经成为了一种必备的的标准设计风格

关键要点
理解 Restful 风格需要理解以下几点:

  • 资源

资源指的就是一个抽象的信息实体,可以是一个用户、一首歌曲、一篇文章,只要是可作为引用的对象就是资源。
每个资源通常会被映射到一个URI,通过访问这个URI可以获取到信息。

  • 资源的表述

资源表述(Representation)指的则是资源的外在表现形式
比如一个帖子,可以通过HTML格式展现,也可以通过XML、JSON等格式输出到客户端。

在前面的文章(SpringBoot-Scope详解)中提到,HTTP协议通过MIME来统一定义数据信息的格式标准。
通常,AcceptContent-Type可以用来指定客户端及服务端可接受的信息格式,而这个就是资源的表述

  • 状态转移

在HTTP访问过程中,资源的状态发生变化。这里会涉及到以下的几个动词:

名称 语义
GET 获取资源
POST 新建资源
PUT 更新资源
DELETE 删除资源

对于不同的访问方法,服务器会产生对应的行为并促使资源状态产生转换。

关于无状态
Restful 是无状态的设计,这点意味着交互过程中的请求应该能包含所有需要的信息,
而不需要依赖于已有的上下文。
然而 JavaEE中存在一些违背的做法,比如Cookie中设置JSESSIONID,
在多次请求间传递该值作为会话唯一标识,这标识着服务端必须保存着这些会话状态数据。

PlayFramework框架实现了无状态的Session,其将会话数据经过加密编码并置入Cookie中,
这样客户端的请求将直接携带上全部的信息,是
无状态的请求**,这点非常有利于服务端的可扩展性。

三、SpringBoot 实现 Restful

接下来,我们利用 SpringBoot 来实现一个Restful 风格的样例。

说明
基于 PetStore(宠物店) 的案例,实现对某顾客(Customer)名下的宠物(Pet)的增删改查。

1. 实体定义

Customer

public class Customer {private String name;public Customer() {super();}public Customer(String name) {super();this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}}

Customer 只包含一个name属性,我们假定这是唯一的标志。

Pet

public class Pet {private String petId;private String name;private String type;private String description;public String getPetId() {return petId;}public void setPetId(String petId) {this.petId = petId;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getType() {return type;}public void setType(String type) {this.type = type;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}}

Pet 包含了以下几个属性

属性名 描述
petId 宠物ID编号
name 宠物名称
type 宠物类型
description 宠物的描述

2. URL资源

基于Restful 的原则,我们定义了以下的一组URL:

接口 方法 URL
添加宠物 POST /rest/pets/{customer}
获取宠物列表 GET /rest/pets/{customer}
获取宠物信息 GET /rest/pets/{customer}/{petId}
更新宠物信息 PUT /rest/pets/{customer}/{petId}
删除宠物 DELETE /rest/pets/{customer}/{petId}

3. 数据管理

接下来实现一个PetManager 类,用于模拟在内存中对Pet数据进行增删改查
代码如下:

@Component
public class PetManager {private static Map<String, Customer> customers = new ConcurrentHashMap<String, Customer>();private static Map<String, Map<String, Pet>> pets = new ConcurrentHashMap<String, Map<String, Pet>>();@PostConstructpublic void init() {String[] customerNames = new String[] { "Lilei", "Hanmeimei", "Jim Green" };for (String customerName : customerNames) {customers.put(customerName, new Customer(customerName));}}/*** 获取customer* * @param customer* @return*/public Customer getCustomer(String customer) {if (StringUtils.isEmpty(customer)) {return null;}return customers.get(customer);}/*** 获取customer名下的 pet 列表* * @param customer* @return*/public List<Pet> getPets(String customer) {if (StringUtils.isEmpty(customer)) {return Collections.emptyList();}if (!pets.containsKey(customer)) {return Collections.emptyList();}return pets.get(customer).values().stream().collect(Collectors.toList());}/*** 获取某个pet* * @param customer* @param petId* @return*/public Pet getPet(String customer, String petId) {if (StringUtils.isEmpty(customer) || StringUtils.isEmpty(petId)) {return null;}if (!pets.containsKey(customer)) {return null;}return pets.get(customer).get(petId);}/*** 删除pet* * @param customer* @param petId* @return*/public boolean removePet(String customer, String petId) {if (StringUtils.isEmpty(customer) || StringUtils.isEmpty(petId)) {return false;}if (!pets.containsKey(customer)) {return false;}return pets.get(customer).remove(petId) != null;}/*** 添加pet* * @param customer* @param pet* @return*/public Pet addPet(String customer, Pet pet) {if (StringUtils.isEmpty(customer) || pet == null) {return null;}Map<String, Pet> customerPets = null;if (!pets.containsKey(customer)) {customerPets = new LinkedHashMap<String, Pet>();Map<String, Pet> previous = pets.putIfAbsent(customer, customerPets);// 已经存在if (previous != null) {customerPets = previous;}} else {customerPets = pets.get(customer);}if (pet.getPetId() == null) {pet.setPetId(UUID.randomUUID().toString());}customerPets.put(pet.getPetId(), pet);return pet;}/*** 更新某个pet* * @param customer* @param petPojo* @return*/public Pet updatePet(String customer, Pet petPojo) {if (StringUtils.isEmpty(customer) || petPojo == null) {return null;}if (petPojo.getPetId() == null) {return null;}Pet pet = getPet(customer, petPojo.getPetId());pet.setType(petPojo.getType());pet.setName(petPojo.getName());pet.setDescription(petPojo.getDescription());return pet;}}

4. 控制层实现

SpringBoot 提供了 @RestController,用于快速定义一个Restful 风格的Controller类
@RestController=@ResponseBody + @Controller

@RestController
@RequestMapping("/rest/pets/{customer}")
public class RestApiController {@Autowiredprivate PetManager dataManager;/*** 添加宠物* * @param customer* @param pet* @return*/@PostMappingpublic ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet pet) {validateCustomer(customer);Pet newPet = dataManager.addPet(customer, pet);// 返回 201.createdif (newPet != null) {URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{petId}").buildAndExpand(newPet.getPetId()).toUri();return ResponseEntity.created(location).build();}// 返回 204.noContentreturn ResponseEntity.noContent().build();}/*** 获取宠物列表* * @param customer* @return*/@GetMapping@ResponseBodypublic List<Pet> listPets(@PathVariable String customer) {validateCustomer(customer);List<Pet> pets = dataManager.getPets(customer);return pets;}/*** 获取某个宠物* * @param customer* @param petId*/@GetMapping("/{petId}")@ResponseBodypublic Pet getPet(@PathVariable String customer, @PathVariable String petId) {validateCustomer(customer);validatePet(customer, petId);Pet pet = dataManager.getPet(customer, petId);return pet;}/*** 更新宠物信息* * @param customer* @param petId* @param pet*/@PutMapping("/{petId}")public ResponseEntity<Object> updatePet(@PathVariable String customer, @PathVariable String petId, @RequestBody Pet pet) {validateCustomer(customer);validatePet(customer, petId);pet.setPetId(petId);Pet petObject = dataManager.updatePet(customer, pet);if (petObject != null) {return ResponseEntity.ok(petObject);}return ResponseEntity.noContent().build();}/*** 删除某个宠物* * @param customer* @param petId* @return*/@DeleteMapping("/{petId}")public ResponseEntity<Object> removePet(@PathVariable String customer, @PathVariable String petId) {validateCustomer(customer);validatePet(customer, petId);dataManager.removePet(customer, petId);return ResponseEntity.ok().build();}

上述代码中已经实现了完整的增删改查语义。
在Restful 风格的API 接口定义中,往往会引用 HTTP 状态码用于表示不同的结果,比如一些错误的状态类型。
这里我们Customer、Pet 进行存在性校验,若资源不存在返回404_NotFound。

    /*** 校验customer是否存在* * @param customer*/private void validateCustomer(String customer) {if (dataManager.getCustomer(customer) == null) {throw new ObjectNotFoundException(String.format("the customer['%s'] is not found", customer));}}/*** 校验pet是否存在* * @param customer*/private void validatePet(String customer, String petId) {if (dataManager.getPet(customer, petId) == null) {throw new ObjectNotFoundException(String.format("the pet['%s/%s'] is not found", customer, petId));}}

自定义异常拦截

    /*** 自定义异常,及拦截逻辑* * @author atp**/@SuppressWarnings("serial")public static class ObjectNotFoundException extends RuntimeException {public ObjectNotFoundException(String msg) {super(msg);}}@ResponseBody@ExceptionHandler(ObjectNotFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND)public String objectNotFoundExceptionHandler(ObjectNotFoundException ex) {return ex.getMessage();}

5. 接口验证

1. 添加宠物

URL
POST http://{{server}}/rest/pets/LiLei
请求内容

{"name": "Smart Baby","description": "very small and smart also.","type": "Dog"
}

返回示例

201 created
Content-Length →0
Date →Mon, 09 Jul 2018 05:15:01 GMT
Location →http://localhost:8090/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543

2. 获取宠物列表

URL
GET http://{{server}}/rest/pets/LiLei
请求内容

<Empty>

返回示例

200 OK
Content-Type →application/json;charset=UTF-8
Date →Mon, 09 Jul 2018 05:23:27 GMT
Transfer-Encoding →chunked
[{"petId": "b5400334-e7b3-42f1-b192-f5e7c3193543","name": "Smart Baby","type": "Dog","description": "very small and smart also."},{"petId": "610780af-94f1-4011-a175-7a0f3895163d","name": "Big Cat","type": "Cat","description": "very old but I like it."}
]

3. 查询宠物信息

URL
GET http://{{server}}/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543
请求内容

<Empty>

返回示例

200 OK
Content-Type →application/json;charset=UTF-8
Date →Mon, 09 Jul 2018 05:25:24 GMT
Transfer-Encoding →chunked
{"petId": "b5400334-e7b3-42f1-b192-f5e7c3193543","name": "Smart Baby","type": "Dog","description": "very small and smart also."
}

4. 更新宠物信息

URL
PUT http://{{server}}/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543
请求内容

{"name": "Big Cat V2","description": "I don't like it any more","type": "Cat"
}

返回示例

200 OK
Content-Type →application/json;charset=UTF-8
Date →Mon, 09 Jul 2018 05:31:28 GMT
Transfer-Encoding →chunked
{"petId": "a98e4478-e754-4969-851b-bcaccd67263e","name": "Big Cat V2","type": "Cat","description": "I don't like it any more"
}

5. 删除宠物

URL
DELETE http://{{server}}/rest/pets/LiLei/b5400334-e7b3-42f1-b192-f5e7c3193543
请求内容

<empty>

返回示例

200 OK
Content-Length →0
Date →Mon, 09 Jul 2018 05:32:51 GMT

相关出错

  • 客户不存在:404 the customer['test'] is not found
  • 宠物不存在:404 the pet['LiLei/b5400334-e7b3-42f1-b192-f5e7c31935431'] is not found

四、Swagger 的使用

关于Swagger

Swagger是目前非常流行的一个API设计开发框架(基于OpenApi),
可用于API的设计、管理、代码生成以及Mock测试等。

目前Swagger的应用非常广,其涵盖的开源模块也比较多,这里将使用swagger-ui实现API在线DOC的生成。

引入依赖

<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.7.0</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.7.0</version></dependency>

定义API配置


@EnableSwagger2
@Configuration
public class SwaggerConfig {public static final String VERSION = "1.0.0";@Value("${swagger.enable}")private boolean enabled;ApiInfo apiInfo() {return new ApiInfoBuilder().title("Pet Api Definition").description("The Petstore CRUD Example").license("Apache 2.0").licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html").termsOfServiceUrl("").version(VERSION).contact(new Contact("", "", "zalesfoo@163.com")).build();}@Beanpublic Docket customImplementation() {return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.withClassAnnotation(Api.class)).build().enable(enabled).apiInfo(apiInfo());}
}

@EnableSwagger2声明了Swagger的启用,Docket的Bean定义是API配置的入口,
可以设置API名称、版本号,扫描范围等。

声明API描述

在原有的Controller 方法上添加关于API的声明,如下:

@Api(value = "Pet Restful api")
@RestController
@RequestMapping("/rest/pets/{customer}")
public class RestApiController {@ApiOperation("添加宠物")@ApiImplicitParams({@ApiImplicitParam(paramType = "path", name = "customer", dataType = "String", required = true, value = "客户名", defaultValue = ""),@ApiImplicitParam(paramType = "body", name = "pet", dataType = "Pet", required = true, value = "pet 请求", defaultValue = "") })@ApiResponses({@ApiResponse(code = 201, message = "添加成功"),@ApiResponse(code = 404, message = "资源不存在")})@PostMappingpublic ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet pet) {...

为了能描述返回对象的文档说明,为Pet类做API声明:

@ApiModel("宠物信息")
public class Pet {@ApiModelProperty(name="petId", value="宠物ID")private String petId;@ApiModelProperty(name="name", value="宠物名称")private String name;@ApiModelProperty(name="type", value="宠物类型")private String type;@ApiModelProperty(name="description", value="宠物描述")private String description;

相关的注解:

注解 描述
@ApiModelProperty 用在出入参数对象的字段上
@Api 用于controller类
@ApiOperation 用于controller方法,描述操作
@ApiResponses 用于controller方法,描述响应
@ApiResponse 用于@ApiResponses内,描述单个响应结果
@ApiImplicitParams 用于controller的方法,描述入参
@ApiImplicitParam 用于@ApiImplicitParams内,描述单个入参
@ApiModel 用于返回对象类

访问文档

最后,访问 http://localhost:8000/swagger_ui.html,可看到生成的文档界面:

码云同步代码

参考文档

SpringBoot-tutorials-bookmarks
阮一峰-理解Restful架构
SprintBoot-使用Swagger发布API
swagger-2-documentation-for-spring-rest-api

欢迎继续关注"美码师的补习系列-springboot篇" ,期待更多精彩内容^-^

作者:美码师

补习系列(5)-springboot- restful应用相关推荐

  1. 补习系列(15)-springboot 分布式会话原理

    目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope ...

  2. 补习系列(14)-springboot redis 整合-数据读写

    目录 一.简介 二.SpringBoot Redis 读写 A. 引入 spring-data-redis B. 序列化 C. 读写样例 三.方法级缓存 四.连接池 小结 一.简介 在 补习系列(A3 ...

  3. 补习系列(4)-springboot 参数校验详解

    目录 目标 一.PathVariable 校验 二.方法参数校验 三.表单对象校验 四.RequestBody 校验 五.自定义校验规则 六.异常拦截器 参考文档 目标 对于几种常见的入参方式,了解如 ...

  4. 补习系列(19)-springboot JPA + PostGreSQL

    目录 SpringBoot 整合 PostGreSQL 一.PostGreSQL简介 二.关于 SpringDataJPA 三.整合 PostGreSQL A. 依赖包 B. 配置文件 C. 模型定义 ...

  5. 补习系列(18)-springboot H2 迷你数据库

    目录 关于 H2 一.H2 用作本地数据库 1. 引入依赖: 2. 配置文件 3. 样例数据 二.H2 用于单元测试 1. 依赖包 2. 测试配置 3. 测试代码 小结 关于 H2 H2 数据库是一个 ...

  6. 补习系列(17)-springboot mongodb 内嵌数据库

    目录 简介 一.使用 flapdoodle.embed.mongo A. 引入依赖 B. 准备测试类 C. 完善配置 D. 启动测试 细节 二.使用Fongo A. 引入框架 B. 准备测试类 C.业 ...

  7. 补习系列(16)-springboot mongodb 数据库应用技巧

    目录 一.关于 MongoDB 二.Spring-Data-Mongo 三.整合 MongoDB CRUD A. 引入框架 B. 数据库配置 C. 数据模型 D. 数据操作 E. 自定义操作 四.高级 ...

  8. 补习系列(13)-springboot redis 与发布订阅

    目录 一.订阅发布 常见应用 二.Redis 与订阅发布 三.SpringBoot 与订阅发布 A. 消息模型 B. 序列化 C. 发布消息 D. 接收消息 小结 一.订阅发布 订阅发布是一种常见的设 ...

  9. 补习系列(12)-springboot 与邮件发送

    目录 一.邮件协议 关于数据传输 二.SpringBoot 与邮件 A. 添加依赖 B. 配置文件 C. 发送文本邮件 D.发送附件 E. 发送Html邮件 三.CID与图片 参考文档 一.邮件协议 ...

  10. 补习系列(10)-springboot 之配置读取

    目录 简介 一.配置样例 二.如何注入配置 1. 缺省配置文件 2. 使用注解 3. 启动参数 还有.. 三.如何读取配置 @Value 注解 Environment 接口 @Configuratio ...

最新文章

  1. 2022-2028年中国PET薄膜行业市场深度分析及未来趋势预测报告
  2. 到「黄埔学院」去:打造AI首席架构师,第二期限量招募!
  3. Codeforces 724 G Xor-matic Number of the Graph 线性基+DFS
  4. VC6.0: fatal error C1010: unexpected end of file while looking for precompiled head
  5. struct和typedef struct的区别(转)
  6. 人类大脑每日24小时工作节奏表
  7. 变化的和不变的-让自己慢下来(49)
  8. 参数化的JUnit测试
  9. python写http post请求的四种请求体
  10. 华为回应“WIFI联盟撤销会员资格”:不会受个别组织影响
  11. SQL报错信息(3001-6999)
  12. TortoiseSVN的安装与使用
  13. 【Python读文件】'gbk' codec can't decode byte 0xb1 in position 5: illegal multibyte sequence
  14. 特征选择relief算法介绍
  15. stagefright框架
  16. 最新版微信小程序如何引入iconfont阿里矢量图库解决方案
  17. 通信LOGO隐秘史(设备商篇)
  18. 小型机 PC服务器 性能,pc服务器小型机
  19. 08 python 集合
  20. 接口测试平台代码实现134: 全局变量-13

热门文章

  1. codeblocks printf函数打印不出来_最全C语言基本程序交互函数之输出到屏幕
  2. java compare时间_java.time.LocalDateTime compareTo()方法
  3. Luogu P1164小A点菜
  4. dirname,basename的用法与用途
  5. BZOJ 1108: [POI2007]天然气管道Gaz
  6. 方阵和的行列式、方阵行列式的和
  7. GCD和NSThread延时执行对比
  8. bzoj2751 [HAOI2012]容易题(easy)
  9. 一位程序员的爱情故事
  10. 使用javascript在客户端获取URL参数值的函数