【獨家焦點】使用 Spring 構建 REST 服務

2022-12-27 19:15:27 來源:51CTO博客

REST 已迅速成為在 Web 上構建 Web 服務的事實標準,因為它們易于構建和使用。

關于 REST 如何適應微服務領域,還有更大的討論,但是在本教程中,讓我們只看一下構建 RESTful 服務。


【資料圖】

為什么休息?REST 包含 Web 的準則,包括其架構、優勢和其他一切。這并不奇怪,因為它的作者羅伊·菲爾丁(Roy Fielding)參與了大約十幾個規范,這些規范控制著網絡的運作方式。

有什么好處?Web及其核心協議HTTP提供了一系列功能:

適當的操作(、、、、...)GETPOSTPUTDELETE緩存重定向和轉發安全性(加密和身份驗證)

這些都是構建彈性服務的關鍵因素。但這還不是全部。網絡是由許多微小的規范構建而成的,因此它能夠輕松發展,而不會陷入“標準戰爭”。

開發人員能夠利用實現這些不同規范的第三方工具包,并立即擁有觸手可及的客戶端和服務器技術。

通過在HTTP之上構建,REST API提供了構建的方法:

向后兼容的 API可進化的接口可擴展的服務安全對象服務從無狀態到有狀態服務的一系列

重要的是要認識到,REST,無論多么普遍,本身并不是一個標準,而是一種方法,一種風格,一組對架構的約束,可以幫助你構建Web規模的系統。在本教程中,我們將使用 Spring 產品組合來構建 RESTful 服務,同時利用 REST 的無堆棧功能。

開始

在完成本教程時,我們將使用彈簧啟動.轉到Spring Initializr并將以下依賴項添加到項目中:

蹼太平紳士H2

將名稱更改為“工資單”,然后選擇“生成項目”。A 將下載。解壓縮它。在里面你會發現一個簡單的,基于Maven的項目,包括一個構建文件(注意:你可以使用Gradle。本教程中的示例將基于 Maven。??.zip????pom.xml??

Spring Boot 可以與任何 IDE 一起使用。您可以使用Eclipse,IntelliJ IDEA,Netbeans等。彈簧工具套件是一個開源的、基于 Eclipse 的 IDE 發行版,它提供了 Eclipse 的 Java EE 發行版的超集。它包括使使用 Spring 應用程序更加容易的功能。這絕不是必需的。但是,如果您想為擊鍵提供額外的魅力,請考慮一下。

到目前為止的故事...

讓我們從我們可以構建的最簡單的東西開始。事實上,為了使它盡可能簡單,我們甚至可以省略REST的概念。 (稍后,我們將添加REST來理解其中的區別。

大圖:我們將創建一個簡單的工資單服務來管理公司的員工。我們將員工對象存儲在(H2 內存中)數據庫中,并訪問它們(通過稱為JPA的東西)。然后,我們將用允許通過互聯網訪問的東西(稱為SpringMVC層)來包裝它。

以下代碼在我們的系統中定義了一個員工。

nonrest/src/main/java/payroll/Employee.java

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 name;  private String role;  Employee() {}  Employee(String name, String role) {    this.name = name;    this.role = role;  }  public Long getId() {    return this.id;  }  public String getName() {    return this.name;  }  public String getRole() {    return this.role;  }  public void setId(Long id) {    this.id = id;  }  public void setName(String name) {    this.name = name;  }  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.name, employee.name)        && Objects.equals(this.role, employee.role);  }  @Override  public int hashCode() {    return Objects.hash(this.id, this.name, this.role);  }  @Override  public String toString() {    return "Employee{" + "id=" + this.id + ", name="" + this.name + "\"" + ", role="" + this.role + "\"" + "}";  }}

盡管很小,但這個 Java 類包含很多內容:

??@Entity??是一個 JPA 注釋,用于使此對象準備好存儲在基于 JPA 的數據存儲中。??id??、 和 是我們員工的屬性namerole域對象. 用更多 JPA 注釋標記,以指示它是主鍵,并由 JPA 提供程序自動填充。id當我們需要創建新實例但還沒有 ID 時,會創建自定義構造函數。

有了這個域對象定義,我們現在可以轉向春季數據 JPA處理繁瑣的數據庫交互。

Spring Data JPA 存儲庫是具有支持針對后端數據存儲創建、讀取、更新和刪除記錄的方法的接口。某些存儲庫還支持數據分頁和排序(如果適用)。Spring Data 根據接口中方法命名中的約定合成實現。

除了 JPA 之外,還有多個存儲庫實現。您可以使用Spring Data MongoDB,Spring Data GemFire,Spring Data Cassandra等。在本教程中,我們將堅持使用 JPA。

Spring 使訪問數據變得容易。通過簡單地聲明以下接口,我們將能夠自動??EmployeeRepository??

創建新員工更新現有刪除員工查找員工(一個、全部或按簡單或復雜屬性搜索)

nonrest/src/main/java/payroll/EmployeeRepository.java

package payroll;import org.springframework.data.jpa.repository.JpaRepository;interface EmployeeRepository extends JpaRepository {}

為了獲得所有這些免費功能,我們所要做的就是聲明一個擴展Spring Data JPA的接口,將域類型指定為,將id類型指定為。??JpaRepository????Employee????Long??

春季數據存儲庫解決方案可以避開數據存儲細節,而是使用特定于域的術語解決大多數問題。

信不信由你,這足以啟動一個應用程序!Spring 引導應用程序至少是一個入口點和注釋。這告訴Spring Boot盡可能提供幫助。??public static void main????@SpringBootApplication??

