在这篇文章中,我们将学习如何在Spring boot的restful web应用中实现hateoas。
超媒体是RESTful服务中最重要的方面。简而言之,超媒体是一种格式,其中一个资源的响应包含指向其他相关资源的链接。任何使用这种格式的系统都被称为超文本作为应用状态的引擎,或者简称为**HATEOAS**。
把一个网页看作是一种资源。它包含指向其他网页的链接。这就是我们所说的超文本页面(HTML)。现在想象一下,如果可以对JSON响应做同样的事情。这就是media type: application/hal+xml
和media type: application/hal+json
的作用。
有了这些新的超媒体格式,一个简单的订单对象将看起来像下面这样。
{
"orderId": 1,
"total": 54.99,
"_links": {
"self": {
"href": "http://localhost:8080/orders/1"
},
"account": {
"href": "http://localhost:8080/accounts/2"
},
"items": {
"href": "http://localhost:8080/orders/1/items"
}
}
}
Code language: JSON / JSON with Comments (json)
响应可能看起来很简单。收到这些响应的客户只需要按照链接的资源来获得他们想要的信息。但是对于一个服务器来说,要生成这些引用,它需要有适当的POJO,可以接受多个链接。一种在当前请求的上下文中引用其他控制器方法的方法等。
如果我们必须自己实现所有这些,我们最终会有一大堆代码逻辑。多亏了spring boot中的HATEOAS启动器,这项工作对开发者来说变得简单了。
让我们看看如何将上一篇文章中的例子转换为符合HATEOAS的RESTful服务。
这个设置真的很简单。在上一篇文章的源代码中,只需将spring-boot-starter-web
替换为spring-boot-starter-hateoas
。这一步非常简单,因为这个启动器内部只包含web启动器和spring-hateoas
库。这个库带有帮助方法和超文本应用语言(HAL)的模型。
现在maven部分已经完成,我们需要对我们的代码做一些修改。让我们回顾一下我们的项目控制器。如果不做任何改动,控制器的方法看起来如下。
@GetMapping("/")
List<Item> get() {
return itemRepository.findAll();
}
@GetMapping("/{itemId}")
Item get(@PathVariable Integer itemId) {
return itemRepository.findById(itemId).orElseThrow(RuntimeException::new);
}
Code language: CSS (css)
而这些控制器方法的响应是这样的
GET http://localhost:8080/items
[
{
"id": 1,
"itemName": "Shoe",
"price": 12.99
},
{
"id": 2,
"itemName": "Shirt",
"price": 8.99
}
]
Code language: JavaScript (javascript)
GET http://localhost:8080/items/2
{
"id": 2,
"itemName": "Shirt",
"price": 8.99
}
Code language: JavaScript (javascript)
为了给这些响应添加_links
,spring-hateoas
提供了两个主要的封装类。它们是EntityModel
和CollectionModel
。使用这些类,我们可以用几行代码添加相关链接。让我们看看我是如何做到的。
package com.springhow.examples.springboot.rest.controller;
import com.springhow.examples.springboot.rest.entities.Item;
import com.springhow.examples.springboot.rest.entities.repositories.ItemRepository;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@RestController
@RequestMapping("/items")
public class ItemController {
private final ItemRepository itemRepository;
public ItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
@GetMapping("/")
CollectionModel<EntityModel<Item>> get() {
List<EntityModel<Item>> items = itemRepository.findAll().stream().map(item -> EntityModel.of(item,
linkTo(methodOn(ItemController.class).get(item.getId())).withSelfRel(),
linkTo(methodOn(ItemController.class).get()).withRel("items")))
.collect(Collectors.toList());
return CollectionModel.of(items, linkTo(methodOn(ItemController.class).get()).withSelfRel());
}
@GetMapping("/{itemId}")
EntityModel<Item> get(@PathVariable Integer itemId) {
Item item = itemRepository.findById(itemId).orElseThrow(RuntimeException::new);
return EntityModel.of(item,
linkTo(methodOn(ItemController.class).get(itemId)).withSelfRel(),
linkTo(methodOn(ItemController.class).get()).withRel("items"));
}
}
Code language: JavaScript (javascript)
这里的linkTo
和methodOn
是来自hateoas库的静态方法,帮助处理生成URL。完成上述设置后,让我们来启动API。
GET http://localhost:8080/items/1
{
"id" : 1,
"itemName" : "Shoe",
"price" : 12.99,
"_links" : {
"self" : {
"href" : "http://localhost:8080/items/1"
},
"items" : {
"href" : "http://localhost:8080/items/"
}
}
}
Code language: JavaScript (javascript)
再举一个例子,一旦提交了购物车,我们就希望能创建一个订单。在这种情况下,购物车与已创建的订单没有直接关系。然而,我们可以通过HAL提供一个链接到创建的订单资源,就像这样。
@PutMapping("/{cartId}")
EntityModel<Cart> update(@PathVariable Integer cartId) {
Cart cartFromDB = cartRepository.findById(cartId).orElseThrow(RuntimeException::new);
cartFromDB.setStatus(CartStatus.SUBMITTED);
//Ignoring validation if the cart content has changed for simplicity.
OrderHeader orderHeader = orderService.saveOrder(cartFromDB);
return EntityModel.of(cartFromDB,
linkTo(methodOn(CartController.class).get(cartFromDB.getId())).withSelfRel(),
linkTo(methodOn(OrderController.class).getOrder(orderHeader.getId())).withRel("order"));
}
Code language: JavaScript (javascript)
注意,突出显示的linkTo方法是指一个不同的控制器。以前,这是不可能的。spring boot starter for hateoas的hateoas包有助于解决这些问题。
另外,请注意,底层的服务方法完全没有变化。就RESTfulness而言,POSTs仍然可以接受一个简单的POJO作为输入。