- Spring Boot進階:原理、實戰與面試題分析
- 鄭天民
- 1742字
- 2022-07-05 09:41:54
4.2.3 Spring HATEOAS案例分析
現在,讓我們從一個實戰案例切入,來演示如何使用Spring HATEOAS實現自解釋Web API的開發步驟。我們先來設計一個實體類,如代碼清單4-47所示。
代碼清單4-47 Employee類定義代碼
public class Employee { private final int id; private String firstName; private String lastName; private String role; //省略構造函數和getter/setter }
為了簡化演示過程,我們直接構建一個Service層組件EmployeeService,該組件內部使用一個數組來進行內存級別的數據管理,基本就是對Employee對象的CRUD,如代碼清單4-48所示。
代碼清單4-48 EmployeeService的CRUD操作代碼
@Service public class EmployeeService { private static final List<Employee> EMPLOYEES = new ArrayList<>(); private EmployeeService() { create(new Employee("FirstName1", "LastName1", "USER")); create(new Employee("FirstName2", "LastName2", "ADMIN")); } public List<Employee> findAll() { return EMPLOYEES; } public Employee findById(int id) { return EMPLOYEES.get(id); } public Employee findByName(String firstName, String lastName) { return EMPLOYEES.stream().filter(employee -> employee.getFirstName().equals(firstName) && employee.getLastName().equals(lastName)).findFirst().orElseThrow(() -> EmployeeNotFound.byName(firstName + " " + lastName)); } public Employee findByRole(String role) { return EMPLOYEES.stream().filter(employee -> employee.getRole().equals(role)).findFirst().orElseThrow(() -> EmployeeNotFound.byRole(role)); } public Employee create(Employee newEmployee) { Employee newlyCreatedEmployee = new Employee(EMPLOYEES.size(), newEmployee.getFirstName(), newEmployee.getLastName(), newEmployee.getRole()); EMPLOYEES.add(newlyCreatedEmployee); return newlyCreatedEmployee; } public Employee replace(Employee updatedEmployee, int id) { EMPLOYEES.remove(id); EMPLOYEES.add(id, updatedEmployee); return findById(id); } }
定義了領域實體以及Service層組件之后,接下來就可以對資源和鏈接進行有效的管理。
1. 創建資源和鏈接
我們先來看一個查詢單個Employee的示例。如果使用傳統的RESTful風格,我們可以創建一個PlainController,然后實現如代碼清單4-49所示的一組HTTP端點。
代碼清單4-49 PlainController類代碼
@RestController public class PlainController { private final EmployeeService employeeService; public PlainController(EmployeeService employeeService) { this.employeeService = employeeService; } @GetMapping("/plain/employees") public List<Employee> all() { return this.employeeService.findAll(); } @PostMapping("/plain/employees") public Employee create(@RequestBody Employee newEmployee) { return this.employeeService.create(newEmployee); } @GetMapping("/plain/employees/{id}") public Employee single(@PathVariable int id) { return this.employeeService.findById(id); } @PutMapping("/plain/employees/{id}") public Employee update(@RequestBody Employee updatedEmployee, @PathVariable int id) { return this.employeeService.replace(updatedEmployee, id); } }
可以看到,在未引入Spring HATEOAS時,HTTP端點返回的就是一個Employee對象?,F在,我們創建一個HypermediaController,并嘗試對PlainController中的single()方法進行重構,重構之后的結果如代碼清單4-50所示。
代碼清單4-50 HypermediaController類代碼
@RestController public class HypermediaController { private final EmployeeService employeeService; @GetMapping("/hypermedia/employees/{id}") public EntityModel<Employee> single(@PathVariable int id) { Link selfLink = linkTo(methodOn(HypermediaController.class).single(id)).withSelfRel(); Affordance update = afford(methodOn(HypermediaController.class).update(null, id)); Link aggregateRoot = linkTo(methodOn(HypermediaController.class).all()).withRel("employees"); return EntityModel.of(employeeService.findById(id), selfLink.andAffordance(update), aggregateRoot); } }
首先注意到上述single()方法的返回值是一個EntityModel對象。我們可以通過如代碼清單4-51所示的方法來構建一個EntityModel對象。
代碼清單4-51 創建EntityModel示例代碼
Employee employee = new Employee... EntityModel<Employee> model = EntityModel.of(employee);
當然,如果你想構建包含多個業務對象的CollectionModel,也可以采用類似的實現方式,如代碼清單4-52所示。
代碼清單4-52 創建CollectionModel示例代碼
Collection<Employee> employees = Collections...; CollectionModel<Employee> model = CollectionModel.of(employees);
另外,single()方法包含了一組創建超媒體資源常見的工具方法,其中methodOn()方法相當于為Controller創建了一個代理類,該代理類記錄Controller中指定方法的調用。通過methodOn()方法,我們知道需要為哪個方法創建鏈接,正如代碼清單4-50中methodOn(HypermediaController.class). single(id)這行代碼的作用對象是HypermediaController中的single()方法。
這里的linkTo()方法比較好理解,就是對methodOn()指定的目標方法創建一個鏈接。而withRel()方法用于定義鏈接關系的名稱。例如在代碼清單4-50中,我們將Hypermedia-Controller中的另一個all()方法命名為employees。對應地,withSelfRel()則使用默認的自鏈接(Self Link)關系為當前方法指定一個鏈接名稱。
注意,這里還存在一個Affordance對象,Affordance的字面意思就是“功能可見性”。換句話說,我們可以通過Affordances來展示Controller中的其他功能。在代碼清單4-50中,我們通過afford(methodOn(HypermediaController.class).update(null, id))語句告訴客戶端在HypermediaController中存在一個update()方法,這里的afford()方法會自動獲取該方法的HTTP請求方式以及請求參數,從而為客戶端提供調用該方法的有效途徑。
我們運行Spring Boot應用程序,并通過GET方法訪問http://localhost:8080/hypermedia/employees/{id}端點,得到的結果如代碼清單4-53所示。
代碼清單4-53 HTTP端點響應結果示例代碼
{ "id":1, "firstName":"FirstName2", "lastName":"LastName2", "role":"ADMIN", "_links":{ "self":{ "href":"http://localhost:8080/hypermedia/employees/1" }, "employees":{ "href":"http://localhost:8080/hypermedia/employees" } }, "_templates":{ "default":{ "method":"put", "properties":[ { "name":"firstName" }, { "name":"id", "readOnly":true }, { "name":"lastName" }, { "name":"role" } ] } } }
顯然,我們可以把上述結果拆分為三大部分,第一部分就是正常返回的一個Employee對象;第二部分則是_links段,分別針對當前請求自身以及根路徑提供了兩個鏈接;而第三部分則是_templates段,用來暴露當前Controller中所具備的HTTP方法為put的端點,即前面通過afford()方法所指定的update()方法。這里把該方法所應該傳遞的各個參數都列舉出來,從而提供了API的自解釋性。
2. 創建資源裝配器
很多時候,我們在Controller層嵌入各種HATEOAS相關的對象并不是一個很好的做法。因為從職責分離的角度講,Controller的作用是基于業務代碼暴露HTTP端點,而不應該過多關注API的表示形式?;谶@個考慮,Spring HATEOAS也提供了裝配器的概念。裝配器的作用就是把Link、Affordance等各種對象進行有效的組合。創建裝配器的過程也比較簡單,我們直接實現SimpleRepresentationModelAssembler接口即可,示例代碼如代碼清單4-54所示。
代碼清單4-54 SimpleRepresentationModelAssembler接口實現代碼
@Service public class HypermediaEmployeeAssembler implements SimpleRepresentationModelAss-embler<Employee> { @Override public void addLinks(EntityModel<Employee> resource) { int id = resource.getContent().getId(); Link selfLink = linkTo(methodOn(HypermediaController.class).single(id)).withSelfRel(); Affordance update = afford(methodOn(HypermediaController.class).update(null, id)); resource.add(selfLink.andAffordance(update)); resource.add(linkTo(methodOn(HypermediaController.class).all()).withRel("employees")); } @Override public void addLinks(CollectionModel<EntityModel<Employee>> resources) { resources.add(linkTo(methodOn(HypermediaController.class).all()).withSelfRel().andAffordance(afford(methodOn(HypermediaController.class).create(null)))); } }
可以看到,這里我們分別針對代表單個實體的EntityModel<Employee>以及代表實體組合的CollectionModel<EntityModel<Employee>>實現了對應的addLinks()方法。而在SimpleRepresentationModelAssembler的toModel()和toCollectionModel()方法中,就會調用這兩個addLinks()方法完成組裝操作。
現在,我們再回過頭來看HypermediaController,它的代碼就顯得非常簡潔。重構之后的完整版HypermediaController如代碼清單4-55所示。
代碼清單4-55 完整版HypermediaController類代碼
@RestController public class HypermediaController { private final EmployeeService employeeService; private final HypermediaEmployeeAssembler assembler; public HypermediaController(EmployeeService employeeService, HypermediaEmployee-Assembler assembler) { this.employeeService = employeeService; this.assembler = assembler; } @GetMapping("/hypermedia/employees") public CollectionModel<EntityModel<Employee>> all() { return assembler.toCollectionModel(employeeService.findAll()); } @PostMapping("/hypermedia/employees") public EntityModel<Employee> create(@RequestBody Employee newEmployee) { return assembler.toModel(employeeService.create(newEmployee)); } @GetMapping("/hypermedia/employees/{id}") public EntityModel<Employee> single(@PathVariable int id) { Link selfLink = linkTo(methodOn(HypermediaController.class).single(id)).withSelfRel(); Affordance update = afford(methodOn(HypermediaController.class).update(null, id)); Link aggregateRoot = linkTo(methodOn(HypermediaController.class).all()).withRel("employees"); return EntityModel.of(employeeService.findById(id), selfLink.andAffordance(update), aggregateRoot); } @PutMapping("/hypermedia/employees/{id}") public EntityModel<Employee> update(@RequestBody Employee updatedEmployee, @PathVariable int id) { return assembler.toModel(employeeService.replace(updatedEmployee, id)); } }
與該案例相關的源代碼都放在GitHub上,你可以自己嘗試訪問這些HTTP端點:https://github.com/tianminzheng/spring-boot-examples/tree/main/SpringHateoasExample。
- Oracle 11g從入門到精通(第2版) (軟件開發視頻大講堂)
- Python自動化運維快速入門
- Java Web開發之道
- SQL基礎教程(視頻教學版)
- 微信小程序開發解析
- Big Data Analytics
- Elasticsearch Server(Third Edition)
- Cybersecurity Attacks:Red Team Strategies
- Spring技術內幕:深入解析Spring架構與設計原理(第2版)
- JavaScript+jQuery網頁特效設計任務驅動教程
- Scala Functional Programming Patterns
- Learning Android Application Testing
- FusionCharts Beginner’s Guide:The Official Guide for FusionCharts Suite
- 深度學習的數學:使用Python語言
- Advanced C++