nonrest/src/main/java/payroll/PayrollApplication.java

package payroll;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class PayrollApplication {  public static void main(String... args) {    SpringApplication.run(PayrollApplication.class, args);  }}

??@SpringBootApplication??是一個元注釋,它引入了組件掃描自動配置屬性支持。在本教程中,我們不會深入探討 Spring Boot 的細節,但本質上,它將啟動一個 servlet 容器并提供我們的服務。

盡管如此,沒有數據的應用程序并不是很有趣,所以讓我們預加載它。以下類將由 Spring 自動加載:

nonrest/src/main/java/payroll/LoadDatabase.java

package payroll;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.CommandLineRunner;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationclass LoadDatabase {  private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);  @Bean  CommandLineRunner initDatabase(EmployeeRepository repository) {    return args -> {      log.info("Preloading " + repository.save(new Employee("Bilbo Baggins", "burglar")));      log.info("Preloading " + repository.save(new Employee("Frodo Baggins", "thief")));    };  }}

加載時會發生什么?

一旦加載了應用程序上下文,Spring Boot 將運行所有 bean。CommandLineRunner此運行器將請求您剛剛創建的副本。EmployeeRepository使用它,它將創建兩個實體并存儲它們。

右鍵單擊并運行,這就是你得到的:??PayRollApplication??

顯示數據預加載的控制臺輸出片段

...2018-08-09 11:36:26.169  INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=1, name=Bilbo Baggins, role=burglar)2018-08-09 11:36:26.174  INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=2, name=Frodo Baggins, role=thief)...

