SpringHateoas超媒体API之资源表示与链接关系详解
作者:程序媛学姐
引言
在RESTful架构风格中,超媒体作为应用状态引擎(HATEOAS - Hypermedia As The Engine Of Application State)是一个重要概念,它使API客户端能够通过服务器提供的超媒体链接动态发现可用操作。Spring HATEOAS是Spring生态系统中的一个项目,专门用于帮助开发者构建符合HATEOAS约束的RESTful服务。
一、HATEOAS基本概念
HATEOAS是REST架构风格的一个关键约束,它强调API响应中应包含相关资源的链接。遵循HATEOAS原则的API允许客户端通过服务提供的链接导航整个API,而不必硬编码资源URL。这种方式提高了API的自描述性,减少了客户端与服务器之间的耦合,同时增强了API的可演化性。
在HATEOAS中,服务器响应不仅包含请求的数据,还包含与该数据相关的操作链接。例如,当获取一个用户资源时,响应可能同时包含修改、删除该用户的链接,以及查看该用户相关订单的链接。这种方式允许客户端无需事先了解API结构,而是通过跟随链接来发现可用操作。
Spring HATEOAS提供了一套工具和抽象,简化了在Spring MVC和Spring WebFlux应用中实现HATEOAS的过程。要使用Spring HATEOAS,首先需要添加相应的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>二、资源模型与表示
Spring HATEOAS提供了一套模型类来表示超媒体资源。在Spring HATEOAS中,资源被表示为包含数据和链接的对象。核心模型类包括:
2.1 EntityModel
EntityModel是一个包装类,可以将任何域对象包装成一个包含链接的资源:
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@RestController
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/users/{id}")
public EntityModel<User> getUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// 创建一个包含链接的EntityModel
return EntityModel.of(user,
linkTo(methodOn(UserController.class).getUser(id)).withSelfRel(),
linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"),
linkTo(methodOn(OrderController.class).getOrdersForUser(id)).withRel("orders"));
}
@GetMapping("/users")
public CollectionModel<EntityModel<User>> getAllUsers() {
// 实现获取所有用户的逻辑
// ...
}
}上述代码中,EntityModel.of()方法将一个User对象包装成一个EntityModel实例,并添加了三个链接:self链接(指向当前资源)、users链接(指向所有用户列表)和orders链接(指向当前用户的订单)。
2.2 CollectionModel
CollectionModel用于表示资源集合,可以包含集合级别的链接:
@GetMapping("/users")
public CollectionModel<EntityModel<User>> getAllUsers() {
List<User> users = userRepository.findAll();
List<EntityModel<User>> userModels = users.stream()
.map(user -> EntityModel.of(user,
linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel(),
linkTo(methodOn(OrderController.class).getOrdersForUser(user.getId())).withRel("orders")))
.collect(Collectors.toList());
return CollectionModel.of(userModels,
linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel());
}在这个例子中,我们创建了一个包含多个EntityModel<User>的CollectionModel,并为集合本身添加了self链接。
2.3 PagedModel
PagedModel是CollectionModel的扩展,专门用于表示分页资源:
@GetMapping("/users")
public PagedModel<EntityModel<User>> getUsers(Pageable pageable) {
Page<User> userPage = userRepository.findAll(pageable);
List<EntityModel<User>> userModels = userPage.getContent().stream()
.map(user -> EntityModel.of(user,
linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel()))
.collect(Collectors.toList());
PagedModel.PageMetadata metadata = new PagedModel.PageMetadata(
userPage.getSize(), userPage.getNumber(), userPage.getTotalElements(), userPage.getTotalPages());
return PagedModel.of(userModels, metadata,
linkTo(methodOn(UserController.class).getUsers(pageable)).withSelfRel());
}PagedModel包含分页元数据,如当前页码、页大小、总元素数和总页数,使客户端能够理解分页结构并导航到其他页面。
三、链接构建与关系
Spring HATEOAS提供了便捷的链接构建工具,使开发者能够轻松创建指向控制器方法的链接。主要的链接构建方式如下:
3.1 静态链接
使用linkTo()方法和控制器类引用创建静态链接:
Link usersLink = linkTo(UserController.class).slash("search").withRel("search");3.2 方法引用链接
使用methodOn()和linkTo()组合创建指向特定控制器方法的链接:
Link userLink = linkTo(methodOn(UserController.class).getUser(123L)).withSelfRel();
3.3 链接关系类型
Spring HATEOAS定义了常见的链接关系类型,如IanaLinkRelations.SELF、IanaLinkRelations.ITEM等:
Link selfLink = linkTo(methodOn(UserController.class).getUser(123L))
.withRel(IanaLinkRelations.SELF);也可以使用自定义关系类型:
Link ordersLink = linkTo(methodOn(OrderController.class).getOrdersForUser(123L))
.withRel("orders");四、资源装配器
对于复杂应用,可以创建专门的资源装配器(Assembler)来封装资源创建逻辑:
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@Component
public class UserModelAssembler implements RepresentationModelAssembler<User, EntityModel<User>> {
@Override
public EntityModel<User> toModel(User user) {
return EntityModel.of(user,
linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel(),
linkTo(methodOn(UserController.class).getAllUsers()).withRel("users"),
linkTo(methodOn(OrderController.class).getOrdersForUser(user.getId())).withRel("orders"));
}
}在控制器中使用装配器:
@RestController
public class UserController {
private final UserRepository userRepository;
private final UserModelAssembler assembler;
public UserController(UserRepository userRepository, UserModelAssembler assembler) {
this.userRepository = userRepository;
this.assembler = assembler;
}
@GetMapping("/users/{id}")
public EntityModel<User> getUser(@PathVariable Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
return assembler.toModel(user);
}
@GetMapping("/users")
public CollectionModel<EntityModel<User>> getAllUsers() {
List<User> users = userRepository.findAll();
return assembler.toCollectionModel(users);
}
}资源装配器提高了代码的可重用性和可维护性,特别是当多个控制器方法需要创建相同类型的资源表示时。
五、超媒体驱动的状态转换
在RESTful API中,资源的状态可以通过链接表示和操作。Spring HATEOAS可以根据资源的当前状态动态生成链接,实现超媒体驱动的状态转换:
@GetMapping("/orders/{id}")
public EntityModel<Order> getOrder(@PathVariable Long id) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
EntityModel<Order> orderModel = EntityModel.of(order,
linkTo(methodOn(OrderController.class).getOrder(id)).withSelfRel(),
linkTo(methodOn(OrderController.class).getAllOrders()).withRel("orders"));
// 根据订单状态添加不同的操作链接
if (order.getStatus() == Status.IN_PROGRESS) {
orderModel.add(linkTo(methodOn(OrderController.class)
.cancelOrder(id)).withRel("cancel"));
orderModel.add(linkTo(methodOn(OrderController.class)
.completeOrder(id)).withRel("complete"));
}
if (order.getStatus() == Status.COMPLETED) {
orderModel.add(linkTo(methodOn(OrderController.class)
.refundOrder(id)).withRel("refund"));
}
return orderModel;
}在这个例子中,根据订单的当前状态,API响应中包含不同的操作链接。这种方式使客户端能够理解资源的当前状态以及可执行的操作。
六、响应格式与内容协商
Spring HATEOAS默认使用HAL(Hypertext Application Language)格式来表示超媒体资源。HAL是一种JSON格式,定义了资源和链接的标准表示方式:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": {
"href": "http://example.com/api/users/123"
},
"users": {
"href": "http://example.com/api/users"
},
"orders": {
"href": "http://example.com/api/users/123/orders"
}
}
}除了HAL,Spring HATEOAS还支持其他超媒体类型,如HAL-FORMS、Collection+JSON和UBER。通过配置,可以支持内容协商,允许客户端请求不同格式的表示:
@Configuration
public class HateoasConfig {
@Bean
public RepresentationModelProcessorInvoker processorInvoker() {
return new RepresentationModelProcessorInvoker(Arrays.asList(
new HalFormsConfiguration()));
}
}客户端可以通过Accept头指定所需的媒体类型:
Accept: application/prs.hal-forms+json
总结
Spring HATEOAS为开发者提供了一套强大的工具和抽象,简化了符合HATEOAS约束的RESTful API的开发过程。
通过将域对象包装为包含链接的资源表示,API响应变得更加自描述,使客户端能够通过跟随链接来发现和使用API的功能。资源模型类(如EntityModel、CollectionModel和PagedModel)以及链接构建工具使开发者能够轻松创建丰富的超媒体API。
资源装配器进一步提高了代码的可重用性和可维护性,特别是在复杂应用中。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
