补习系列(5)-springboot- restful应用
一、目标
- 了解 Restful 是什么,基本概念及风格;
- 能使用SpringBoot 实现一套基础的 Restful 风格接口;
- 利用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来统一定义数据信息的格式标准。
通常,Accept、Content-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应用的更多相关文章
- 补习系列(15)-springboot 分布式会话原理
目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope ...
- 补习系列(14)-springboot redis 整合-数据读写
目录 一.简介 二.SpringBoot Redis 读写 A. 引入 spring-data-redis B. 序列化 C. 读写样例 三.方法级缓存 四.连接池 小结 一.简介 在 补习系列(A3 ...
- 补习系列(4)-springboot 参数校验详解
目录 目标 一.PathVariable 校验 二.方法参数校验 三.表单对象校验 四.RequestBody 校验 五.自定义校验规则 六.异常拦截器 参考文档 目标 对于几种常见的入参方式,了解如 ...
- 补习系列(19)-springboot JPA + PostGreSQL
目录 SpringBoot 整合 PostGreSQL 一.PostGreSQL简介 二.关于 SpringDataJPA 三.整合 PostGreSQL A. 依赖包 B. 配置文件 C. 模型定义 ...
- 补习系列(18)-springboot H2 迷你数据库
目录 关于 H2 一.H2 用作本地数据库 1. 引入依赖: 2. 配置文件 3. 样例数据 二.H2 用于单元测试 1. 依赖包 2. 测试配置 3. 测试代码 小结 关于 H2 H2 数据库是一个 ...
- 补习系列(16)-springboot mongodb 数据库应用技巧
目录 一.关于 MongoDB 二.Spring-Data-Mongo 三.整合 MongoDB CRUD A. 引入框架 B. 数据库配置 C. 数据模型 D. 数据操作 E. 自定义操作 四.高级 ...
- 补习系列(1)-springboot项目基础搭建课
目录 前言 一.基础结构 二.添加代码 三.应用配置 四.日志配置 五.打包部署 小结 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区 ...
- 补习系列(17)-springboot mongodb 内嵌数据库
目录 简介 一.使用 flapdoodle.embed.mongo A. 引入依赖 B. 准备测试类 C. 完善配置 D. 启动测试 细节 二.使用Fongo A. 引入框架 B. 准备测试类 C.业 ...
- 补习系列(13)-springboot redis 与发布订阅
目录 一.订阅发布 常见应用 二.Redis 与订阅发布 三.SpringBoot 与订阅发布 A. 消息模型 B. 序列化 C. 发布消息 D. 接收消息 小结 一.订阅发布 订阅发布是一种常见的设 ...
- 补习系列(12)-springboot 与邮件发送
目录 一.邮件协议 关于数据传输 二.SpringBoot 与邮件 A. 添加依赖 B. 配置文件 C. 发送文本邮件 D.发送附件 E. 发送Html邮件 三.CID与图片 参考文档 一.邮件协议 ...
随机推荐
- CGI、FastCGI、PHP-FPM联系与区别(理解总结自其他博文)
参考:http://blog.csdn.net/tyrantbear/article/details/52077321 参考:http://mp.weixin.qq.com/s?src=11& ...
- 网页加水印 svg 方式
/** *网页加水印 svg 方式 * * @export * @param {*} [{ * container = document.body, * content = '请勿外传', * wid ...
- js 事件模型详解
把js的事件模型,分为两类,DOM0级和DOM2级, DOM0级 通常直接在DOM对象上绑定函数对象,指定事件类型,dom.onClick = function(){};类似于这种写法,移除事件,则直 ...
- 发现一款适合php网站的管理软件——kodexplorer,能取代ftp
今天偶然看到可以利用可道云来管理网站的文件.可道云不需要数据库,因此搭建非常简单.搭建的方法也很简单.传统的 WordPress 站点的文件管理,通常是是通过 FTP 或者服务器面板自带的文件管理器来 ...
- Django之csrf防御机制
1.csrf攻击过程 csrf攻击说明: 1.用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A; 2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站 ...
- gradle环境变量设置_配置注意事项
看<Spring源码深度解析>的时候,在windows7系统中按照书里配置gradle环境变量,配置完后输入gradle -v,一直显示:'gradle'不是内部或外部命令,也不是可执行的 ...
- 查询Python版本
- 干货分享: 长达150页的openvswitch的ppt,不实验无真相
下载链接: Openvswitch实验教程 http://files.cnblogs.com/popsuper1982/Openvswtich.pptx 一.概论 Software Defined N ...
- VSCode调试Flutter的问题解决
错误:Target of URI doesn't exist: package:fultter/material.dart 原因:flutter版本不匹配.flutter升级到了最新版本,而项目引用的 ...
- [Swift]LeetCode310. 最小高度树 | Minimum Height Trees
For an undirected graph with tree characteristics, we can choose any node as the root. The result gr ...