這不是整個日志,而只是預加載數據的關鍵位。(事實上,請查看整個控制臺。這是光榮的。

HTTP是平臺

要用 Web 層包裝存儲庫,您必須轉向 Spring MVC。多虧了 Spring Boot,基礎設施中幾乎沒有代碼。相反,我們可以專注于行動:

nonrest/src/main/java/payroll/EmployeeController.java

package payroll;import java.util.List;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RestController;@RestControllerclass EmployeeController {  private final EmployeeRepository repository;  EmployeeController(EmployeeRepository repository) {    this.repository = repository;  }  // Aggregate root  // tag::get-aggregate-root[]  @GetMapping("/employees")  List all() {    return repository.findAll();  }  // end::get-aggregate-root[]  @PostMapping("/employees")  Employee newEmployee(@RequestBody Employee newEmployee) {    return repository.save(newEmployee);  }  // Single item    @GetMapping("/employees/{id}")  Employee one(@PathVariable Long id) {        return repository.findById(id)      .orElseThrow(() -> new EmployeeNotFoundException(id));  }  @PutMapping("/employees/{id}")  Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) {        return 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);      });  }  @DeleteMapping("/employees/{id}")  void deleteEmployee(@PathVariable Long id) {    repository.deleteById(id);  }}
??@RestController??指示每個方法返回的數據將直接寫入響應正文,而不是呈現模板。構造函數將 A 注入控制器。EmployeeRepository我們為每個操作都有路由(、 和 , 對應于 HTTP 、、 和調用)。(注意:閱讀每種方法并了解它們的作用很有用。@GetMapping@PostMapping@PutMapping@DeleteMappingGETPOSTPUTDELETE??EmployeeNotFoundException??是用于指示何時查找但未找到員工的例外。

nonrest/src/main/java/payroll/EmployeeNotFoundException.java

package payroll;class EmployeeNotFoundException extends RuntimeException {  EmployeeNotFoundException(Long id) {    super("Could not find employee " + id);  }}

當拋出 an 時,Spring MVC 配置的這個額外花絮用于渲染HTTP 404:??EmployeeNotFoundException??

nonrest/src/main/java/payroll/EmployeeNotFoundAdvice.java

package payroll;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.ResponseStatus;@ControllerAdviceclass EmployeeNotFoundAdvice {  @ResponseBody  @ExceptionHandler(EmployeeNotFoundException.class)  @ResponseStatus(HttpStatus.NOT_FOUND)  String employeeNotFoundHandler(EmployeeNotFoundException ex) {    return ex.getMessage();  }}
??@ResponseBody??表示此建議直接呈現到響應正文中。??@ExceptionHandler??將建議配置為僅在引發 an 時響應。EmployeeNotFoundException??@ResponseStatus??說發出一個,即一個HTTP 404HttpStatus.NOT_FOUND建議的正文生成內容。在這種情況下,它會給出異常的消息。

若要啟動應用程序,請右鍵單擊 ,然后選擇“從 IDE 中運行”,或者:??public static void main????PayRollApplication??

Spring Initializr 使用 maven 包裝器,所以鍵入以下內容:

$ ./mvnw clean spring-boot:run

或者使用您安裝的 maven 版本鍵入以下內容:

$ mvn clean spring-boot:run

當應用程序啟動時,我們可以立即詢問它。

$ curl -v localhost:8080/employees

這將產生:

*   Trying ::1...* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> GET /employees HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 200< Content-Type: application/json;charset=UTF-8< Transfer-Encoding: chunked< Date: Thu, 09 Aug 2018 17:58:00 GMT<* Connection #0 to host localhost left intact[{"id":1,"name":"Bilbo Baggins","role":"burglar"},{"id":2,"name":"Frodo Baggins","role":"thief"}]

在這里,您可以看到壓縮格式的預加載數據。

如果您嘗試查詢不存在的用戶...

$ curl -v localhost:8080/employees/99

你得到...

*   Trying ::1...* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> GET /employees/99 HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 404< Content-Type: text/plain;charset=UTF-8< Content-Length: 26< Date: Thu, 09 Aug 2018 18:00:56 GMT<* Connection #0 to host localhost left intactCould not find employee 99

此消息很好地顯示了帶有自定義消息“找不到員工 99”的HTTP 404錯誤。

不難顯示當前編碼的交互...

如果您使用 Windows 命令提示符發出 cURL 命令,則以下命令可能無法正常工作。您必須選擇支持單引號參數的終端,或者使用雙引號,然后轉義 JSON 中的終端。

要創建新記錄,我們在終端中使用以下命令 — 開頭表示它后面是終端命令:??Employee????$??

$ curl -X POST localhost:8080/employees -H "Content-type:application/json" -d "{"name": "Samwise Gamgee", "role": "gardener"}"

然后它存儲新創建的員工并將其發送回給我們:

{"id":3,"name":"Samwise Gamgee","role":"gardener"}

您可以更新用戶。讓我們改變他的角色。

$ curl -X PUT localhost:8080/employees/3 -H "Content-type:application/json" -d "{"name": "Samwise Gamgee", "role": "ring bearer"}"

我們可以看到輸出中反映的變化。

{"id":3,"name":"Samwise Gamgee","role":"ring bearer"}

構建服務的方式可能會產生重大影響。在這種情況下,我們說更新,但替換是一個更好的描述。例如,如果未提供名稱,則會將其注銷。

最后,您可以像這樣刪除用戶:

$ curl -X DELETE localhost:8080/employees/3# Now if we look again, it"s gone$ curl localhost:8080/employees/3Could not find employee 3

這一切都很好,但是我們有RESTful服務嗎?(如果你沒有抓住提示,答案是否定的。

缺少什么?

是什么讓事物變得令人不安?

到目前為止,您有一個基于 Web 的服務來處理涉及員工數據的核心操作。但這還不足以讓事情變得“RESTful”。

漂亮的網址,比如不是REST。/employees/3僅僅使用 、 等不是 REST。GETPOST布置所有 CRUD 操作不是 REST。

事實上,到目前為止我們構建的內容更好地描述為RPC遠程過程調用)。這是因為沒有辦法知道如何與此服務交互。如果您今天發布了此內容,您還必須編寫文檔或在某個包含所有詳細信息的開發人員門戶。

Roy Fielding的這句話可能進一步為RESTRPC之間的區別提供了線索:

我對將任何基于 HTTP 的接口稱為 REST API 的人數感到沮喪。今天的例子是SocialSite REST API。那就是RPC。它尖叫著RPC。顯示的耦合太多,應該給它一個X等級。

需要做些什么來使 REST 架構風格明確超文本是一種約束的概念?換句話說,如果應用程序狀態引擎(以及 API)不是由超文本驅動的,那么它就不能是 RESTful 的,也不能是 REST API。時期。是否有一些損壞的手冊需要修復?

— 羅伊·菲爾丁https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

在我們的表示中不包含超媒體的副作用是,客戶端必須對 URI 進行硬編碼才能導航 API。這導致了在網絡上電子商務興起之前相同的脆性。這是一個信號,表明我們的 JSON 輸出需要一點幫助。

正在推出春天的哈特亞斯,一個旨在幫助您編寫超媒體驅動輸出的 Spring 項目。要將服務升級到 RESTful,請將以下內容添加到您的構建中:

將春季 HATEOAS 添加到??dependencies????pom.xml??

  org.springframework.boot  spring-boot-starter-hateoas

這個小庫將為我們提供定義 RESTful 服務的構造,然后將其呈現為可接受的格式以供客戶端使用。

任何 RESTful 服務的關鍵要素是添加鏈接到相關操作。要使您的控制器更加 RESTful,請添加如下鏈接:

獲取單個項資源

@GetMapping("/employees/{id}")EntityModel one(@PathVariable Long id) {  Employee employee = repository.findById(id) //      .orElseThrow(() -> new EmployeeNotFoundException(id));  return EntityModel.of(employee, //      linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(),      linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));}

本教程基于 Spring MVC,并使用靜態輔助程序方法來構建這些鏈接。如果您在項目中使用 Spring WebFlux,則必須改用 .??WebMvcLinkBuilder????WebFluxLinkBuilder??

這與我們之前的情況非常相似,但有一些事情發生了變化:

該方法的返回類型已從 更改為 。 是Spring HATEOAS的一個通用容器,它不僅包含數據,還包含鏈接集合。EmployeeEntityModelEntityModel??linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()??要求Spring HATEOAS構建指向 的方法的鏈接,并將其標記為EmployeeControllerone()自我鏈接。??linkTo(methodOn(EmployeeController.class).all()).withRel("employees")??要求Spring HATEOAS建立與聚合根的鏈接,并將其稱為“員工”。all()

我們所說的“建立鏈接”是什么意思?Spring HATEOAS 的核心類型之一是 。它包括一個URI和一個rel(關系)。鏈接是賦予網絡權力的東西。在萬維網出現之前,其他文檔系統會呈現信息或鏈接,但正是文檔與這種關系元數據的鏈接將網絡拼接在一起。??Link??

Roy Fielding 鼓勵使用與 Web 成功的相同技術構建 API,鏈接就是其中之一。

如果重新啟動應用程序并查詢Bilbo的員工記錄,則得到的響應將與之前略有不同:

冰壺更漂亮

當您的 curl 輸出變得更加復雜時,它可能會變得難以閱讀。使用這個或其他提示要美化 curl 返回的 JSON,請執行以下操作:

# The indicated part pipes the output to json_pp and asks it to make your JSON pretty. (Or use whatever tool you like!)#                                  v------------------vcurl -v localhost:8080/employees/1 | json_pp

REST單個員工的表示形式

{  "id": 1,  "name": "Bilbo Baggins",  "role": "burglar",  "_links": {    "self": {      "href": "http://localhost:8080/employees/1"    },    "employees": {      "href": "http://localhost:8080/employees"    }  }}

此解壓縮輸出不僅顯示您之前看到的數據元素(和 ),還顯示包含兩個 URI 的條目。整個文檔的格式為??id????name????role????_links??哈爾.

HAL 是一種輕量級媒體類型這不僅允許對數據進行編碼,還允許對超媒體控件進行編碼,提醒消費者他們可以導航到 API 的其他部分。在這種情況下,有一個“self”鏈接(有點像代碼中的語句)以及一個返回??this???聚合根目錄?.

為了使聚合根也更 RESTful,您需要包含頂級鏈接,同時還要包含任何 RESTful 組件。

所以我們把這個

獲取聚合根目錄

@GetMapping("/employees")List all() {  return repository.findAll();}

進入這個

獲取聚合根資源

@GetMapping("/employees")CollectionModel> all() {  List> employees = repository.findAll().stream()      .map(employee -> EntityModel.of(employee,          linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),          linkTo(methodOn(EmployeeController.class).all()).withRel("employees")))      .collect(Collectors.toList());  return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel());}

哇!那個方法,曾經只是,都長大了!不用擔心。讓我們解開它。??repository.findAll()??

??CollectionModel<>??是另一個春天的HATEOAS容器;它旨在封裝資源集合,而不是像以前那樣封裝單個資源實體。,也允許您包含鏈接。??EntityModel<>????CollectionModel<>??

不要讓第一句話溜走。“封裝集合”是什么意思?員工集合?

差一點。

由于我們談論的是 REST,它應該封裝員工資源的集合。

這就是為什么你獲取所有員工,然后將它們轉換為對象列表。(感謝Java 8 Streams!??EntityModel??

如果重新啟動應用程序并獲取聚合根,則可以看到它現在的外觀。

REST 員工資源集合的表示形式

{  "_embedded": {    "employeeList": [      {        "id": 1,        "name": "Bilbo Baggins",        "role": "burglar",        "_links": {          "self": {            "href": "http://localhost:8080/employees/1"          },          "employees": {            "href": "http://localhost:8080/employees"          }        }      },      {        "id": 2,        "name": "Frodo Baggins",        "role": "thief",        "_links": {          "self": {            "href": "http://localhost:8080/employees/2"          },          "employees": {            "href": "http://localhost:8080/employees"          }        }      }    ]  },  "_links": {    "self": {      "href": "http://localhost:8080/employees"    }  }}

對于提供員工資源集合的聚合根,有一個頂級的“自我”鏈接。“集合”列在“_embedded”部分下方;這就是 HAL 表示集合的方式。

集合的每個成員都有他們的信息以及相關鏈接。

添加所有這些鏈接有什么意義?它使 REST 服務隨著時間的推移而發展成為可能。可以保留現有鏈接,同時將來可以添加新鏈接。新客戶端可以利用新鏈接,而舊客戶端可以在舊鏈接上維持自身。如果服務被重新定位和移動,這將特別有用。只要保持鏈接結構,客戶端仍然可以找到事物并與之交互。

簡化鏈接創建

在前面的代碼中,您是否注意到單個員工鏈接創建的重復性?用于向員工提供單個鏈接以及創建指向聚合根的“員工”鏈接的代碼顯示兩次。如果這引起了您的關注,那很好!有一個解決方案。

簡而言之,您需要定義一個將對象轉換為對象的函數。雖然您可以輕松地自己編寫此方法,但實現Spring HATEOAS的接口是有好處的 - 它將為您完成工作。??Employee????EntityModel????RepresentationModelAssembler??

evolution/src/main/java/payroll/EmployeeModelAssembler.java

package payroll;import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;import org.springframework.hateoas.EntityModel;import org.springframework.hateoas.server.RepresentationModelAssembler;import org.springframework.stereotype.Component;@Componentclass EmployeeModelAssembler implements RepresentationModelAssembler> {  @Override  public EntityModel toModel(Employee employee) {    return EntityModel.of(employee, //        linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),        linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));  }}

這個簡單的界面有一個方法:。它基于將非模型對象 () 轉換為基于模型的對象 ()。??toModel()????Employee????EntityModel??

您之前在控制器中看到的所有代碼都可以移動到此類中。通過應用Spring Framework的注釋,匯編程序將在應用程序啟動時自動創建。??@Component??

Spring HATEOAS 所有模型的抽象基類是 。但為了簡單起見,我建議使用作為您的機制,輕松地將所有 POJO 包裝為模型。??RepresentationModel????EntityModel??

要利用此匯編程序,您只需通過在構造函數中注入匯編器來更改 。??EmployeeController??

將員工模型匯編器注入控制器

@RestControllerclass EmployeeController {  private final EmployeeRepository repository;  private final EmployeeModelAssembler assembler;  EmployeeController(EmployeeRepository repository, EmployeeModelAssembler assembler) {    this.repository = repository;    this.assembler = assembler;  }  ...}

從這里,您可以在單項員工方法中使用該匯編程序:

使用匯編程序獲取單個項目資源

@GetMapping("/employees/{id}")EntityModel one(@PathVariable Long id) {  Employee employee = repository.findById(id) //      .orElseThrow(() -> new EmployeeNotFoundException(id));  return assembler.toModel(employee);}

此代碼幾乎相同,只是不是在此處創建實例,而是將其委托給匯編程序。也許這看起來并不多。??EntityModel??

在聚合根控制器方法中應用相同的東西更令人印象深刻:

使用匯編程序獲取聚合根資源

@GetMapping("/employees")CollectionModel> all() {  List> employees = repository.findAll().stream() //      .map(assembler::toModel) //      .collect(Collectors.toList());  return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel());}

同樣,代碼幾乎相同,但是您可以將所有創建邏輯替換為 .借助 Java 8 方法引用,插入它并簡化控制器非常容易。??EntityModel????map(assembler::toModel)??

Spring HATEOAS的一個關鍵設計目標是使做正確的事情?變得更容易。在此方案中:將超媒體添加到服務中,而無需對內容進行硬編碼。

在這個階段,你已經創建了一個Spring MVC REST控制器,它實際上產生了超媒體驅動的內容!不使用 HAL 的客戶端可以在使用純數據時忽略額外的位。使用 HAL 的客戶端可以導航您的授權 API。

但這并不是使用 Spring 構建真正的 RESTful 服務所需的唯一內容。

不斷發展的 REST API

通過一個額外的庫和幾行額外的代碼,您已經將超媒體添加到了應用程序中。但這并不是使您的服務成為 RESTful 所需的唯一內容。REST的一個重要方面是它既不是技術堆棧也不是單一標準。

REST 是體系結構約束的集合,采用這些約束后,應用程序將更具彈性。彈性的一個關鍵因素是,當您升級服務時,您的客戶不會遭受停機的影響。

在“過去”的日子里,升級因破壞客戶而臭名昭著。換句話說,升級到服務器需要更新客戶端。在當今時代,升級花費數小時甚至數分鐘的停機時間可能會造成數百萬美元的收入損失。

一些公司要求您向管理層提供計劃,以盡量減少停機時間。過去,您可以在負載最小的星期日凌晨 2:00 進行升級。但在當今與其他時區的國際客戶的基于互聯網的電子商務中,這種策略并不那么有效。

基于 SOAP 的服務和基于 CORBA 的服務非常脆弱。很難推出同時支持新舊客戶端的服務器。使用基于 REST 的實踐,這要容易得多。特別是使用彈簧堆棧。

支持對 API 的更改

想象一下這個設計問題:你已經推出了一個具有基于此記錄的系統。該系統是一個重大打擊。您已經將系統出售給無數企業。突然間,需要將員工的名字拆分并出現。??Employee????firstName????lastName??

呃哦。沒想到。

在打開類并將單個字段替換為 和 之前,請停下來思考一下。這會破壞任何客戶嗎?升級它們需要多長時間。您甚至控制訪問您的服務的所有客戶端嗎???Employee????name????firstName????lastName??

停機時間 = 虧損。管理層準備好了嗎?

有一個古老的策略比 REST 早幾年。

切勿刪除數據庫中的列。

— 未知

您始終可以向數據庫表添加列(字段)。但不要拿走一個。RESTful 服務中的原理是相同的。

向 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????name??

您不僅應該以“舊方式”和“新方式”顯示此信息,還應該以兩種方式處理傳入的數據。

如何?簡單。喜歡這個:

處理“舊”和“新”客戶的員工記錄

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??

字段已替換為 和 。namefirstNamelastName定義了舊屬性的“虛擬”獲取器。它使用 and 字段來生成值。namegetName()firstNamelastName還定義了舊屬性的“虛擬”資源庫。它解析傳入的字符串并將其存儲到適當的字段中。namesetName()

當然,并非對 API 的每次更改都像拆分字符串或合并兩個字符串那樣簡單。但對于大多數場景,想出一組轉換肯定不是不可能,對吧?

不要忘記更改預加載數據庫的方式(在 中)以使用此新構造函數。??LoadDatabase??

log.info("Preloading " + repository.save(new Employee("Bilbo", "Baggins", "burglar")));log.info("Preloading " + repository.save(new Employee("Frodo", "Baggins", "thief")));

正確的回應

朝著正確方向邁出的另一步是確保每個 REST 方法返回正確的響應。更新 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);}
新對象將像以前一樣保存。但是生成的對象是使用 .EmployeeEmployeeModelAssemblerSpring MVC 用于創建HTTP 201 Created狀態消息。這種類型的響應通常包括位置響應標頭,我們使用從模型的自相關鏈接派生的 URI。ResponseEntity此外,返回已保存對象的基于模型的版本。

進行這些調整后,您可以使用同一終端節點創建新的員工資源,并使用舊字段:??name??

$ curl -v -X POST localhost:8080/employees -H "Content-Type:application/json" -d "{"name": "Samwise Gamgee", "role": "gardener"}"

輸出如下所示:

> POST /employees HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*> Content-Type:application/json> Content-Length: 46>< Location: http://localhost:8080/employees/3< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Fri, 10 Aug 2018 19:44:43 GMT<{  "id": 3,  "firstName": "Samwise",  "lastName": "Gamgee",  "role": "gardener",  "name": "Samwise Gamgee",  "_links": {    "self": {      "href": "http://localhost:8080/employees/3"    },    "employees": {      "href": "http://localhost:8080/employees"    }  }}

這不僅在 HAL 中呈現了生成的對象(以及 /),而且還用 .?超媒體驅動的客戶端可以選擇“瀏覽”這個新資源并繼續與之交互。??name????firstName????lastName????http://localhost:8080/employees/3??

PUT 控制器方法需要類似的調整:

為不同的客戶端處理 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);}

然后,使用 到 到對象中包裝從操作生成的對象。使用該方法,您可以檢索由 創建的 與 rel。此方法返回必須轉換為 with 方法的 a。??Employee????save()????EmployeeModelAssembler????EntityModel????getRequiredLink()????Link????EmployeeModelAssembler????SELF????Link????URI????toUri??

由于我們想要比200 OK更詳細的 HTTP 響應代碼,我們將使用 Spring MVC 的包裝器。它有一個方便的靜態方法,我們可以在其中插入資源的 URI。HTTP 201 Created是否具有正確的語義是值得商榷的,因為我們不一定在“創建”新資源。但它預裝了一個位置響應標頭,因此請使用它運行。??ResponseEntity????created()??

$ curl -v -X PUT localhost:8080/employees/3 -H "Content-Type:application/json" -d "{"name": "Samwise Gamgee", "role": "ring bearer"}"* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> PUT /employees/3 HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*> Content-Type:application/json> Content-Length: 49>< HTTP/1.1 201< Location: http://localhost:8080/employees/3< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Fri, 10 Aug 2018 19:52:56 GMT{  "id": 3,  "firstName": "Samwise",  "lastName": "Gamgee",  "role": "ring bearer",  "name": "Samwise Gamgee",  "_links": {    "self": {      "href": "http://localhost:8080/employees/3"    },    "employees": {      "href": "http://localhost:8080/employees"    }  }}

該員工資源現已更新,位置 URI 已發回。最后,適當地更新 DELETE 操作:

處理刪除請求

@DeleteMapping("/employees/{id}")ResponseEntity deleteEmployee(@PathVariable Long id) {  repository.deleteById(id);  return ResponseEntity.noContent().build();}

這將返回HTTP 204 無內容響應。

$ curl -v -X DELETE localhost:8080/employees/1* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> DELETE /employees/1 HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 204< Date: Fri, 10 Aug 2018 21:30:26 GMT

對類中的字段進行更改需要與數據庫團隊協調,以便他們可以正確地將現有內容遷移到新列中。??Employee??

您現在已準備好進行升級,升級不會干擾現有客戶端,而較新的客戶端可以利用增強功能!

順便問一下,您是否擔心通過網絡發送太多信息?在某些每個字節都很重要的系統中,API 的演進可能需要退居二線。但是,在測量之前,不要追求這種過早的優化。

在 REST API 中構建鏈接

到目前為止,您已經構建了一個具有基本鏈接的可進化 API。為了發展您的 API 并更好地為客戶服務,您需要接受超媒體作為應用程序狀態引擎的概念

那是什么意思?在本節中,您將詳細探索它。

業務邏輯不可避免地會構建涉及流程的規則。這種系統的風險是我們經常將這樣的服務器端邏輯帶入客戶端并建立強大的耦合。REST是關于打破這種連接并最小化這種耦合。

為了展示如何在不觸發客戶端中斷性更改的情況下處理狀態更改,請假設添加一個履行訂單的系統。

第一步,定義一條記錄:??Order??

links/src/main/java/payroll/Order.java

package payroll;import java.util.Objects;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.Table;@Entity@Table(name = "CUSTOMER_ORDER")class Order {  private @Id @GeneratedValue Long id;  private String description;  private Status status;  Order() {}  Order(String description, Status status) {    this.description = description;    this.status = status;  }  public Long getId() {    return this.id;  }  public String getDescription() {    return this.description;  }  public Status getStatus() {    return this.status;  }  public void setId(Long id) {    this.id = id;  }  public void setDescription(String description) {    this.description = description;  }  public void setStatus(Status status) {    this.status = status;  }  @Override  public boolean equals(Object o) {    if (this == o)      return true;    if (!(o instanceof Order))      return false;    Order order = (Order) o;    return Objects.equals(this.id, order.id) && Objects.equals(this.description, order.description)        && this.status == order.status;  }  @Override  public int hashCode() {    return Objects.hash(this.id, this.description, this.status);  }  @Override  public String toString() {    return "Order{" + "id=" + this.id + ", description="" + this.description + "\"" + ", status=" + this.status + "}";  }}
The class requires a JPAannotation changing the table’s name tobecauseis not a valid name for table.@TableCUSTOMER_ORDERORDER它包括一個字段和一個字段。descriptionstatus

從客戶提交訂單到訂單被履行或取消,訂單必須經歷一系列特定的狀態轉換。這可以捕獲為Java:??enum??

links/src/main/java/payroll/Status.java

package payroll;enum Status {  IN_PROGRESS, //  COMPLETED, //  CANCELLED}

這捕獲了一個罐子占據的各種狀態。對于本教程,讓我們保持簡單。??enum????Order??

為了支持與數據庫中的訂單交互,您必須定義相應的 Spring 數據存儲庫:

Spring Data JPA 的基本接口??JpaRepository??

interface OrderRepository extends JpaRepository {}

完成此操作后,您現在可以定義一個基本的:??OrderController??

links/src/main/java/payroll/OrderController.java

@RestControllerclass OrderController {  private final OrderRepository orderRepository;  private final OrderModelAssembler assembler;  OrderController(OrderRepository orderRepository, OrderModelAssembler assembler) {    this.orderRepository = orderRepository;    this.assembler = assembler;  }  @GetMapping("/orders")  CollectionModel> all() {    List> orders = orderRepository.findAll().stream() //        .map(assembler::toModel) //        .collect(Collectors.toList());    return CollectionModel.of(orders, //        linkTo(methodOn(OrderController.class).all()).withSelfRel());  }  @GetMapping("/orders/{id}")  EntityModel one(@PathVariable Long id) {    Order order = orderRepository.findById(id) //        .orElseThrow(() -> new OrderNotFoundException(id));    return assembler.toModel(order);  }  @PostMapping("/orders")  ResponseEntity> newOrder(@RequestBody Order order) {    order.setStatus(Status.IN_PROGRESS);    Order newOrder = orderRepository.save(order);    return ResponseEntity //        .created(linkTo(methodOn(OrderController.class).one(newOrder.getId())).toUri()) //        .body(assembler.toModel(newOrder));  }}
它包含與您到目前為止構建的控制器相同的 REST 控制器設置。它注入了一個以及一個(尚未構建)。OrderRepositoryOrderModelAssembler前兩個Spring MVC路由處理聚合根以及單個項目資源請求。Order第三個Spring MVC路由通過在狀態中啟動新訂單來處理創建新訂單。IN_PROGRESS所有控制器方法都返回Spring HATEOAS的一個子類,以正確呈現超媒體(或圍繞此類類型的包裝器)。RepresentationModel

在構建 之前,讓我們討論一下需要發生什么。您正在對 、 和 之間的狀態流進行建模。向客戶端提供此類數據時,很自然的事情是讓客戶端根據此有效負載決定它可以做什么。??OrderModelAssembler????Status.IN_PROGRESS????Status.COMPLETED????Status.CANCELLED??

但那是錯誤的。

在此流中引入新狀態時會發生什么情況?UI 上各種按鈕的位置可能是錯誤的。

如果您更改了每個州的名稱,也許是在編碼國際支持并顯示每個州特定于區域設置的文本時,該怎么辦?這很可能會破壞所有客戶端。

輸入HATEOAS超媒體作為應用程序狀態的引擎。客戶端不要解析有效負載,而是為它們提供鏈接以發出有效操作的信號。將基于狀態的操作與數據有效負載分離。換句話說,當CANCELCOMPLETE是有效操作時,請動態地將它們添加到鏈接列表中。客戶端只需要在鏈接存在時向用戶顯示相應的按鈕。

這使客戶端不必知道此類操作何時有效,從而降低了服務器及其客戶端在狀態轉換邏輯上不同步的風險。

在已經接受了Spring HATEOAS組件的概念之后,將這樣的邏輯放入其中將是捕獲此業務規則的理想位置:??RepresentationModelAssembler????OrderModelAssembler??

links/src/main/java/payroll/OrderModelAssembler.java

package payroll;import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;import org.springframework.hateoas.EntityModel;import org.springframework.hateoas.server.RepresentationModelAssembler;import org.springframework.stereotype.Component;@Componentclass OrderModelAssembler implements RepresentationModelAssembler> {  @Override  public EntityModel toModel(Order order) {    // Unconditional links to single-item resource and aggregate root    EntityModel orderModel = EntityModel.of(order,        linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(),        linkTo(methodOn(OrderController.class).all()).withRel("orders"));    // Conditional links based on state of the order    if (order.getStatus() == Status.IN_PROGRESS) {      orderModel.add(linkTo(methodOn(OrderController.class).cancel(order.getId())).withRel("cancel"));      orderModel.add(linkTo(methodOn(OrderController.class).complete(order.getId())).withRel("complete"));    }    return orderModel;  }}

此資源組裝器始終包含指向單項資源的鏈接以及返回到聚合根的鏈接。但它還包括兩個條件鏈接 以及(尚未定義)。僅當訂單狀態為 .??OrderController.cancel(id)????OrderController.complete(id)????Status.IN_PROGRESS??

如果客戶可以采用HAL和讀取鏈接的能力,而不是簡單地讀取普通舊JSON的數據,他們就可以交換對訂單系統領域知識的需求。這自然減少了客戶端和服務器之間的耦合。它打開了調整訂單履行流程的大門,而不會在此過程中破壞客戶。

要完善訂單履行,請將以下內容添加到操作中:??OrderController????cancel??

在訂單控制器中創建“取消”操作

@DeleteMapping("/orders/{id}/cancel")ResponseEntity cancel(@PathVariable Long id) {  Order order = orderRepository.findById(id) //      .orElseThrow(() -> new OrderNotFoundException(id));  if (order.getStatus() == Status.IN_PROGRESS) {    order.setStatus(Status.CANCELLED);    return ResponseEntity.ok(assembler.toModel(orderRepository.save(order)));  }  return ResponseEntity //      .status(HttpStatus.METHOD_NOT_ALLOWED) //      .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) //      .body(Problem.create() //          .withTitle("Method not allowed") //          .withDetail("You can"t cancel an order that is in the " + order.getStatus() + " status"));}

它會在允許取消之前檢查狀態。如果它不是有效狀態,則返回??Order????RFC-7807????Problem??,一個支持超媒體的錯誤容器。如果轉換確實有效,則會將 轉換為 。??Order????CANCELLED??

并將其添加到 以及 以及 完成訂單:??OrderController??

在訂單控制器中創建“完成”操作

@PutMapping("/orders/{id}/complete")ResponseEntity complete(@PathVariable Long id) {  Order order = orderRepository.findById(id) //      .orElseThrow(() -> new OrderNotFoundException(id));  if (order.getStatus() == Status.IN_PROGRESS) {    order.setStatus(Status.COMPLETED);    return ResponseEntity.ok(assembler.toModel(orderRepository.save(order)));  }  return ResponseEntity //      .status(HttpStatus.METHOD_NOT_ALLOWED) //      .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) //      .body(Problem.create() //          .withTitle("Method not allowed") //          .withDetail("You can"t complete an order that is in the " + order.getStatus() + " status"));}

這將實現類似的邏輯,以防止狀態完成,除非處于正確的狀態。??Order??

讓我們更新以預加載一些 s 以及之前加載的 s。??LoadDatabase????Order????Employee??

更新數據庫預加載程序

package payroll;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.CommandLineRunner;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationclass LoadDatabase {  private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);  @Bean  CommandLineRunner initDatabase(EmployeeRepository employeeRepository, OrderRepository orderRepository) {    return args -> {      employeeRepository.save(new Employee("Bilbo", "Baggins", "burglar"));      employeeRepository.save(new Employee("Frodo", "Baggins", "thief"));      employeeRepository.findAll().forEach(employee -> log.info("Preloaded " + employee));            orderRepository.save(new Order("MacBook Pro", Status.COMPLETED));      orderRepository.save(new Order("iPhone", Status.IN_PROGRESS));      orderRepository.findAll().forEach(order -> {        log.info("Preloaded " + order);      });          };  }}

現在您可以測試一下了!

要使用新鑄造的訂單服務,只需執行以下幾項操作:

$ curl -v http://localhost:8080/orders{  "_embedded": {    "orderList": [      {        "id": 3,        "description": "MacBook Pro",        "status": "COMPLETED",        "_links": {          "self": {            "href": "http://localhost:8080/orders/3"          },          "orders": {            "href": "http://localhost:8080/orders"          }        }      },      {        "id": 4,        "description": "iPhone",        "status": "IN_PROGRESS",        "_links": {          "self": {            "href": "http://localhost:8080/orders/4"          },          "orders": {            "href": "http://localhost:8080/orders"          },          "cancel": {            "href": "http://localhost:8080/orders/4/cancel"          },          "complete": {            "href": "http://localhost:8080/orders/4/complete"          }        }      }    ]  },  "_links": {    "self": {      "href": "http://localhost:8080/orders"    }  }}

此 HAL 文檔會根據每個訂單的當前狀態立即顯示其不同鏈接。

第一個訂單,已完成只有導航鏈接。不顯示狀態轉換鏈接。第二個訂單,IN_PROGRESS另外具有取消鏈接以及完整鏈接。

嘗試取消訂單:

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel> DELETE /orders/4/cancel HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 200< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Mon, 27 Aug 2018 15:02:10 GMT<{  "id": 4,  "description": "iPhone",  "status": "CANCELLED",  "_links": {    "self": {      "href": "http://localhost:8080/orders/4"    },    "orders": {      "href": "http://localhost:8080/orders"    }  }}

此響應顯示HTTP 200狀態代碼,指示它已成功。響應 HAL 文檔顯示其新狀態的順序 ()。改變狀態的鏈接消失了。??CANCELLED??

如果您再次嘗試相同的操作...

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> DELETE /orders/4/cancel HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 405< Content-Type: application/problem+json< Transfer-Encoding: chunked< Date: Mon, 27 Aug 2018 15:03:24 GMT<{  "title": "Method not allowed",  "detail": "You can"t cancel an order that is in the CANCELLED status"}

...您會看到HTTP 405 方法不允許響應。刪除已成為無效操作。響應對象明確指示不允許“取消”已處于“已取消”狀態的訂單。??Problem??

此外,嘗試完成相同的訂單也會失敗:

$ curl -v -X PUT localhost:8080/orders/4/complete* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> PUT /orders/4/complete HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 405< Content-Type: application/problem+json< Transfer-Encoding: chunked< Date: Mon, 27 Aug 2018 15:05:40 GMT<{  "title": "Method not allowed",  "detail": "You can"t complete an order that is in the CANCELLED status"}

完成所有這些操作后,您的訂單履行服務能夠有條件地顯示可用的操作。它還可以防止無效操作。

通過利用超媒體和鏈接的協議,客戶端可以構建得更堅固,并且不太可能僅僅因為數據更改而中斷。Spring HATEOAS 簡化了構建您為客戶提供服務所需的超媒體。

總結

在本教程中,您介紹了構建 REST API 的各種策略。事實證明,REST不僅僅是漂亮的URI和返回JSON而不是XML。

相反,以下策略有助于降低服務中斷您可能控制也可能無法控制的現有客戶端的可能性:

不要刪除舊字段。相反,支持他們。使用基于 rel 的鏈接,以便客戶端不必對 URI 進行硬編碼。盡可能長時間地保留舊鏈接。即使必須更改 URI,也要保留 rel,以便較舊的客戶端具有訪問新功能的路徑。使用鏈接(而不是有效負載數據)在各種狀態驅動操作可用時指示客戶端。

為每種資源類型構建實現并在所有控制器中使用這些組件似乎有點費力。但是這種額外的服務器端設置(由于Spring HATEOAS而變得容易)可以確保您控制的客戶端(更重要的是,您不控制的客戶端)可以在您發展API時輕松升級。??RepresentationModelAssembler??

我們關于如何使用 Spring 構建 RESTful 服務的教程到此結束。本教程的每個部分都作為單個 github 存儲庫中的單獨子項目管理:

nonrest— 簡單的 Spring MVC 應用程序,沒有超媒體rest— Spring MVC + Spring HATEOAS 應用程序,其中包含每個資源的 HAL 表示演進— REST 應用,其中字段已演化,但保留舊數據以實現向后兼容性鏈接— REST 應用,其中條件鏈接用于向客戶端發出有效狀態更改的信號

標簽: 應用程序 匯編程序 數據存儲

上一篇:環球快消息!Spring安全和角度(二)
下一篇:Python之多任務編程進程和線程的對比