<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 使用Spring構建REST服務 REST易于構建和使用,因此已迅速成為在Web上構建Web服務的實際標準。 關于REST如何適用于微服務世界,還有很多討論,但是-在本教程中-讓我們來看構建RESTful服務。 為什么要REST? REST包含Web的戒律,包括其體系結構,優勢和其他所有內容。 鑒于其作者Roy Fielding參與了十二個規范網絡操作的規范,這不足為奇。 有什么好處? Web及其核心協議HTTP提供了一系列功能: * 適當的動作( `GET`, `POST`, `PUT`, `DELETE`, …?) * 快取 * 重定向和轉發 * 安全性(加密和身份驗證) 這些都是構建彈性服務的關鍵因素。 但這還不是全部。 網絡是建立在許多微小的規格之上的,因此它能夠輕松發展,而不會陷入“標準之戰”。 開發人員可以利用實現這些不同規格的第三方工具包,立即擁有客戶端和服務器技術。 通過在HTTP之上進行構建,REST API提供了以下構建方法: * 向后兼容的API * 可進化的API * 可擴展的服務 * 安全的服務 * 無狀態到有狀態服務的范圍 重要的是要認識到,REST 無處不在,而是一個標準, *本身* 而是 一種方法,一種樣式,一組 *約束* 架構上的 ,可以幫助您構建Web規模的系統。 在本教程中,我們將使用Spring產品組合來構建RESTful服務,同時利用REST的無堆棧功能。 ## 入門 在學習本教程時,我們將使用 [Spring Boot](https://spring.io/projects/spring-boot) 。 轉到 [Spring Initializr](https://start.spring.io/) 并將以下依賴項添加到項目中: * 網頁 * JPA * H2 將名稱更改為“工資單”,然后選擇“生成項目”。 一個 `.zip`將下載。 解壓縮。 在內部,您會發現一個簡單的基于Maven的項目,其中包括 `pom.xml`構建文件(注意:您 *可以* 使用Gradle。本教程中的示例將基于Maven。) Spring Boot可以與任何IDE一起使用。 您可以使用Eclipse,IntelliJ IDEA,Netbeans等。Spring [Tool Suite](https://spring.io/tools/) 是基于Eclipse的開源IDE發行版,它提供Eclipse的Java EE發行版的超集。 它包含的功能使使用Spring應用程序的工作變得更加輕松。 絕不是必需的。 但是,如果您想 額外的 考慮一下 **魅力** 為按鍵提供 ,請 。 這是一個演示如何開始使用STS和Spring Boot的視頻。 這是使您熟悉這些工具的一般介紹。 ## 到目前為止的故事... 讓我們從我們可以構造的最簡單的東西開始。 實際上,為了使其盡可能簡單,我們甚至可以省略REST的概念。 (稍后,我們將添加REST以了解它們之間的區別。) 大圖:我們將創建一個簡單的工資服務來管理公司的員工。 我們將員工對象存儲在(H2內存)數據庫中,并(通過稱為 訪問它們 JPA的方式 ) 。 然后,我們將使用允許通過Internet訪問的內容(稱為Spring 包裝 MVC 層)進行 。 以下代碼在我們的系統中定義了一個Employee。 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; @Entity class 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`, `name`, 和 `role`是Employee 屬性 [域對象的](https://www.google.com/search?q=what+is+a+domain+object+in+java) 。 `id` 標有更多的JPA批注以指示它是主鍵,并由JPA提供程序自動填充。 * 當我們需要創建新實例但還沒有ID時,會創建一個自定義構造函數。 有了這個領域對象定義,我們現在可以轉向 [Spring Data JPA](https://spring.io/guides/gs/accessing-data-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<Employee, Long> { } ~~~ 為了獲得所有這些免費功能,我們要做的就是聲明一個擴展Spring Data JPA的接口。 `JpaRepository`,將域類型指定為 `Employee` 和id類型為 `Long`. Spring Data的 [存儲庫解決方案](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories) 可以避開數據存儲細節,而可以使用特定于域的術語解決大多數問題。 信不信由你,這足以啟動一個應用程序! Spring Boot應用程序至少是一個 `public static void main` 入口點和 `@SpringBootApplication`注解。 這告訴Spring Boot盡可能地提供幫助。 nonrest / src / main / java / payroll / PayrollApplication.java ~~~ package payroll; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class PayrollApplication { public static void main(String... args) { SpringApplication.run(PayrollApplication.class, args); } } ~~~ `@SpringBootApplication`是一個元注釋,可引入 **組件掃描** , **自動配置** 和 **屬性支持** 。 在本教程中,我們不會深入探討Spring Boot的細節,但從本質上講,它將啟動servlet容器并提供我們的服務。 盡管如此,沒有數據的應用程序并不是很有趣,所以讓我們預加載它。 Follow類將在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; @Configuration class 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將運行所有 `CommandLineRunner` 一旦應用程序上下文被加載,beans。 * 該跑步者將要求提供一份 `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; @RestController class EmployeeController { private final EmployeeRepository repository; EmployeeController(EmployeeRepository repository) { this.repository = repository; } // Aggregate root // tag::get-aggregate-root[] @GetMapping("/employees") List<Employee> 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`指示每種方法返回的數據將直接寫入響應主體中,而不呈現模板。 * 一個 `EmployeeRepository` 由構造函數注入到控制器中。 * 我們為每個操作提供路線( `@GetMapping`, `@PostMapping`, `@PutMapping` 和 `@DeleteMapping`,對應于HTTP `GET`, `POST`, `PUT`, 和 `DELETE`電話)。 (注意:閱讀每種方法并了解它們的作用非常有用。) * `EmployeeNotFoundException` 是一個例外,用于指示何時查找員工但未找到該員工。 nonrest / src / main / java / payroll / EmployeeNotFoundException.java ~~~ package payroll; class EmployeeNotFoundException extends RuntimeException { EmployeeNotFoundException(Long id) { super("Could not find employee " + id); } } ~~~ 當一個 `EmployeeNotFoundException`拋出該異常,Spring MVC配置的這個額外花絮用于呈現 **HTTP 404** : 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; @ControllerAdvice class EmployeeNotFoundAdvice { @ResponseBody @ExceptionHandler(EmployeeNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) String employeeNotFoundHandler(EmployeeNotFoundException ex) { return ex.getMessage(); } } ~~~ * `@ResponseBody`表示此建議已直接呈現到響應主體中。 * `@ExceptionHandler` 將建議配置為僅在以下情況下響應 `EmployeeNotFoundException` 被拋出。 * `@ResponseStatus` 說要發出 `HttpStatus.NOT_FOUND`,即 **HTTP 404** 。 * 建議的主體生成內容。 在這種情況下,它會給出異常消息。 要啟動該應用程序,請右鍵單擊 `public static void main` 在 `PayRollApplication`并選擇 **運行** 從IDE ,或者: 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 intact Could not find employee 99 ~~~ 此消息很好地顯示了 **HTTP 404** 錯誤和自定義消息“ **找不到雇員99”** 。 顯示當前編碼的交互并不難... 如果您使用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"} ~~~ 構建服務的方式可能會產生重大影響。 在這種情況下,我們說 update ,但是 replace 是一個更好的描述。 例如,如果未提供名稱,則將其清空。 最后,您可以像這樣刪除用戶: ~~~ $ curl -X DELETE localhost:8080/employees/3 # Now if we look again, it's gone $ curl localhost:8080/employees/3 Could not find employee 3 ~~~ 這一切都很好,但是我們有RESTful服務嗎? (如果您沒有收到提示,那么答案是否定的。) 缺少了什么? ## 是什么使RESTful變得有趣? 到目前為止,您已經有了基于Web的服務,該服務可以處理涉及員工數據的核心操作。 但這還不足以使事情變得“ RESTful”。 * 漂亮的網址,例如 `/employees/3` 不是REST。 * 僅僅使用 `GET`, `POST`等不是REST。 * 安排所有CRUD操作不是REST。 實際上,到目前為止,我們更好地描述了 **RPC** ( **遠程過程調用** )。 那是因為沒有辦法知道如何與該服務進行交互。 如果您今天發布了此文檔,則還必須編寫文檔或將開發人員的門戶托管在所有詳細信息的某個位置。 Roy Fielding的這一聲明可能進一步為 之間的區別提供了線索 **REST** 和 **RPC** : > 人們對將任何基于HTTP的接口稱為REST API的人數感到沮喪。 今天的示例是SocialSite REST API。 那就是RPC。 它尖叫RPC。 顯示器上耦合太多,因此應給定X等級。 > > 要使REST體系結構風格清晰地認識到超文本是一種約束,需要采取什么措施? 換句話說,如果應用程序狀態的引擎(以及API)不是由超文本驅動的,則它不能是RESTful的,也不能是REST API。 時期。 是否有一些需要修復的損壞的手冊? —羅伊·菲爾丁(Roy Fielding) https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 在我們的表示中不包含超媒體的副作用是客戶端必須使用硬編碼URI來導航API。 這導致了與電子商務在網絡上興起之前一樣的脆弱性。 這表明我們的JSON輸出需要一點幫助。 介紹 [Spring HATEOAS](https://spring.io/projects/spring-hateoas) ,這是一個Spring項目,旨在幫助您編寫超媒體驅動的輸出。 要將服務升級為RESTful,請將其添加到您的構建中: 將Spring HATEOAS添加到 `dependencies` 的部分 `pom.xml` ~~~ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> ~~~ 這個很小的庫將為我們提供構造以定義RESTful服務,然后將其呈現為可接受的格式以供客戶端使用。 任何RESTful服務的關鍵要素是添加 [鏈接](https://tools.ietf.org/html/rfc8288) 到相關操作的 。 為了使您的控制器更加RESTful,請添加如下鏈接: 獲取單個項目資源 ~~~ @GetMapping("/employees/{id}") EntityModel<Employee> 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,并使用來自的靜態幫助器方法。 WebMvcLinkBuilder建立這些鏈接。 如果您在項目中使用Spring WebFlux,則必須改為使用 WebFluxLinkBuilder. 這與我們以前的非常相似,但是有一些變化: * 方法的返回類型已從更改為 `Employee` 到 `EntityModel<Employee>`. `EntityModel<T>` 是Spring HATEOAS的通用容器,它不僅包含數據,還包含鏈接的集合。 * `linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()` 要求Spring HATEOAS建立指向 `EmployeeController` 的 `one()`方法,并將其標記為 [自我](https://www.iana.org/assignments/link-relations/link-relations.xhtml) 鏈接。 * `linkTo(methodOn(EmployeeController.class).all()).withRel("employees")` 要求Spring HATEOAS構建到聚合根的鏈接, `all()`,并將其稱為“員工”。 “建立鏈接”是什么意思? Spring HATEOAS的核心類型之一是 `Link`。 它包括一個 **URI** 和一個 **rel** (關系)。 鏈接是賦予網絡權力的要素。 在萬維網出現之前,其他文檔系統會呈現信息或鏈接,但是將文檔與具有這種關系元數據的鏈接緊密地聯系在一起就是網絡。 Roy Fielding鼓勵使用使網絡成功的相同技術來構建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------------------v curl -v localhost:8080/employees/1 | json_pp RESTful代表單個員工 ~~~ { "id": 1, "name": "Bilbo Baggins", "role": "burglar", "_links": { "self": { "href": "http://localhost:8080/employees/1" }, "employees": { "href": "http://localhost:8080/employees" } } } ~~~ 解壓縮后的輸出不僅顯示您先前看到的數據元素( `id`, `name` 和 `role`),但也 `_links`包含兩個URI的條目。 整個文檔使用 格式化 [HAL](http://stateless.co/hal_specification.html) 。 HAL是一個輕量級的 [介質類型](https://tools.ietf.org/html/draft-kelly-json-hal-08) ,允許編碼不只是數據,而且還超媒體管制,提醒消費者,他們可以向瀏覽API的其他部分。 在這種情況下,存在一個“自我”鏈接(有點像 `this`代碼中的語句)以及返回 的鏈接 **[聚合根](https://www.google.com/search?q=What+is+an+aggregate+root)** 。 為了使聚合根ALSO更具RESTful,您希望包括頂級鏈接,同時ALSO包括其中的所有RESTful組件。 所以我們把這個 獲取聚合根 ~~~ @GetMapping("/employees") List<Employee> all() { return repository.findAll(); } ~~~ 進入這個 獲取聚合的根 **資源** ~~~ @GetMapping("/employees") CollectionModel<EntityModel<Employee>> all() { List<EntityModel<Employee>> 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<>`是另一個Spring HATEOAS容器; 它旨在封裝資源集合,而不是單個資源實體,例如 `EntityModel<>` 從以前開始。 `CollectionModel<>`,也允許您包含鏈接。 不要讓第一個陳述漏掉。 “封裝集合”是什么意思? 員工收款? 不完全的。 由于我們在談論REST,因此它應該封裝 集合 **員工資源的** 。 這就是為什么您要獲取所有員工,然后將其轉換為以下列表的原因 `EntityModel<Employee>`對象。 (感謝Java 8流!) 如果重新啟動應用程序并獲取聚合根,則現在可以看到它的外觀。 RESTful表示員工資源集合 ~~~ { "_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服務成為可能。 可以維護現有鏈接,而將來可以添加新鏈接。 較新的客戶端可以利用新鏈接,而舊客戶端可以在舊鏈接上維持自己的狀態。 如果服務被重新定位和移動,這將特別有用。 只要保持鏈接結構,客戶端就可以查找并與事物進行交互。 ## 簡化鏈接創建 在前面的代碼中,您是否注意到在創建單個員工鏈接時重復執行此操作? 兩次顯示了提供指向員工的單個鏈接以及創建指向聚合根的“員工”鏈接的代碼。 如果那引起您的關注,那就好! 有一個解決方案。 簡而言之,您需要定義一個函數來轉換 `Employee` 反對 `EntityModel<Employee>`對象。 雖然您可以輕松地自己編寫此方法,但在實現Spring HATEOAS的過程中仍有很多好處 `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; @Component class EmployeeModelAssembler implements RepresentationModelAssembler<Employee, EntityModel<Employee>> { @Override public EntityModel<Employee> 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<Employee>`). 您之前在控制器中看到的所有代碼都可以移入此類。 并通過應用Spring Framework的 `@Component` 注釋,則在應用啟動時將自動創建匯編器。 Spring HATEOAS所有模型的抽象基類是 RepresentationModel。 但為簡單起見,我建議使用 EntityModel&lt;T&gt; 作為將所有POJO輕松包裝為模型的機制。 要利用此匯編器,您只需更改 `EmployeeController` 通過將匯編程序注入構造函數中。 將EmployeeModelAssembler注入控制器 ~~~ @RestController class EmployeeController { private final EmployeeRepository repository; private final EmployeeModelAssembler assembler; EmployeeController(EmployeeRepository repository, EmployeeModelAssembler assembler) { this.repository = repository; this.assembler = assembler; } ... } ~~~ 從這里,您可以在單項employee方法中使用該匯編器: 使用匯編器獲取單項資源 ~~~ @GetMapping("/employees/{id}") EntityModel<Employee> one(@PathVariable Long id) { Employee employee = repository.findById(id) // .orElseThrow(() -> new EmployeeNotFoundException(id)); return assembler.toModel(employee); } ~~~ 這段代碼幾乎相同,除了不是創建 `EntityModel<Employee>`在這里,您將其委托給匯編器。 也許看起來并不多。 在聚合根控制器方法中應用相同的內容會更加令人印象深刻: 使用匯編器獲取聚合根資源 ~~~ @GetMapping("/employees") CollectionModel<EntityModel<Employee>> all() { List<EntityModel<Employee>> employees = repository.findAll().stream() // .map(assembler::toModel) // .collect(Collectors.toList()); return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel()); } ~~~ 再次,代碼幾乎相同,但是您必須替換所有代碼 `EntityModel<Employee>` 與創建邏輯 `map(assembler::toModel)`。 感謝Java 8方法參考,將其插入并簡化您的控制器非常容易。 A key design goal of Spring HATEOAS is to make it easier to do The Right Thing?. In this scenario: adding hypermedia to your service without hard coding a thing. At this stage, you’ve created a Spring MVC REST controller that actually produces hypermedia-powered content! Clients that don’t speak HAL can ignore the extra bits while consuming the pure data. Clients that DO speak HAL can navigate your empowered API. But that is not the only thing needed to build a truly RESTful service with Spring. ## Evolving REST APIs With one additional library and a few lines of extra code, you have added hypermedia to your application. But that is not the only thing needed to make your service RESTful. An important facet of REST is the fact that it’s neither a technology stack nor a single standard. REST是體系結構約束的集合,采用這些約束可使您的應用程序更具彈性。 彈性的關鍵因素是,當您升級服務時,您的客戶不會遭受停機時間的困擾。 在“過去”的日子里,升級是臭名昭著的,因為它破壞了客戶。 換句話說,對服務器的升級需要對客戶端的更新。 在當今時代,升級花費的停機時間甚至數小時甚至數分鐘可能會導致數百萬美元的收入損失。 一些公司要求您向管理層提出計劃,以最大程度地減少停機時間。 過去,您可以在周日凌晨2:00進行升級,而此時負載已降至最低。 但是,在當今與其他時區的國際客戶進行的基于Internet的電子商務中,這種策略并不那么有效。 [基于SOAP的服務 基于](https://www.tutorialspoint.com/soap/what_is_soap.htm) 和 [CORBA的服務](https://www.corba.org/faq.htm) 非常脆弱。 很難推出可以同時支持新舊客戶端的服務器。 借助基于REST的實踐,它變得容易得多。 特別是使用Spring堆棧。 ### 支持對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; @Entity class 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()`被定義為。 它使用 `firstName` 和 `lastName` 產生價值的字段。 * 舊的“虛擬”二傳手 `name` 屬性也被定義, `setName()`。 它解析輸入的字符串并將其存儲到適當的字段中。 當然,對您的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<Employee> entityModel = assembler.toModel(repository.save(newEmployee)); return ResponseEntity // .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) // .body(entityModel); } ~~~ * 新的 `Employee`對象將像以前一樣保存。 但是使用 `EmployeeModelAssembler`. * 春季MVC `ResponseEntity`用于創建 **HTTP 201已創建** 狀態消息。 這種類型的響應通常包括一個 **Location** 響應標頭,并且我們使用從模型的自相關鏈接派生的URI。 * 此外,返回已保存對象的基于模型的版本。 進行這些調整后,您可以使用相同的端點來創建新的員工資源,并使用舊版 `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`),還 了 **Location** 填充 標頭 `[http://localhost:8080/employees/3](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<Employee> entityModel = assembler.toModel(updatedEmployee); return ResponseEntity // .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) // .body(entityModel); } ~~~ 這 `Employee` 從 `save()` 然后使用 `EmployeeModelAssembler` 變成一個 `EntityModel<Employee>`目的。 使用 `getRequiredLink()` 方法,您可以檢索 `Link` 由創建 `EmployeeModelAssembler` 與一個 `SELF`rel。 此方法返回一個 `Link` 必須將其變成 `URI` 與 `toUri` 方法。 由于我們需要比 更詳細的HTTP響應代碼 **200 OK** ,因此我們將使用Spring MVC的 `ResponseEntity`包裝紙。 它有一個方便的靜態方法 `created()`我們可以在其中插入資源的URI。 如果 這是有爭議的, **HTTP 201 Created** 帶有正確的語義, 因為我們不一定要“創建”新資源。 但是它預裝了 **Location** 響應標頭,因此請與它一起運行。 ~~~ $ 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操作: 處理DELETE請求 ~~~ @DeleteMapping("/employees/{id}") ResponseEntity<?> deleteEmployee(@PathVariable Long id) { repository.deleteById(id); return ResponseEntity.noContent().build(); } ~~~ 這將返回 **HTTP 204 No Content** 響應。 ~~~ $ 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并更好地為您的客戶服務,您需要接受 的概念 **Hypermedia作為應用程序狀態引擎** 。 這意味著什么? 在本節中,您將詳細研究它。 業務邏輯不可避免地建立涉及流程的規則。 此類系統的風險在于,我們經常將此類服務器端邏輯帶入客戶端,并建立牢固的耦合。 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 + '}'; } } ~~~ * 該課程需要一個JPA `@Table` 批注將表的名稱更改為 `CUSTOMER_ORDER` 因為 `ORDER` 不是表格的有效名稱。 * 它包括一個 `description` 領域以及 `status` 場地。 從客戶提交訂單到完成或取消訂單之時,訂單必須經歷一系列特定的狀態轉換。 可以將其捕獲為Java `enum`: 鏈接/src/main/java/payroll/Status.java ~~~ package payroll; enum Status { IN_PROGRESS, // COMPLETED, // CANCELLED } ~~~ 這個 `enum` 捕獲各種狀態 `Order`可以占領。 對于本教程,讓我們保持簡單。 為了支持與數據庫中的訂單進行交互,您必須定義一個相應的Spring Data存儲庫: Spring Data JPA的 `JpaRepository` 基本介面 ~~~ interface OrderRepository extends JpaRepository<Order, Long> { } ~~~ With this in place, you can now define a basic `OrderController`: links/src/main/java/payroll/OrderController.java ~~~ @RestController class OrderController { private final OrderRepository orderRepository; private final OrderModelAssembler assembler; OrderController(OrderRepository orderRepository, OrderModelAssembler assembler) { this.orderRepository = orderRepository; this.assembler = assembler; } @GetMapping("/orders") CollectionModel<EntityModel<Order>> all() { List<EntityModel<Order>> orders = orderRepository.findAll().stream() // .map(assembler::toModel) // .collect(Collectors.toList()); return CollectionModel.of(orders, // linkTo(methodOn(OrderController.class).all()).withSelfRel()); } @GetMapping("/orders/{id}") EntityModel<Order> one(@PathVariable Long id) { Order order = orderRepository.findById(id) // .orElseThrow(() -> new OrderNotFoundException(id)); return assembler.toModel(order); } @PostMapping("/orders") ResponseEntity<EntityModel<Order>> 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控制器設置。 * 它同時注入 `OrderRepository` 以及(尚未構建) `OrderModelAssembler`. * The first two Spring MVC routes handle the aggregate root as well as a single item `Order` resource request. * The third Spring MVC route handles creating new orders, by starting them in the `IN_PROGRESS` state. * All the controller methods return one of Spring HATEOAS’s `RepresentationModel` subclasses to properly render hypermedia (or a wrapper around such a type). 在構建之前 `OrderModelAssembler`,讓我們討論一下需要發生的事情。 您正在建模之間的狀態流 `Status.IN_PROGRESS`, `Status.COMPLETED`, 和 `Status.CANCELLED`。 向客戶端提供此類數據時,很自然的事情是讓客戶端根據此有效負載決定它可以做什么。 但這是錯誤的。 在此流程中引入新狀態時會發生什么? UI上各種按鈕的放置可能是錯誤的。 如果您更改了每個州的名稱,可能是在編碼國際支持并顯示每個州的特定于語言環境的文本時呢? 那很可能會破壞所有客戶。 輸入 **HATEOAS** 或 **Hypermedia作為應用程序狀態引擎** 。 與其讓客戶端解析有效負載,不如讓客戶端鏈接以發出有效動作信號。 將基于狀態的操作與數據的有效負載分離。 換句話說,當 **CANCEL** 和 **COMPLETE** 是有效動作時,將它們動態添加到鏈接列表中。 鏈接存在時,客戶端僅需要向用戶顯示相應的按鈕。 這使客戶端不必知道何時需要執行此類操作,從而減少了服務器及其客戶端在狀態轉換邏輯上不同步的風險。 已經接受Spring HATEOAS的概念 `RepresentationModelAssembler` 組件,將這樣的邏輯放在 `OrderModelAssembler` 將是捕獲此業務規則的理想場所: 鏈接/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; @Component class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<Order>> { @Override public EntityModel<Order> toModel(Order order) { // Unconditional links to single-item resource and aggregate root EntityModel<Order> 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; } } ~~~ This resource assembler always includes the **self** link to the single-item resource as well as a link back to the aggregate root. But it also includes two conditional links to `OrderController.cancel(id)` as well as `OrderController.complete(id)` (not yet defined). These links are ONLY shown when the order’s status is `Status.IN_PROGRESS`. 如果客戶可以采用HAL并具有讀取鏈接的能力,而不是簡單地讀取普通的舊JSON數據,則可以交易對訂單系統領域知識的需求。 這自然減少了客戶端和服務器之間的耦合。 它為調整訂單履行流程打開了一扇門,而不會破壞流程中的客戶。 要完善訂單履行,請將以下內容添加到 `OrderController` 為了 `cancel` 手術: 在OrderController中創建“取消”操作 ~~~ @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](https://tools.ietf.org/html/rfc7807) `Problem`,一個支持超媒體的錯誤容器。 如果轉換確實有效,則轉換 `Order` 到 `CANCELLED`. And add this to the `OrderController` as well for order completion: Creating a "complete" operation in the 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` 除非處于適當狀態,否則狀態將無法完成。 讓我們更新 `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; @Configuration class 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" } ~~~ 完成所有這些操作后,您的訂單履行服務便可以有條件地顯示可用的操作。 它還可以防止無效操作。 By leveraging the protocol of hypermedia and links, clients can be built sturdier and less likely to break simply because of a change in the data. And Spring HATEOAS eases building the hypermedia you need to serve to your clients. ## Summary Throughout this tutorial, you have engaged in various tactics to build REST APIs. As it turns out, REST isn’t just about pretty URIs and returning JSON instead of XML. Instead, the following tactics help make your services less likely to break existing clients you may or may not control: * 不要刪除舊字段。 相反,支持他們。 * 使用基于rel的鏈接,這樣客戶端就不必對URI進行硬編碼。 * 盡可能保留舊的鏈接。 即使必須更改URI,也請保留rels,以便較舊的客戶端可以使用較新的功能。 * 使用鏈接(而不是有效負載數據)來指示客戶端何時可以進行各種狀態驅動操作。 似乎需要一些努力才能建立起來 `RepresentationModelAssembler`每種資源類型的實現,并在所有控制器中使用這些組件。 但是,服務器端設置的這一額外點(借助Spring HATEOAS可以輕松實現)可以確保您控制的客戶端(更重要的是,那些您不需要的客戶端)可以在您開發API時輕松升級。 到此,我們的教程結束了如何使用Spring構建RESTful服務。 本教程的每個部分在單個github存儲庫中作為單獨的子項目進行管理: * **nonrest** —沒有超媒體的簡單Spring MVC應用程序 * **rest** — Spring MVC + Spring HATEOAS應用程序,每個資源都有HAL表示形式 * **Evolution** — REST應用程序,其中的字段已演化,但保留了舊數據以實現向后兼容性 * **鏈接** \-REST應用程序,其中條件鏈接用于向客戶端發送有效狀態更改信號 要查看使用Spring HATEOAS的更多示例,請參見 [https://github.com/spring-projects/spring-hateoas-examples](https://github.com/spring-projects/spring-hateoas-examples) 。 要進行更多探索,請查看Spring隊友Oliver Gierke的以下視頻:
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看