作者 | Ion Pascari
譯者 | 天道酬勤 責編 | 徐威龍
最近,作者在把HATEOAS實現到REST Web服務時遇到了一件有趣的事情,而且他也很幸運地嘗試了一個名為MongoDB的NoSQL數據庫,他發現該數據庫在許多不需要管理實務的不同情況下非常方便。因此,今天他將和我們分享這種經驗。也許我們中的一些人可能會學到一些新的東西,即使已經已經學過,但仍然可以對已經學知識有一個鞏固複習。
下面我們來看一下作者是如何使用MongoDB和HATEOAS創建REST Web服務,該服務可以實現Richardson成熟度模型的第三級。
首先,我們來介紹一下REST,然後逐步介紹HATEOAS和MongoDB。那麼,REST是什麼呢?
REST
萬維網聯盟指出,REST是一個如何構建Web服務的模型。REST Web是WWW(基於HTTP)的子集,其中代理提供統一的接口語義,本質上是創建,檢索,更新和刪除,而不是任意或特定於應用程序的接口,並且僅通過交換表示來操縱資源。
那麼,現在我們知道REST是什麼了,作者將簡要列出Roy Fielding在其論文的第五章中提到的所有約束:
- 客戶端-服務器:以這樣的方式實施服務:將用戶界面關注點(客戶端獲得可移植性)與數據存儲關注點(服務器獲得可伸縮性)分離開來。
- 無狀態:在客戶端和服務器之間實現通信時,服務器在處理請求時永遠不會利用儲存在服務器上下文中的任何信息,而與會話相關的所有信息都存儲在客戶端中。
- 緩存:當可以(隱式或顯式)緩存請求的響應時,客戶端應獲取緩存的響應。
- 統一接口:所有REST服務都應依賴組件之間相同的統一設計。接口應與提供的服務解耦。
- 分層系統:客戶端永遠不知道它們是直接連接到服務器還是連接到某些中間服務器。例如,請求可以通過代理,該代理具有負載平衡或共享緩存的功能。
Richardson成熟度模型
圖1:Richardson成熟度模型的級別
正如Martin Fowler所說,該模型是“由Leonard Richardson開發的模型,它將REST方法的主要元素分解為三個步驟。這些步驟引入了資源、HTTP動詞和超媒體控件”。
這裡簡要介紹一下這些級別:
- POX沼澤:只有一種資源和一種請求方法POST,並且只有一種通信方式XML。
- 資源:我們堅持使用POST方法,但是我們獲得了更多可以處理的資源。
- HTTP動詞:目前在適當的情況下(資源),我們正在使用其他HTTP方法,例如GET或DELETE。通常,CRUD操作在此處實現。
- 超媒體控件:HATEOAS(應用程序狀態的超文本引擎),應為客戶端提供一個使用服務的啟動鏈接,然後,每個響應都應包含指向該服務其他可能性的超鏈接。
既然我們知道了REST,並且已經介紹了它的成熟度模型,接下來我們再簡要介紹一個NoSQL數據庫MongoDB,然後,我們將進行演示!
為什麼選擇HATEOAS?
首先,我們要指出REST並不容易,沒有真正理解REST的人會說這很容易。通常,對於短期內不會增長或更改的小型服務,如果你達到了第二級(HTTP動詞),那就更好了。
那麼那些正在增長的大型服務呢?很多人會說,只要你做第二級就可以了。為什麼?因為HATEOAS是使REST變得複雜的原因之一,這是困難的。如果你真的想獲得其優勢,則必須在客戶端上編寫更多代碼——處理錯誤、如何解釋資源、如何分析提供的鏈接和服務器來構建全面而有用的鏈接等。讓我們來看看HATEOAS其中的一些優勢:
- 可用性:客戶端開發人員可以根據你提供的鏈接來有效地使用、瞭解和探索你的服務。而且,他們可以想象你項目的框架。
- 可伸縮性:遵循所提供的鏈接而不是不依賴於服務的代碼更改來構造鏈接的客戶端。
- 靈活性:提供服務較老版本和較新版本的鏈接,使你可以輕鬆地與基於舊版本的客戶端和基於新版本的客戶端進行互操作。
- 有效性:依賴HATEOAS的客戶端永遠不必擔心服務器上的新版本或代碼更改(如硬編碼的版本)。
- 松耦合:HATEOAS通過分配構建和提供鏈接到服務器的職責來促進客戶端和服務器之間的松耦合。
NoSQL?MongoDB?
那麼,什麼是NoSQL數據庫?從名稱“非SQL”或“非關係型”衍生而來,這些數據庫不使用類似SQL的查詢語言,通常稱為結構化存儲。這些數據庫自1960年就已經存在,但是直到現在一些大公司(例如Google和Facebook)開始使用它們時,這些數據庫才流行起來。該數據庫最明顯的優勢是擺脫了一組固定的列、連接和類似SQL的查詢語言的限制。
有時,NoSQL這個名稱也可能表示“不僅僅SQL”,來確保它們可能支持SQL。 NoSQL數據庫使用諸如鍵值、寬列、圖形或文檔之類的數據結構,並且可以如JSON之類的不同格式存儲。
MongoDB是一種無模式的NoSQL數據庫,它是面向文檔的,因此,如上所述,它提供了高性能和良好的可伸縮性,並且是跨平臺的。 之所以推薦MongoDB,是因為它具有完整的索引支持,JSON格式的對象存儲結構簡單明瞭,出色的動態文檔查詢支持,不必將應用程序對象轉換到到數據庫對象以及MongoDB的專業支持。
準備使用MongoDB來編寫代碼
現在我們準備進行正題。讓我們構建一個簡單的EmployeeManager Web服務,我們將使用它來演示與MongoDB連接的HATEOAS。
為了引導我們的應用程序,我們將使用Spring Initializr。我們將使用Spring HATEOAS和Spring Data MongoDB作為依賴項。你應該看到類似下圖2所示的內容。
圖2 :引導應用程序
配置完成後,下載zip並將其作為Maven項目導入你喜歡的IDE中。
首先,讓我們配置application.properties。要獲得MongoDB連接,你應該處理以下參數:
<code>spring.data.mongodb.host= //Mongo server hostspring.data.mongodb.port= //Mongo server portspring.data.mongodb.username= //Login userspring.data.mongodb.password= //Passwordspring.data.mongodb.database= //Database name/<code>
一般來說,如果所有內容都是全新安裝的,並且你沒有更改或修改任何Mongo屬性,則只需提供一個數據庫名稱(已經通過GUI創建了一個數據庫名稱)。
<code>spring.data.mongodb.database = EmployeeManager/<code>
另外,為了啟動Mongo實例,作者創建了一個.bat,它指向安裝文件夾和數據文件夾。它是這樣的:
<code>"C:\Program Files\MongoDB\Server\3.6\bin\mongod" --dbpath D:\Inther\EmployeeManager\warehouse-data\db /<code>
現在,我們來快速創建模型。這裡有兩個模型,員工模型和部門模型。檢查它們,確保有沒有參數、getter、setter、equals方法和hashCode生成的構造函數。(不用擔心,所有代碼都在GitHub上,你可以稍後查看它:https://github.com/theFaustus/EmployeeManager。)
<code>public class Employee { private String employeeId; private String firstName; private String lastName; private int age;}public class Department { private String department; private String name; private String description; private List employees;}/<code>
現在我們已經完成了模型的製作,讓我們來創建存儲庫,以便來測試持久性。存儲庫如下所示:
<code>public interface EmployeeRepository extends MongoRepository {}public interface DepartmentRepository extends MongoRepository{}/<code>
如上所示,這裡沒有方法,因為大家都知道Spring Data中的中心接口被命名為Repository,在其之上是CrudRepository,它提供了處理模型的基本操作。
在CrudRepository之上,我們有PagingAndSortingRepository,它為我們提供了一些擴展功能,來簡化分頁和排序訪問。在我們的案例中,最重要的是MongoRepository,它用於嚴格處理我們的Mongo實例。
因此,對於我們的案例來說,除了那些現成的方法外,我們不需要任何其他方法,但是僅出於學習目的,作者在這裡要提到的是你可以添加其他查詢方法的兩種方法:
- “惰性”(查詢創建):此策略將嘗試通過分析查詢方法的名稱並推斷關鍵字(例如findByLastnameAndFirstname)來構建查詢。
- 編寫查詢:這裡沒有什麼特別的。例如,只用@Query註釋你的方法,然後自己編寫查詢。你也可以在MongoDB中編寫查詢。下面是基於JSON的查詢方法的示例:
<code>@Query("{ 'firstname' : ?0 }")List findByTheEmployeesFirstname(String firstname);/<code>
至此,我們已經可以測試我們持久性如何工作。我們只需要對模型進行一些調整即可。通過調整,作者的意思是我們需要註釋一些東西。Spring Data MongoDB使用MappingMongoConverter將對象映射到文檔,下面是我們將要使用的一些註釋:
- @Id :字段級別註釋,指出你的哪個字段是身份標識。
- @Document :類級別的註釋,用於表示該類將被持久化到數據庫中。
- @DBRef :描述參考性的字段級別註釋。
註釋完成後,我們可以使用CommandLineRunner獲取數據庫中的一些數據,CommandLineRunner是一個接口,用於在應用程序完全啟動時(即在run()方法之前)運行代碼段。在下面,你可以看一下作者的Bean配置。
<code>@Bean public CommandLineRunner init(EmployeeRepository employeeRepository, DepartmentRepository departmentRepository) { return (args) -> { employeeRepository.deleteAll(); departmentRepository.deleteAll(); Employee e = employeeRepository.save(new Employee("Ion", "Pascari", 23)); departmentRepository.save(new Department("Service Department", "Service Rocks!", Arrays.asList(e))); for (Department d : departmentRepository.findAll()) { LOGGER.info("Department: " + d); } };} /<code>
我們已經創建了一些模型,並對它們進行了持久化。現在,我們需要一種與他們交互的方式。如上所說,所有代碼都可以在GitHub上找到,因此作者在這裡將僅向我們展示一個域服務(接口和實現)。
接口如下:
<code>public interface EmployeeService { Employee saveEmployee(Employee e); Employee findByEmployeeId(String employeeId); void deleteByEmployeeId(String employeeId); void updateEmployee(Employee e); boolean employeeExists(Employee e); List findAll(); void deleteAll();}/<code>
接口的實現如下:
<code>@Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeRepository employeeRepository; @Override public Employee saveEmployee(Employee e) { return employeeRepository.save(e); } @Override public Employee findByEmployeeId(String employeeId) { return employeeRepository.findOne(employeeId); } @Override public void deleteByEmployeeId(String employeeId) { employeeRepository.delete(employeeId); } @Override public void updateEmployee(Employee e) { employeeRepository.save(e); } @Override public boolean employeeExists(Employee e) { return employeeRepository.exists(Example.of(e)); } @Override public List findAll() { return employeeRepository.findAll(); } @Override public void deleteAll() { employeeRepository.deleteAll(); }}/<code>
這裡沒有什麼特別的要注意的,下面我們將繼續討論最後一個難題——控制器!你可以在下面看到員工資源的控制器實現。
<code>@RestController@RequestMapping("/employees")public class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping(value = "/list/", method = RequestMethod.GET) public HttpEntity> getAllEmployees() { List employees = employeeService.findAll(); if (employees.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } else { return new ResponseEntity<>(employees, HttpStatus.OK); } } @RequestMapping(value = "/employee/{id}", method = RequestMethod.GET) public HttpEntity getEmployeeById(@PathVariable("id") String employeeId) { Employee byEmployeeId = employeeService.findByEmployeeId(employeeId); if (byEmployeeId == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } else { return new ResponseEntity<>(byEmployeeId, HttpStatus.OK); } } @RequestMapping(value = "/employee/", method = RequestMethod.POST) public HttpEntity> saveEmployee(@RequestBody Employee e) { if (employeeService.employeeExists(e)) { return new ResponseEntity<>(HttpStatus.CONFLICT); } else { Employee employee = employeeService.saveEmployee(e); URI location = ServletUriComponentsBuilder .fromCurrentRequest().path("/employees/employee/{id}") .buildAndExpand(employee.getEmployeeId()).toUri(); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setLocation(location); return new ResponseEntity<>(httpHeaders, HttpStatus.CREATED); } } @RequestMapping(value = "/employee/{id}", method = RequestMethod.PUT) public HttpEntity> updateEmployee(@PathVariable("id") String id, @RequestBody Employee e) { Employee byEmployeeId = employeeService.findByEmployeeId(id); if(byEmployeeId == null){ return new ResponseEntity<>(HttpStatus.NOT_FOUND); } else { byEmployeeId.setAge(e.getAge()); byEmployeeId.setFirstName(e.getFirstName()); byEmployeeId.setLastName(e.getLastName()); employeeService.updateEmployee(byEmployeeId); return new ResponseEntity<>(employeeService, HttpStatus.OK); } } @RequestMapping(value = "/employee/{id}", method = RequestMethod.DELETE) public ResponseEntity> deleteEmployee(@PathVariable("id") String employeeId) { employeeService.deleteByEmployeeId(employeeId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @RequestMapping(value = "/employee/", method = RequestMethod.DELETE) public ResponseEntity> deleteAll() { employeeService.deleteAll(); return new ResponseEntity<>(HttpStatus.NO_CONTENT); }} /<code>
因此,對於上面實現的所有方法,我們將自己定位在Richardson成熟度模型的第二級,因為我們使用了HTTP動詞並實現了CRUD操作。現在,我們有了與數據進行交互的方法,並且可以使用Postman,我們可以如下圖3所示檢索資源,或者可以如下圖4所示添加新資源。
圖3 :檢索JSON中的部門列表
圖4:JSON中添加新員工
HATEOAS即將來臨
絕大多數人都止步於此,因為通常情況下,對他們或Web服務而言,這已經就足夠了,但這不是我們在這裡的原因。因此,如前所述,支持HATEOAS或超媒體驅動的站點的Web服務應該能夠提供有關如何使用和導航Web服務的信息,方法是包含與響應之間具有某種關係的鏈接。
你可以將HATEOAS想象成一個路標。當你開車的時候,這些標誌會指引你。例如,如果你需要到達機場,則只需遵循指示標誌,如果你需要返回,則再次遵循指示標誌就可以了,而且你一直知道你可以待在哪裡、停車或開車等等。
讓我們實現資源表示形式附帶的鏈接,我們必須通過擴展ResourceSupport來繼承add()方法來調整模型,這給我們提供一個不錯的選擇,可以為資源表示形式設置值,而無需添加任何新字段 。
<code>@Documentpublic class Employee extends ResourceSupport{...} /<code>
現在,讓我們開始創建鏈接。為此,Spring HATEOAS提供了一個Link對象來存儲這種信息,並提供CommandLinkBuilder來構建它。
假設我們想要為員工id添加一個GET響應的鏈接。
<code>@RequestMapping(value = "/employee/{id}", method = RequestMethod.GET)public HttpEntity getEmployeeById(@PathVariable("id") String employeeId) { Employee byEmployeeId = employeeService.findByEmployeeId(employeeId); if (byEmployeeId == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } else { byEmployeeId.add(linkTo(methodOn(EmployeeController.class).getEmployeeById(byEmployeeId.getEmployeeId())).withSelfRel()); return new ResponseEntity<>(byEmployeeId, HttpStatus.OK); }}/<code>
如果你注意到以下幾個方法:
- add():設置鏈接值的方法。
- linkTo(Class controller):一個靜態導入的方法,該方法允許創建一個新的ControllerLinkBuilder,它的基類指向控制器類。
- methodOn(Class controller,Object ... parameters):靜態導入的方法,它創建到控制器類的間接引用,從而能夠從該類調用方法並使用其返回類型。
- withSelfRel():一種最終創建鏈接的方法,該鏈接默認具有指向自身的關係。
現在,GET將產生以下響應:
<code>{ "employeeId": "5a6f67519fea6938e0196c4d", "firstName": "Ion", "lastName": "Pascari", "age": 23, "_links": { "self": { "href": "http://localhost:8080/employees/employee/5a6f67519fea6938e0196c4d" } } }/<code>
<code>響應不僅包含員工的詳細信息,還包含可在其中導航的自鏈接URL。/<code>
- _links代表資源表示的新設置值。
- self代表鏈接指向的關係類型。在這種情況下,它是一個自引用超鏈接。也可能有其他類型的關係,例如指向另一個類(我們將在稍後介紹)。
- href是標識資源的URL。
現在,假設我們要為部門列表添加指向GET響應的鏈接。在這裡,事情變得越來越有趣,因為部門不僅指向自己,也指向員工,員工也指向自己和他們的列表。因此,讓我們看一下代碼:
<code>@RequestMapping(value = "/list/", method = RequestMethod.GET)public HttpEntity> getAllDepartments() { List departments = departmentService.findAll(); if (departments.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT) } else { departments.forEach(d -> d.add(linkTo(methodOn(DepartmentController.class).getAllDepartments()).withRel("departments"))); departments.forEach(d -> d.add(linkTo(methodOn(DepartmentController.class).getDepartmentById(d.getDepartmentId())).withSelfRel())); departments.forEach(d -> d.getEmployees().forEach(e -> { e.add(linkTo(methodOn(EmployeeController.class).getAllEmployees()).withRel("employees")); e.add(linkTo(methodOn(EmployeeController.class).getEmployeeById(e.getEmployeeId())).withSelfRel()); })); return new ResponseEntity<>(departments, HttpStatus.OK); }}/<code>
<code> 因此,此代碼將產生以下響應:/<code>
<code>{ "departmentId": "5a6f6c269fea690904a02657", "name": "Service Department", "description": "Service Rocks!", "employees": [ { "employeeId": "5a6f6c269fea690904a02656", "firstName": "Ion", "lastName": "Pascari", "age": 23, "_links": { "employees": { "href": "http://localhost:8080/employees/list/" }, "self": { "href": "http://localhost:8080/employees/employee/5a6f6c269fea690904a02656" } } } ], "_links": { "departments": { "href": "http://localhost:8080/departments/list/" }, "self": { "href": "http://localhost:8080/departments/department/5a6f6c269fea690904a02657" } }}除了存在一些未命名為self的關係鏈接之外,沒有任何改變。這些是作者之前談到的其他類型的關係,它們是與withRel(String rel)建立在一起的:/<code>
- withRel(String rel):一種方法,該方法最終以指向給定rel的關係創建Link。
恭喜你, 到這裡,我們可以說已經達到了Richardson成熟度模型的第3級,當然,我們之所以沒有這樣做,是因為我們需要對Web服務進行更多的檢查和改進,例如提供有關資源狀態或任何其他事物的鏈接,但是我們幾乎做到了!
你可以在此處獲得完整的GitHub源代碼:
https://github.com/theFaustus/EmployeeManager
希望你能喜歡,如果有不清楚的地方或其他意見,歡迎評論區留言告訴我們或者和我們討論。
關於愛可生
愛可生成立於2003年,依託於融合、開放、創新的數據處理技術和服務能力,為大型行業用戶的特定場景提供深度挖掘數據價值的解決方案。
公司持續積累的核心關鍵技術,覆蓋到分佈式數據庫集群、雲數據平臺、數據庫大體量運管平臺、海量數據集成於存儲、清洗與治理、人工智能分析挖掘、可視化展現、安全與隱私保護等多個領域。
公司已與多個行業內的專業公司建立了長期夥伴關係,不斷促進新技術與行業知識相結合,為用戶尋求新的數據驅動的價值增長點。公司已在金融、能源電力、廣電、政府等行業取得了眾多大型用戶典型成功案例,獲得了市場的認可和業務的持續增長。