最近在閱讀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使用各種策略來連接和合並不同的實例。同樣,它已經變成了自綁定的泛型類型,以允許向實例添加鏈接的方法返回實例本身。 -
該
LinkDiscovererAPI已移動到client包。 -
在
LinkBuilder和EntityLinksAPI已經被移到了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
