書接上文???
不斷發(fā)展的 REST API
通過一個額外的庫和幾行額外的代碼,您已將超媒體添加到您的應(yīng)用程序中。但這并不是使您的服務(wù)成為 RESTful 所需的唯一事情。REST 的一個重要方面是它既不是技術(shù)堆棧也不是單一標準。
REST 是架構(gòu)約束的集合,采用這些約束會使您的應(yīng)用程序更具彈性。彈性的一個關(guān)鍵因素是,當您對服務(wù)進行升級時,您的客戶不會遭受停機時間的困擾。
在“過去”的日子里,升級因破壞客戶端而臭名昭著。換句話說,升級到服務(wù)器需要更新客戶端。在當今時代,花費數(shù)小時甚至數(shù)分鐘進行升級的停機時間可能會造成數(shù)百萬美元的收入損失。
有些公司要求您向管理層提出一個計劃,以盡量減少停機時間。過去,您可以在周日凌晨 2:00 進行升級,此時負載最低。但在今天的基于互聯(lián)網(wǎng)的電子商務(wù)中,國際客戶在其他時區(qū),這樣的策略就沒有那么有效了。
基于 SOAP 的服務(wù)和基于CORBA 的服務(wù)非常脆弱。很難推出可以同時支持新舊客戶端的服務(wù)器。使用基于 REST 的實踐,這要容易得多。特別是使用 Spring 堆棧。
支持對 API 的更改
想象一下這個設(shè)計問題:您已經(jīng)推出了一個具有Employee基于此記錄的系統(tǒng)。該系統(tǒng)大受歡迎。你已經(jīng)把你的系統(tǒng)賣給了無數(shù)的企業(yè)。突然,需要拆分員工的姓名firstName并lastName出現(xiàn)。
哦哦。沒想到。
在您打開課程并用andEmployee替換單個字段之前,請停下來想一想。這會破壞任何客戶嗎?升級它們需要多長時間。您甚至控制所有訪問您服務(wù)的客戶端嗎?namefirstNamelastName
停機時間 = 損失金錢。管理層準備好了嗎?
有一個比 REST 早幾年的舊策略。
永遠不要刪除數(shù)據(jù)庫中的列。
— 未知
您始終可以將列(字段)添加到數(shù)據(jù)庫表中。但不要帶走一個。RESTful 服務(wù)中的原理是相同的。
將新字段添加到您的 JSON 表示中,但不要帶走任何字段。像這樣:
支持多個客戶端的 JSON
{ "id": 1, "firstName": "Bilbo", "lastName": "Baggins", "role": "burglar", "name": "Bilbo Baggins", "_links": { "self": { "href": "http://localhost:8080/employees/1" }, "employees": { "href": "http://localhost:8080/employees" } }}
請注意此格式如何顯示firstName, lastName, AND name?雖然它包含重復(fù)信息,但其目的是同時支持新老客戶。這意味著您可以升級服務(wù)器,而無需同時升級客戶端。一個可以減少停機時間的好舉措。
您不僅應(yīng)該以“舊方式”和“新方式”顯示這些信息,還應(yīng)該以兩種方式處理傳入的數(shù)據(jù)。
如何?簡單的。像這樣:
處理“舊”和“新”客戶的員工記錄
package payroll;import java.util.Objects;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;@Entityclass Employee { private @Id @GeneratedValue Long id; private String firstName; private String lastName; private String role; Employee() {} Employee(String firstName, String lastName, String role) { this.firstName = firstName; this.lastName = lastName; this.role = role; } public String getName() { return this.firstName + " " + this.lastName; } public void setName(String name) { String[] parts = name.split(" "); this.firstName = parts[0]; this.lastName = parts[1]; } public Long getId() { return this.id; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public String getRole() { return this.role; } public void setId(Long id) { this.id = id; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setRole(String role) { this.role = role; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Employee)) return false; Employee employee = (Employee) o; return Objects.equals(this.id, employee.id) && Objects.equals(this.firstName, employee.firstName) && Objects.equals(this.lastName, employee.lastName) && Objects.equals(this.role, employee.role); } @Override public int hashCode() { return Objects.hash(this.id, this.firstName, this.lastName, this.role); } @Override public String toString() { return "Employee{" + "id=" + this.id + ", firstName='" + this.firstName + '\'' + ", lastName='" + this.lastName + '\'' + ", role='" + this.role + '\'' + '}'; }}
這個類與以前版本的Employee. 讓我們回顧一下變化:
- 字段name已替換為firstName和lastName。
- 定義了舊name屬性的“虛擬”吸氣劑。getName()它使用firstNameandlastName字段來產(chǎn)生一個值。
- name還定義了舊屬性的“虛擬”設(shè)置器, setName(). 它解析傳入的字符串并將其存儲到適當?shù)淖侄沃小?/li>
當然,并非對 API 的每一次更改都像拆分字符串或合并兩個字符串一樣簡單。但是對于大多數(shù)場景來說,想出一組轉(zhuǎn)換肯定不是不可能的,對吧?
不要忘記更改預(yù)加載數(shù)據(jù)庫的方式(在 中LoadDatabase)以使用這個新的構(gòu)造函數(shù)。
log.info("Preloading " + repository.save(new Employee("Bilbo", "Baggins", "burglar")));
log.info("Preloading " + repository.save(new Employee("Frodo", "Baggins", "thief")));
適當?shù)姆磻?yīng)
朝著正確方向邁出的另一個步驟是確保您的每個 REST 方法都返回正確的響應(yīng)。像這樣更新 POST 方法:
處理“舊”和“新”客戶端請求的 POST
@PostMapping("/employees")ResponseEntity newEmployee(@RequestBody Employee newEmployee) { EntityModel entityModel = assembler.toModel(repository.save(newEmployee)); return ResponseEntity // .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) // .body(entityModel);}復(fù)制
- 新Employee對象像以前一樣保存。但是生成的對象使用EmployeeModelAssembler.
- Spring MVCResponseEntity用于創(chuàng)建HTTP 201 Created狀態(tài)消息。這種類型的響應(yīng)通常包含一個Location響應(yīng)頭,我們使用從模型的自相關(guān)鏈接派生的 URI。
- 此外,返回已保存對象的基于模型的版本。
通過這些調(diào)整,您可以使用相同的端點來創(chuàng)建新的員工資源,并使用遺留name字段:
$ curl -v -X POST localhost:8080/employees -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'
輸出如下所示:
> POST /員工 HTTP/1.1> 主機:本地主機:8080> 用戶代理:curl/7.54.0> 接受:*/*> 內(nèi)容類型:應(yīng)用程序/json> 內(nèi)容長度:46>< 位置:http://localhost:8080/employees/3< 內(nèi)容類型:application/hal+json;charset=UTF-8< 傳輸編碼:分塊< 日期:格林威治標準時間 2018 年 8 月 10 日星期五 19:44:43<{ “身份證”:3, "firstName": "Samwise", "lastName": "Gamgee", “角色”:“園丁”, "name": "Samwise Gamgee", “_鏈接”:{ “自己”: { “href”:“http://localhost:8080/employees/3” }, “雇員”: { "href": "http://localhost:8080/employees" } }}
這不僅使生成的對象在 HAL(name以及firstName/ lastName)中呈現(xiàn),而且Location標頭也填充了
http://localhost:8080/employees/3. 超媒體驅(qū)動的客戶端可以選擇“沖浪”到這個新資源并繼續(xù)與之交互。
PUT 控制器方法需要類似的調(diào)整:
為不同的客戶端處理 PUT
@PutMapping("/employees/{id}")ResponseEntity replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) { Employee updatedEmployee = repository.findById(id) // .map(employee -> { employee.setName(newEmployee.getName()); employee.setRole(newEmployee.getRole()); return repository.save(employee); }) // .orElseGet(() -> { newEmployee.setId(id); return repository.save(newEmployee); }); EntityModel entityModel = assembler.toModel(updatedEmployee); return ResponseEntity // .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) // .body(entityModel);}復(fù)制
然后使用 將操作Employee構(gòu)建的對象包裝到一個對象中。使用該方法,您可以檢索由rel創(chuàng)建的。這個方法返回一個必須用方法變成一個的。save()
EmployeeModelAssemblerEntityModelgetRequiredLink()LinkEmployeeModelAssemblerSELFLinkURItoUri
由于我們想要一個比200 OK更詳細的 HTTP 響應(yīng)代碼,我們將使用 Spring MVC 的ResponseEntity包裝器。它有一個方便的靜態(tài)方法created(),我們可以在其中插入資源的 URI。HTTP 201 Created是否具有正確的語義值得商榷,因為我們不一定要“創(chuàng)建”新資源。但它預(yù)裝了一個Location響應(yīng)頭,所以用它運行。
$ curl -v -X PUT localhost:8080/employees/3 -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}'* TCP_NODELAY 設(shè)置* 連接到 localhost (::1) 端口 8080 (#0)> PUT /employees/3 HTTP/1.1> 主機:本地主機:8080> 用戶代理:curl/7.54.0> 接受:*/*> 內(nèi)容類型:應(yīng)用程序/json> 內(nèi)容長度:49>< HTTP/1.1 201< 位置:http://localhost:8080/employees/3< 內(nèi)容類型:application/hal+json;charset=UTF-8< 傳輸編碼:分塊< 日期:格林威治標準時間 2018 年 8 月 10 日星期五 19:52:56{“身份證”:3,"firstName": "Samwise","lastName": "Gamgee",“角色”:“戒指持有者”,"name": "Samwise Gamgee",“_鏈接”:{“自己”: {“href”:“http://localhost:8080/employees/3”},“雇員”: {"href": "http://localhost:8080/employees"}}}
該員工資源現(xiàn)已更新,并且位置 URI 已發(fā)回。最后,適當?shù)馗?DELETE 操作:
處理 DELETE 請求
@DeleteMapping("/employees/{id}")ResponseEntity deleteEmployee(@PathVariable Long id) { repository.deleteById(id); return ResponseEntity.noContent().build();}復(fù)制
這將返回HTTP 204 No Content響應(yīng)。
$ curl -v -X 刪除本地主機:8080/employees/1* TCP_NODELAY 設(shè)置* 連接到 localhost (::1) 端口 8080 (#0)> 刪除 /employees/1 HTTP/1.1> 主機:本地主機:8080> 用戶代理:curl/7.54.0> 接受:*/*>< HTTP/1.1 204< 日期:格林威治標準時間 2018 年 8 月 10 日星期五 21:30:26
對類中的字段進行更改Employee需要與您的數(shù)據(jù)庫團隊協(xié)調(diào),以便他們可以正確地將現(xiàn)有內(nèi)容遷移到新列中。
您現(xiàn)在已準備好進行升級,不會干擾現(xiàn)有客戶端,而新客戶端可以利用這些增強功能!
順便說一句,您是否擔心通過網(wǎng)絡(luò)發(fā)送太多信息?在某些每個字節(jié)都很重要的系統(tǒng)中,API 的發(fā)展可能需要退居二線。但是在你測量之前不要追求這種過早的優(yōu)化。
以上就是今天關(guān)于Spring的一些討論,對你有幫助嗎?如果你有興趣深入了解,歡迎到Spring中國教育管理中心留言交流!
審核編輯:湯梓紅
-
spring
+關(guān)注
關(guān)注
0文章
338瀏覽量
14296 -
REST
+關(guān)注
關(guān)注
0文章
32瀏覽量
9394
發(fā)布評論請先 登錄
相關(guān)推薦
評論