補習系列-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("", "", "[email protected]"))
.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篇" ,期待更多精彩內容^-^
補習系列-springboot-restful應用