最近在阅读Spring实战第五版中文版,书中第6章关于Spring HATEOAS部分代码使用的是Spring HATEOAS 0.25的版本,而最新的Spring HATEOAS 1.0对旧版的API做了升级,导致在使用新版Spring Boot(截至文章发布日最新的Spring Boot版本为2.2.4)加载的Spring HATEOAS 1.0.3无法正常运行书中代码,所以我决定在此对书中的代码进行迁移升级。
Spring HATEOAS 1.0 版本的变化
封装结构的最大变化是通过引入超媒体类型注册API来实现的,以支持Spring HATEOAS中的其他媒体类型。这导致客户端API和服务器API(分别命名的包)以及包中的媒体类型实现的明确分离 mediatype
。
最大的变化就是将原来的资源表示为模型,具体变化如下。
在ResourceSupport
/ Resource
/ Resources
/ PagedResources
组类从来没有真正感受到适当命名。毕竟,这些类型实际上并不表示资源,而是表示模型,可以通过超媒体信息和提供的内容来丰富它们。这是新名称映射到旧名称的方式:
ResourceSupport
就是现在RepresentationModel
Resource
就是现在EntityModel
Resources
就是现在CollectionModel
PagedResources
就是现在PagedModel
因此,ResourceAssembler
已被重命名为RepresentationModelAssembler
和及其方法toResource(…)
,并分别toResources(…)
被重命名为toModel(…)
和toCollectionModel(…)
。名称更改也反映在中包含的类中TypeReferences
。
RepresentationModel.getLinks()
现在公开了一个Links
实例(通过List<Link>
),该实例公开了其他API,以Links
使用各种策略来连接和合并不同的实例。同样,它已经变成了自绑定的泛型类型,以允许向实例添加链接的方法返回实例本身。该
LinkDiscoverer
API已移动到client
包。在
LinkBuilder
和EntityLinks
API已经被移到了server
包。ControllerLinkBuilder
已移入server.mvc
,不推荐使用替换WebMvcLinkBuilder
。RelProvider
已重命名为LinkRelationProvider
并返回LinkRelation
实例,而不是String
。VndError
已移至mediatype.vnderror
套件。
另外注意 ResourceProcessor 接口被 RepresentationModelProcessor 取代
更多变化请参考Spring HATEOAS文档:https://spring.io/projects/spring-hateoas
代码迁移升级
书中程序清单6.4 为资源添加超链接
@GetMapping("/recent")
public CollectionModel<EntityModel<Taco>> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending()); List<Taco> tacos = tacoRepo.findAll(page).getContent();
CollectionModel<EntityModel<Taco>> recentResources = CollectionModel.wrap(tacos); recentResources.add(
new Link("http://localhost:8080/design/recent", "recents"));
return recentResources;
}
消除URL硬编码
@GetMapping("/recent")
public CollectionModel<EntityModel<Taco>> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending()); List<Taco> tacos = tacoRepo.findAll(page).getContent();
CollectionModel<EntityModel<Taco>> recentResources = CollectionModel.wrap(tacos); recentResources.add(
linkTo(methodOn(DesignTacoController.class).recentTacos()).withRel("recents"));
return recentResources;
}
public class TacoResource extends RepresentationModel<TacoResource> { @Getter
private String name; @Getter
private Date createdAt; @Getter
private List<Ingredient> ingredients; public TacoResource(Taco taco) {
this.name = taco.getName();
this.createdAt = taco.getCreatedAt();
this.ingredients = taco.getIngredients();
}
}
public class TacoResourceAssembler extends RepresentationModelAssemblerSupport<Taco, TacoResource> {
/**
* Creates a new {@link RepresentationModelAssemblerSupport} using the given controller class and resource type.
*
* @param controllerClass DesignTacoController {@literal DesignTacoController}.
* @param resourceType TacoResource {@literal TacoResource}.
*/
public TacoResourceAssembler(Class<?> controllerClass, Class<TacoResource> resourceType) {
super(controllerClass, resourceType);
} @Override
protected TacoResource instantiateModel(Taco taco) {
return new TacoResource(taco);
} @Override
public TacoResource toModel(Taco entity) {
return createModelWithId(entity.getId(), entity);
}
}
之后对recentTacos()的调整
@GetMapping("/recentNew")
public CollectionModel<TacoResource> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent(); CollectionModel<TacoResource> tacoResources =
new TacoResourceAssembler(DesignTacoController.class, TacoResource.class).toCollectionModel(tacos); tacoResources.add(linkTo(methodOn(DesignTacoController.class)
.recentTacos())
.withRel("recents"));
return tacoResources;
}
创建 IngredientResource 对象
@Data
public class IngredientResource extends RepresentationModel<IngredientResource> {
public IngredientResource(Ingredient ingredient) {
this.name = ingredient.getName();
this.type = ingredient.getType();
} private final String name;
private final Ingredient.Type type;
}
public class IngredientResourceAssembler extends RepresentationModelAssemblerSupport<Ingredient, IngredientResource> {
/**
* Creates a new {@link RepresentationModelAssemblerSupport} using the given controller class and resource type.
*
* @param controllerClass IngredientController {@literal IngredientController}.
* @param resourceType IngredientResource {@literal IngredientResource}.
*/
public IngredientResourceAssembler(Class<?> controllerClass, Class<IngredientResource> resourceType) {
super(controllerClass, resourceType);
} @Override
protected IngredientResource instantiateModel(Ingredient entity) {
return new IngredientResource(entity);
} @Override
public IngredientResource toModel(Ingredient entity) {
return createModelWithId(entity.getId(), entity);
}
}
对 TacoResource 对象的修改
public class TacoResource extends RepresentationModel<TacoResource> {
private static final IngredientResourceAssembler
ingredientAssembler = new IngredientResourceAssembler(IngredientController.class, IngredientResource.class); @Getter
private String name; @Getter
private Date createdAt; @Getter
private CollectionModel<IngredientResource> ingredients; public TacoResource(Taco taco) {
this.name = taco.getName();
this.createdAt = taco.getCreatedAt();
this.ingredients = ingredientAssembler.toCollectionModel(taco.getIngredients()); }
}
程序清单6.7
@RepositoryRestController
public class RecentTacosController {
private TacoRepository tacoRepo; public RecentTacosController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
} /**
* 虽然@GetMapping映射到了“/tacos/recent”路径,但是类级别的@Repository RestController注解会确保这个路径添加
* Spring Data REST的基础路径作为前缀。按照我们的配置,recentTacos()方法将会处理针对“/api/tacos/recent”的GET请求。
* */
@GetMapping(path="/tacos/recent", produces="application/hal+json")
public ResponseEntity<CollectionModel<TacoResource>> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
List<Taco> tacos = tacoRepo.findAll(page).getContent(); CollectionModel<TacoResource> tacoResources =
new TacoResourceAssembler(DesignTacoController.class, TacoResource.class).toCollectionModel(tacos); tacoResources.add(
linkTo(methodOn(RecentTacosController.class).recentTacos())
.withRel("recents"));
return new ResponseEntity<>(tacoResources, HttpStatus.OK);
} }
@Bean
public RepresentationModelProcessor<PagedModel<EntityModel<Taco>>> tacoProcessor(EntityLinks links) { return new RepresentationModelProcessor<PagedModel<EntityModel<Taco>>>() {
@Override
public PagedModel<EntityModel<Taco>> process(PagedModel<EntityModel<Taco>> resource) {
resource.add(
links.linkFor(Taco.class)
.slash("recent")
.withRel("recents"));
return resource;
}
};
}
另一种写法
如果你觉得写使用资源装配器有点麻烦,那么你还可以采用这种方法。
@GetMapping("/employees")
public ResponseEntity<CollectionModel<EntityModel<Taco>>> findAll() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
List<EntityModel<Taco>> employees = StreamSupport.stream(tacoRepo.findAll(page).spliterator(), false)
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(DesignTacoController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(DesignTacoController.class).findAll()).withRel("employees")))
.collect(Collectors.toList()); return ResponseEntity.ok(
new CollectionModel<>(employees,
linkTo(methodOn(DesignTacoController.class).findAll()).withSelfRel()));
}
@GetMapping("/employees/{id}")
public ResponseEntity<EntityModel<Taco>> findOne(@PathVariable long id) { return tacoRepo.findById(id)
.map(employee -> new EntityModel<>(employee,
linkTo(methodOn(DesignTacoController.class).findOne(employee.getId())).withSelfRel(), //
linkTo(methodOn(DesignTacoController.class).findAll()).withRel("employees"))) //
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
参考来源:https://github.com/spring-projects/spring-hateoas-examples/tree/master/simplified
END