一文教你如何使用 MongoDB 和 HATEOAS 创建 REST Web 服务
作者 |?Ion Pascari
譯者 |?天道酬勤?責編 | 徐威龍
封圖|?CSDN 下載于視覺中國
最近,作者在把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連接,你應該處理以下參數:
spring.data.mongodb.host=?//Mongo?server?host spring.data.mongodb.port=?//Mongo?server?port spring.data.mongodb.username=?//Login?user spring.data.mongodb.password=?//Password spring.data.mongodb.database=?//Database?name一般來說,如果所有內容都是全新安裝的,并且你沒有更改或修改任何Mongo屬性,則只需提供一個數據庫名稱(已經通過GUI創建了一個數據庫名稱)。
spring.data.mongodb.database?=?EmployeeManager另外,為了啟動Mongo實例,作者創建了一個.bat,它指向安裝文件夾和數據文件夾。它是這樣的:
"C:\Program?Files\MongoDB\Server\3.6\bin\mongod"?--dbpath?D:\Inther\EmployeeManager\warehouse-data\db?現在,我們來快速創建模型。這里有兩個模型,員工模型和部門模型。檢查它們,確保有沒有參數、getter、setter、equals方法和hashCode生成的構造函數。(不用擔心,所有代碼都在GitHub上,你可以稍后查看它:https://github.com/theFaustus/EmployeeManager。)
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<Employee>?employees; }現在我們已經完成了模型的制作,讓我們來創建存儲庫,以便來測試持久性。存儲庫如下所示:
public?interface?EmployeeRepository extends?MongoRepository<Employee,?String>?{ } public?interface?DepartmentRepository extends?MongoRepository<Department,String>{ }如上所示,這里沒有方法,因為大家都知道Spring Data中的中心接口被命名為Repository,在其之上是CrudRepository,它提供了處理模型的基本操作。
在CrudRepository之上,我們有PagingAndSortingRepository,它為我們提供了一些擴展功能,來簡化分頁和排序訪問。在我們的案例中,最重要的是MongoRepository,它用于嚴格處理我們的Mongo實例。
因此,對于我們的案例來說,除了那些現成的方法外,我們不需要任何其他方法,但是僅出于學習目的,作者在這里要提到的是你可以添加其他查詢方法的兩種方法:
“惰性”(查詢創建):此策略將嘗試通過分析查詢方法的名稱并推斷關鍵字(例如findByLastnameAndFirstname)來構建查詢。
編寫查詢:這里沒有什么特別的。例如,只用@Query注釋你的方法,然后自己編寫查詢。你也可以在MongoDB中編寫查詢。下面是基于JSON的查詢方法的示例:
至此,我們已經可以測試我們持久性如何工作。我們只需要對模型進行一些調整即可。通過調整,作者的意思是我們需要注釋一些東西。Spring Data MongoDB使用MappingMongoConverter將對象映射到文檔,下面是我們將要使用的一些注釋:
@Id :字段級別注釋,指出你的哪個字段是身份標識。
@Document :類級別的注釋,用于表示該類將被持久化到數據庫中。
@DBRef :描述參考性的字段級別注釋。
注釋完成后,我們可以使用CommandLineRunner獲取數據庫中的一些數據,CommandLineRunner是一個接口,用于在應用程序完全啟動時(即在run()方法之前)運行代碼段。在下面,你可以看一下作者的Bean配置。
@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);}}; }?我們已經創建了一些模型,并對它們進行了持久化。現在,我們需要一種與他們交互的方式。如上所說,所有代碼都可以在GitHub上找到,因此作者在這里將僅向我們展示一個域服務(接口和實現)。
接口如下:
public?interface?EmployeeService?{Employee?saveEmployee(Employee?e);Employee?findByEmployeeId(String?employeeId);void?deleteByEmployeeId(String?employeeId);void?updateEmployee(Employee?e);boolean?employeeExists(Employee?e);List<Employee>?findAll();void?deleteAll(); }接口的實現如下:
@Service?public?class?EmployeeServiceImpl?implements?EmployeeService?{@Autowiredprivate?EmployeeRepository?employeeRepository;@Overridepublic?Employee?saveEmployee(Employee?e)?{return?employeeRepository.save(e);}@Overridepublic?Employee?findByEmployeeId(String?employeeId)?{return?employeeRepository.findOne(employeeId);}@Overridepublic?void?deleteByEmployeeId(String?employeeId)?{employeeRepository.delete(employeeId);}@Overridepublic?void?updateEmployee(Employee?e)?{employeeRepository.save(e);}@Overridepublic?boolean?employeeExists(Employee?e)?{return?employeeRepository.exists(Example.of(e));}@Overridepublic?List<Employee>?findAll()?{return?employeeRepository.findAll();}@Overridepublic?void?deleteAll()?{employeeRepository.deleteAll();} }這里沒有什么特別的要注意的,下面我們將繼續討論最后一個難題——控制器!你可以在下面看到員工資源的控制器實現。
@RestController @RequestMapping("/employees") public?class?EmployeeController?{@Autowiredprivate?EmployeeService?employeeService;@RequestMapping(value?=?"/list/",?method?=?RequestMethod.GET)public?HttpEntity<List<Employee>>?getAllEmployees()?{List<Employee>?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<Employee>?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);} }?因此,對于上面實現的所有方法,我們將自己定位在Richardson成熟度模型的第二級,因為我們使用了HTTP動詞并實現了CRUD操作。現在,我們有了與數據進行交互的方法,并且可以使用Postman,我們可以如下圖3所示檢索資源,或者可以如下圖4所示添加新資源。?? ? ? ?
圖3 :檢索JSON中的部門列表
? ? ? ?圖4:JSON中添加新員工
HATEOAS即將來臨
?
絕大多數人都止步于此,因為通常情況下,對他們或Web服務而言,這已經就足夠了,但這不是我們在這里的原因。因此,如前所述,支持HATEOAS或超媒體驅動的站點的Web服務應該能夠提供有關如何使用和導航Web服務的信息,方法是包含與響應之間具有某種關系的鏈接。?
你可以將HATEOAS想象成一個路標。當你開車的時候,這些標志會指引你。例如,如果你需要到達機場,則只需遵循指示標志,如果你需要返回,則再次遵循指示標志就可以了,而且你一直知道你可以待在哪里、停車或開車等等。?
讓我們實現資源表示形式附帶的鏈接,我們必須通過擴展ResourceSupport來繼承add()方法來調整模型,這給我們提供一個不錯的選擇,可以為資源表示形式設置值,而無需添加任何新字段 。
@Document public?class?Employee?extends?ResourceSupport{...}?現在,讓我們開始創建鏈接。為此,Spring HATEOAS提供了一個Link對象來存儲這種信息,并提供CommandLinkBuilder來構建它。?
假設我們想要為員工id添加一個GET響應的鏈接。
@RequestMapping(value?=?"/employee/{id}",?method?=?RequestMethod.GET) public?HttpEntity<Employee>?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);} }如果你注意到以下幾個方法:
add():設置鏈接值的方法。
linkTo(Class controller):一個靜態導入的方法,該方法允許創建一個新的ControllerLinkBuilder,它的基類指向控制器類。
methodOn(Class controller,Object ... parameters):靜態導入的方法,它創建到控制器類的間接引用,從而能夠從該類調用方法并使用其返回類型。
withSelfRel():一種最終創建鏈接的方法,該鏈接默認具有指向自身的關系。
現在,GET將產生以下響應:
{"employeeId":?"5a6f67519fea6938e0196c4d","firstName":?"Ion","lastName":?"Pascari","age":?23,"_links":?{"self":?{"href":?"http://localhost:8080/employees/employee/5a6f67519fea6938e0196c4d"}} }響應不僅包含員工的詳細信息,還包含可在其中導航的自鏈接URL。_links代表資源表示的新設置值。
self代表鏈接指向的關系類型。在這種情況下,它是一個自引用超鏈接。也可能有其他類型的關系,例如指向另一個類(我們將在稍后介紹)。
href是標識資源的URL。
現在,假設我們要為部門列表添加指向GET響應的鏈接。在這里,事情變得越來越有趣,因為部門不僅指向自己,也指向員工,員工也指向自己和他們的列表。因此,讓我們看一下代碼:
@RequestMapping(value?=?"/list/",?method?=?RequestMethod.GET) public?HttpEntity<List<Department>>?getAllDepartments()?{List<Department>?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);} }?因此,此代碼將產生以下響應: {"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)建立在一起的:withRel(String rel):一種方法,該方法最終以指向給定rel的關系創建Link。
恭喜你, 到這里,我們可以說已經達到了Richardson成熟度模型的第3級,當然,我們之所以沒有這樣做,是因為我們需要對Web服務進行更多的檢查和改進,例如提供有關資源狀態或任何其他事物的鏈接,但是我們幾乎做到了!
你可以在此處獲得完整的GitHub源代碼:
https://github.com/theFaustus/EmployeeManager
希望你能喜歡,如果有不清楚的地方或其他意見,歡迎評論區留言告訴我們或者和我們討論。
推薦閱讀:另一種聲音:容器是不是未來? GitHub 疑遭中間人攻擊,最大暗網托管商再被黑! 漫畫:什么是 “模因” ? 1 分鐘抗住 10 億請求!某些 App 怎么做到的?| 原力計劃 2020,國產AI開源框架“亮劍”TensorFlow、PyTorch 探索比特幣獨特時間鏈、挖礦費用及場外交易的概念 真香,朕在看了!總結
以上是生活随笔為你收集整理的一文教你如何使用 MongoDB 和 HATEOAS 创建 REST Web 服务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 邱跃鹏:软硬件一体化、Serverles
- 下一篇: OPPO 正式发布 ColorOS 7,