一、目标

  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>>(); @PostConstruct
public 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 { @Autowired
private PetManager dataManager; /**
* 添加宠物
*
* @param customer
* @param pet
* @return
*/
@PostMapping
public ResponseEntity<Object> addPet(@PathVariable String customer, @RequestBody Pet pet) {
validateCustomer(customer); Pet newPet = dataManager.addPet(customer, pet); // 返回 201.created
if (newPet != null) {
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{petId}")
.buildAndExpand(newPet.getPetId()).toUri(); return ResponseEntity.created(location).build();
} // 返回 204.noContent
return ResponseEntity.noContent().build();
} /**
* 获取宠物列表
*
* @param customer
* @return
*/
@GetMapping
@ResponseBody
public List<Pet> listPets(@PathVariable String customer) {
validateCustomer(customer); List<Pet> pets = dataManager.getPets(customer);
return pets;
} /**
* 获取某个宠物
*
* @param customer
* @param petId
*/
@GetMapping("/{petId}")
@ResponseBody
public 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();
} @Bean
public 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 = "资源不存在")
})
@PostMapping
public 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. 补习系列(16)-springboot mongodb 数据库应用技巧

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

  7. 补习系列(1)-springboot项目基础搭建课

    目录 前言 一.基础结构 二.添加代码 三.应用配置 四.日志配置 五.打包部署 小结 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区 ...

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

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

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

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

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

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

随机推荐

  1. Pytorch多GPU训练

    Pytorch多GPU训练 临近放假, 服务器上的GPU好多空闲, 博主顺便研究了一下如何用多卡同时训练 原理 多卡训练的基本过程 首先把模型加载到一个主设备 把模型只读复制到多个设备 把大的batc ...

  2. Oracle截取JSON字符串内容

    CREATE OR REPLACE FUNCTION PLATFROM.parsejsonstr(p_jsonstr varchar2,startkey varchar2,endkey varchar ...

  3. 解决fastJson无序问题

    对外提供接口,第三方传过来的参数没问题.可是用fastJson 转换的出现 参数顺序不一致,导致 验签失败 解决fastJosn 转换无序问题 https://github.com/alibaba/f ...

  4. WebService常用接口链接(很全面,值得一看)

    天气预报Web服务,数据来源于中国气象局Endpoint :http://www.webxml.com.cn/WebServices/WeatherWebService.asmxDisco       ...

  5. Python-常用字符串操作

    name = 'shanbaoliang.exe' print(name.capitalize()) #将字符串首字母大写 print(name.center(50,'-')) #把字符串居中,并用特 ...

  6. JS数组slice()和splice()的区别

    以前还是纯小白的时候,总会搞混JS数组的 slice() 和 splice() 方法.因为这2个方法名字太像了,就差一个字母,语法也有类似之处.    现在久了没用,有时候也会忘记,所以做一个总结来区 ...

  7. [Swift]LeetCode304. 二维区域和检索 - 矩阵不可变 | Range Sum Query 2D - Immutable

    Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper lef ...

  8. [Swift]LeetCode468. 验证IP地址 | Validate IP Address

    Write a function to check whether an input string is a valid IPv4 address or IPv6 address or neither ...

  9. [Swift]LeetCode507. 完美数 | Perfect Number

    We define the Perfect Number is a positive integer that is equal to the sum of all its positive divi ...

  10. [Swift]LeetCode805. 数组的均值分割 | Split Array With Same Average

    In a given integer array A, we must move every element of A to either list B or list C. (B and C ini ...