初探 Spring Hateoas

441 阅读3分钟

本文的内容参考于 An Intro to Spring HATEOAS

spring hateoas 是什么

Hateoas 全名(Hypertext as the Engine of Application state)

Spring Hateoas 项目是一个为API而服务的库,我们可以通过 Hateoas 简单方便的创建 REST 风格的API。

一般来说,Hateoas 设计原则是通过Server API 返回的信息中包含一些信息,可以告诉 Client 下一步可以进行的操作,这样,理论上可以实现当 Server API 发生改变时,Client 不需要修改。

下面通过一个通过构建一个示例看下 Hateoas 看看是如何实现客户端和服务端解耦的。

示例Demo

首先创建一个项目,这里使用了IDEA,可以看到右下角添加了 Spring HATEOAS 的依赖。

然后我们创建一个BO对象

public class Customer {

    private String customerId;

    private String customerName;

    private String companyName;

    // ...省略构造方法和Get/Set...
}

我们创建一个不使用 HATEOAS 的 CustomerController

@RestController
@RequestMapping(value = "/customers")
public class CustomerController {

    private final CustomerService customerService;

    public CustomerController(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GetMapping("/{customerId}")
    public Customer getCustomerById(@PathVariable String customerId) {
        return customerService.getCustomerDetail(customerId);
    }

}

CustomerService的代码如下

@Service
public class CustomerService {

    public Customer getCustomerDetail(String customerId) {
        if ("1".equals(customerId)) {
            return new Customer(customerId, "name-1", "company-1");
        }
        return null;
    }
}

此时请求API,返回的JSON应该如下

{
  "customerId": "1",
  "customerName": "name-1",
  "companyName": "company-1"
}

此时看下使用 HATEOAS 时,如何编写代码。

首先还是BO对象,需要继承RepresentationModel对象。

public class Customer extends RepresentationModel<Customer> {

    private String customerId;

    private String customerName;

    private String companyName;

    // ...省略构造方法和Get/Set...

}

Spring Hateoas 提供了一个Link对象存储元数据(例如URL链接),这时候修改一下CustomerContrller.getCustomerById()

@GetMapping("/{customerId}")
public Customer getCustomerById(@PathVariable String customerId) {
    Customer customer = customerService.getCustomerDetail(customerId);
    Link link = WebMvcLinkBuilder.linkTo(CustomerController.class)
            .slash(customer.getCustomerId())
            .withRel("self");
    customer.add(link);
    return customer;
}
  • linkTo : 表示包含的映射类
  • slash:追加在URL路径后的值
  • withRel:元数据对象的 key

这时候返回的数据json结构如下

{
  "customerId": "1",
  "customerName": "name-1",
  "companyName": "company-1",
  "_links": {
    "self": {
      "href": "http://localhost:8080/customers/1"
    }
  }
}

刚刚展示了单个Customer Bo的场景,实际的场景中往往会更复杂。现在添加一个Order对象

public class Order extends RepresentationModel<Order> {

    private String orderId;

    private Double price;

    private Integer quantity;

    // ...省略构造方法和Get/Set...

}

然后在CustomerController中添加一个获取某Customer所有Order的接口

@RestController
@RequestMapping(value = "/customers")
public class CustomerController {
    
	@GetMapping("/{customerId}/orders")
    public CollectionModel<Order> getOrdersByCustomer(@PathVariable final String customerId) {
        List<Order> orders = orderService.getAllOrdersByCustomer(customerId);
        for (Order o : orders) {
            Link link = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CustomerController.class)
                    .getOrderById(customerId, o.getOrderId())).withRel("self");
            o.add(link);
        }

        Link link = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CustomerController.class)
                .getOrdersByCustomer(customerId)).withRel("self");
        return CollectionModel.of(orders, link);
    }

    @GetMapping("/{customerId}/order/{orderId}")
    public Order getOrderById(@PathVariable final String customerId, @PathVariable final String orderId) {
        return null;
    }

}
  • methodOn(A.class).a() 可以绑定上a方法对应的 url 全路径

OrderService方法如下

public List<Order> getAllOrdersByCustomer(String customerId) {
    List<Order> orders = new ArrayList<>();
    if (customerId.equals("1")) {
        orders.add(new Order("1", 1.0, 10));
        orders.add(new Order("2", 2.0, 20));
    }
    return orders;
}

这时候请求getOrdersByCustomer接口的响应如下:

{
  "_embedded": {
    "orderList": [
      {
        "orderId": "1",
        "price": 1.0,
        "quantity": 10,
        "_links": {
          "self": {
            "href": "http://localhost:8080/customers/1/1/order"
          }
        }
      },
      {
        "orderId": "2",
        "price": 2.0,
        "quantity": 20,
        "_links": {
          "self": {
            "href": "http://localhost:8080/customers/1/2/order"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/customers/1/orders"
    }
  }
}

现在将整个的逻辑串起来。

首先有一个接口提供查询所有的客户, 返回的结果中,每一个Customer对象的link中,包含self的查询接口,假如Customer存在订单信息,还会包含查询Orders的link

@GetMapping
public CollectionModel<Customer> getAllCustomers() {
    List<Customer> all = customerService.getAll();
    for (Customer c : all) {
        String customerId = c.getCustomerId();
        Link selfLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CustomerController.class)
                .getCustomerById(customerId)).withRel("self");
        c.add(selfLink);
        if (orderService.getAllOrdersByCustomer(customerId).size() > 0) {
            Link allOrdersLink = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(CustomerController.class)
                    .getOrdersByCustomer(customerId)).withRel("allOrders");
            c.add(allOrdersLink);
        }
    }
    Link link = WebMvcLinkBuilder.linkTo(CustomerController.class).withRel("self");
    return CollectionModel.of(all, link);
}

请求的响应如下, 根据返回的link就可以知道,接下来client可以进行order的查询。

{
  "_embedded": {
    "customerList": [
      {
        "customerId": "1",
        "customerName": "name-1",
        "companyName": "company-1",
        "_links": {
          "self": {
            "href": "http://localhost:8080/customers/1"
          },
          "allOrders": {
            "href": "http://localhost:8080/customers/1/orders"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/customers"
    }
  }
}

这样当后端对api的路径修改时,不用再告诉client进行了哪种改动,因为client只需要调用href中的地址就好了,这样就对client和service进行了更好的解耦。

Demo Github 地址