官术网_书友最值得收藏!

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。

主站蜘蛛池模板: 阿合奇县| 进贤县| 高平市| 新宾| 中牟县| 东山县| 分宜县| 榆社县| 蓬溪县| 砚山县| 通化县| 观塘区| 郑州市| 定结县| 宁晋县| 股票| 汾阳市| 师宗县| 砚山县| 灵石县| 阿拉善左旗| 博客| 松阳县| 临泉县| 海丰县| 精河县| 博野县| 满城县| 武城县| 宜兴市| 阿合奇县| 蛟河市| 手机| 平塘县| 罗山县| 阳城县| 沂源县| 太和县| 平舆县| 彭州市| 泽普县|