在 Spring Boot 中为自定义控制器方法启用 HAL 序列化
Enable HAL serialization in Spring Boot for custom controller method
我正在尝试使用 spring-boot-starter-data-rest 构建 RESTful API 和 Spring 引导。有一些实体:帐户、交易、类别和用户 - 只是通常的东西。
当我通过默认生成的 API 在 http://localhost:8080/transactions 检索对象时,一切进展顺利,我得到一个列表,其中包含所有事务作为 JSON 对象,例如那个:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"_links": {
"self": {
"href": "http://localhost:8080/transactions/5"
},
"category": {
"href": "http://localhost:8080/transactions/5/category"
},
"account": {
"href": "http://localhost:8080/transactions/5/account"
}
}
}
但现在的目标是仅检索该 URL 下的最新交易,因为我不想序列化整个数据库 table。所以我写了一个控制器:
@Controller
public class TransactionController {
private final TransactionRepository transactionRepository;
@Autowired
public TransactionController(TransactionRepository transactionRepository) {
this.transactionRepository = transactionRepository;
}
// return the 5 latest transactions
@RequestMapping(value = "/transactions", method = RequestMethod.GET)
public @ResponseBody List<Transaction> getLastTransactions() {
return transactionRepository.findAll(new PageRequest(0, 5, new Sort(new Sort.Order(Sort.Direction.DESC, "date")))).getContent();
}
}
当我现在尝试访问 http://localhost:8080/transactions 时,有一个
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
因为用户和账户之间的循环引用。当我通过向用户中的帐户列表添加 @JsonBackReference 注释来解决此问题时,我可以检索交易列表,但只能使用这种 "classic" 格式:
{
"id": 5,
"amount": -4.5,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"account": {
"id": 2,
"name": "Account Tilman",
"owner": {
"id": 1,
"name": "Tilman"
},
"categories": [
{
"id": 1,
"name": "Groceries"
},
{
"id": 2,
"name": "Restaurant"
}
],
"users": [
{
"id": 1,
"name": "Tilman"
}
]
},
"category": {
"id": 2,
"name": "Restaurant"
}
}
不再有 HAL 链接,一切都由 jackson 直接序列化。我尝试添加
@EnableHypermediaSupport(type = HypermediaType.HAL)
到实体 类 但这并没有让我到任何地方。我只希望我的控制器 return 与生成的 API 相同的对象,使用 HAL _links 而不是序列化每个引用。有什么想法吗?
编辑:
OK,三思而后行,我意识到@EnableHypermediaSupport 注释必须添加到configuration,当然。这解决了循环引用的问题,我可以从用户中删除@JsonBackReference。但是只有对象本身的属性被序列化,没有_links部分:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza"
}
我知道我可以编写包装器 类 为我的所有实体扩展 ResourceSupport 但这似乎毫无意义。由于 spring-hateoas 能够使用自动创建的 REST 接口的 _link 部分神奇地生成表示,因此应该有一种方法可以 return 来自自定义控制器的相同表示,对吧?
您无需创建自己的控制器来限制查询结果或对结果进行排序。只需在您的存储库中创建一个 query method:
public interface TransactionRepository extends MongoRepository<Transaction, String> {
List<Transaction> findFirst10ByOrderByDateDesc();
}
Spring 数据 REST 将在 /transactions/search/findFirst10ByOrderByDateDesc
.
自动将其导出为 method resource
这里有很多方面:
我怀疑 /transactions
处的收集资源真的 return 是您描述的单个事务。这些表示 return 用于项目资源。
如果 TransactionRepository
已经是 PageableAndSortingRepository
可以通过扩展 API 根目录中公开的 URI 模板来调整集合资源 link 名为 transactions
。默认情况下,这是一个 page
、size
和 sort
参数。这意味着客户可以请求您已经公开的内容。
如果你想默认分页和排序选项,实现一个控制器是正确的方法。但是,要实现像 Spring 数据 REST 公开的表示,您至少需要 return ResourceSupport
的实例,因为这是注册 HAL 映射的类型。
如果你仔细想想,这里没有什么神奇的东西。普通实体没有任何 link,ResourcesSupport
和 Resource<T>
等类型允许您包装实体并根据需要用 link 丰富它。 Spring Data REST 基本上使用大量有关隐式可用的域和存储库结构的知识来为您做到这一点。您可以重复使用很多,如下所示。
这里有几个小助手需要注意:
PersistentEntityResourceAssembler
- 通常注入控制器方法。它以 Spring 数据 REST 方式呈现单个实体,这意味着指向托管类型的关联将呈现为 links 等
PagedResourcesAssembler
- 通常注入控制器实例。负责准备页面中包含的项目,可以选择使用专用的 ResourceAssembler
.
Spring Data REST 对页面的主要作用如下:
PersistentEntityResourceAssembler entityAssembler = …;
Resources<?> … = pagedResourcesAssembler.toResources(page, entityAssembler);
这基本上是使用 PagedResourcesAssembler
和 PersistentEntityResourceAssembler
来呈现项目。
返回那个 Resources
实例应该会给你预期的表示设计。
要在控制器中使用 PersistentEntityResourceAssembler,我们应该将其标记为@RepositoryRestController
@RestController
@RequestMapping("/categories")
@RepositoryRestController
public class CategoryController implements ValidableController {
// dependencies
@RequestMapping(method = POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PersistentEntityResource> create(@Valid @RequestBody CategoryForm category,
BindingResult validation,
PersistentEntityResourceAssembler resourceAssembler)
{
validate(validation);
Category entity = categoryConverter.convert(category);
entity = categoryService.save(entity);
return ResponseEntity.ok(resourceAssembler.toFullResource(entity));
}
它构建了相当不错的 HAL 样式响应
{
"createdTime": "2018-07-24T00:55:32.854",
"updatedTime": "2018-07-24T00:55:32.855",
"name": "cfvfcdfgdfdfdfs32",
"options": [
"aaa",
"bbb"
],
"_links": {
"self": {
"href": "http://localhost:8080/shop/categories/34"
},
"category": {
"href": "http://localhost:8080/shop/categories/34{?projection}",
"templated": true
},
"products": {
"href": "http://localhost:8080/shop/categories/34/products"
},
"categories": {
"href": "http://localhost:8080/shop/categories/34/categories{?projection}",
"templated": true
},
"parent": {
"href": "http://localhost:8080/shop/categories/34/parent{?projection}",
"templated": true
}
}
}
我正在尝试使用 spring-boot-starter-data-rest 构建 RESTful API 和 Spring 引导。有一些实体:帐户、交易、类别和用户 - 只是通常的东西。
当我通过默认生成的 API 在 http://localhost:8080/transactions 检索对象时,一切进展顺利,我得到一个列表,其中包含所有事务作为 JSON 对象,例如那个:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"_links": {
"self": {
"href": "http://localhost:8080/transactions/5"
},
"category": {
"href": "http://localhost:8080/transactions/5/category"
},
"account": {
"href": "http://localhost:8080/transactions/5/account"
}
}
}
但现在的目标是仅检索该 URL 下的最新交易,因为我不想序列化整个数据库 table。所以我写了一个控制器:
@Controller
public class TransactionController {
private final TransactionRepository transactionRepository;
@Autowired
public TransactionController(TransactionRepository transactionRepository) {
this.transactionRepository = transactionRepository;
}
// return the 5 latest transactions
@RequestMapping(value = "/transactions", method = RequestMethod.GET)
public @ResponseBody List<Transaction> getLastTransactions() {
return transactionRepository.findAll(new PageRequest(0, 5, new Sort(new Sort.Order(Sort.Direction.DESC, "date")))).getContent();
}
}
当我现在尝试访问 http://localhost:8080/transactions 时,有一个
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
因为用户和账户之间的循环引用。当我通过向用户中的帐户列表添加 @JsonBackReference 注释来解决此问题时,我可以检索交易列表,但只能使用这种 "classic" 格式:
{
"id": 5,
"amount": -4.5,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"account": {
"id": 2,
"name": "Account Tilman",
"owner": {
"id": 1,
"name": "Tilman"
},
"categories": [
{
"id": 1,
"name": "Groceries"
},
{
"id": 2,
"name": "Restaurant"
}
],
"users": [
{
"id": 1,
"name": "Tilman"
}
]
},
"category": {
"id": 2,
"name": "Restaurant"
}
}
不再有 HAL 链接,一切都由 jackson 直接序列化。我尝试添加
@EnableHypermediaSupport(type = HypermediaType.HAL)
到实体 类 但这并没有让我到任何地方。我只希望我的控制器 return 与生成的 API 相同的对象,使用 HAL _links 而不是序列化每个引用。有什么想法吗?
编辑: OK,三思而后行,我意识到@EnableHypermediaSupport 注释必须添加到configuration,当然。这解决了循环引用的问题,我可以从用户中删除@JsonBackReference。但是只有对象本身的属性被序列化,没有_links部分:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza"
}
我知道我可以编写包装器 类 为我的所有实体扩展 ResourceSupport 但这似乎毫无意义。由于 spring-hateoas 能够使用自动创建的 REST 接口的 _link 部分神奇地生成表示,因此应该有一种方法可以 return 来自自定义控制器的相同表示,对吧?
您无需创建自己的控制器来限制查询结果或对结果进行排序。只需在您的存储库中创建一个 query method:
public interface TransactionRepository extends MongoRepository<Transaction, String> {
List<Transaction> findFirst10ByOrderByDateDesc();
}
Spring 数据 REST 将在 /transactions/search/findFirst10ByOrderByDateDesc
.
这里有很多方面:
我怀疑
/transactions
处的收集资源真的 return 是您描述的单个事务。这些表示 return 用于项目资源。如果
TransactionRepository
已经是PageableAndSortingRepository
可以通过扩展 API 根目录中公开的 URI 模板来调整集合资源 link 名为transactions
。默认情况下,这是一个page
、size
和sort
参数。这意味着客户可以请求您已经公开的内容。如果你想默认分页和排序选项,实现一个控制器是正确的方法。但是,要实现像 Spring 数据 REST 公开的表示,您至少需要 return
ResourceSupport
的实例,因为这是注册 HAL 映射的类型。如果你仔细想想,这里没有什么神奇的东西。普通实体没有任何 link,
ResourcesSupport
和Resource<T>
等类型允许您包装实体并根据需要用 link 丰富它。 Spring Data REST 基本上使用大量有关隐式可用的域和存储库结构的知识来为您做到这一点。您可以重复使用很多,如下所示。这里有几个小助手需要注意:
PersistentEntityResourceAssembler
- 通常注入控制器方法。它以 Spring 数据 REST 方式呈现单个实体,这意味着指向托管类型的关联将呈现为 links 等PagedResourcesAssembler
- 通常注入控制器实例。负责准备页面中包含的项目,可以选择使用专用的ResourceAssembler
.
Spring Data REST 对页面的主要作用如下:
PersistentEntityResourceAssembler entityAssembler = …; Resources<?> … = pagedResourcesAssembler.toResources(page, entityAssembler);
这基本上是使用
PagedResourcesAssembler
和PersistentEntityResourceAssembler
来呈现项目。返回那个
Resources
实例应该会给你预期的表示设计。
要在控制器中使用 PersistentEntityResourceAssembler,我们应该将其标记为@RepositoryRestController
@RestController
@RequestMapping("/categories")
@RepositoryRestController
public class CategoryController implements ValidableController {
// dependencies
@RequestMapping(method = POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PersistentEntityResource> create(@Valid @RequestBody CategoryForm category,
BindingResult validation,
PersistentEntityResourceAssembler resourceAssembler)
{
validate(validation);
Category entity = categoryConverter.convert(category);
entity = categoryService.save(entity);
return ResponseEntity.ok(resourceAssembler.toFullResource(entity));
}
它构建了相当不错的 HAL 样式响应
{
"createdTime": "2018-07-24T00:55:32.854",
"updatedTime": "2018-07-24T00:55:32.855",
"name": "cfvfcdfgdfdfdfs32",
"options": [
"aaa",
"bbb"
],
"_links": {
"self": {
"href": "http://localhost:8080/shop/categories/34"
},
"category": {
"href": "http://localhost:8080/shop/categories/34{?projection}",
"templated": true
},
"products": {
"href": "http://localhost:8080/shop/categories/34/products"
},
"categories": {
"href": "http://localhost:8080/shop/categories/34/categories{?projection}",
"templated": true
},
"parent": {
"href": "http://localhost:8080/shop/categories/34/parent{?projection}",
"templated": true
}
}
}