1. 程式人生 > >將Spring實戰第5版中Spring HATEOAS部分程式碼遷移到Spring HATEOAS 1.0

將Spring實戰第5版中Spring HATEOAS部分程式碼遷移到Spring HATEOAS 1.0

       最近在閱讀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無法正常執行書中程式碼,所以我決定在此對書中的程式碼進行遷移升級。

線上閱讀書中這一部分:https://potoyang.gitbook.io/spring-in-action-v5/di-6-zhang-chuang-jian-rest-fu-wu/6.2-qi-yong-chao-mei-ti

 

Spring HATEOAS 1.0 版本的變化

封裝結構的最大變化是通過引入超媒體型別註冊API來實現的,以支援Spring HATEOAS中的其他媒體型別。這導致客戶端API和伺服器API(分別命名的包)以及包中的媒體型別實現的明確分離 mediatype

最大的變化就是將原來的資源表示為模型,具體變化如下。

ResourceSupportResourceResourcesPagedResources組類從來沒有真正感受到適當命名。畢竟,這些型別實際上並不表示資源,而是表示模型,可以通過超媒體資訊和提供的內容來豐富它們。這是新名稱對映到舊名稱的方式:

  • ResourceSupport 就是現在 RepresentationModel

  • Resource 就是現在 EntityModel

  • Resources 就是現在 CollectionModel

  • PagedResources 就是現在 PagedModel

因此,ResourceAssembler已被重新命名為RepresentationModelAssembler和及其方法toResource(…),並分別toResources(…)被重新命名為toModel(…)toCollectionModel(…)

。名稱更改也反映在中包含的類中TypeReferences

  • RepresentationModel.getLinks()現在公開了一個Links例項(通過List<Link>),該例項公開了其他API,以Links使用各種策略來連線和合並不同的例項。同樣,它已經變成了自繫結的泛型型別,以允許向例項新增連結的方法返回例項本身。

  • LinkDiscovererAPI已移動到client包。

  • LinkBuilderEntityLinksAPI已經被移到了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;
    }
程式清單6.5 能夠攜帶領域資料和超連結列表taco資源
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();
    }
}
程式清單6.6 裝配taco資源的資源裝配器
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;
}
IngredientResourceAssembler 物件
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);
    }

}

 

程式清單6.8 為Spring Data REST端點新增自定義的連結
    @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