6.2.2 创建资源装配器

现在需要向列表中包含的 taco 资源添加链接。一种选择是循环遍历 Resources 对象中携带的每个 Resource<Taco> 元素,分别为每个元素添加一个链接。但是这有点乏味,无论在哪里返回 taco 资源列表,都需要在 API 中重复编写代码。

我们需要一个不同的策略。

将定义一个实用工具类,将 taco 对象转换为新的 TacoResource 对象,而不是让 Resources.wrap() 为列表中的每个 taco 创建一个资源对象。TacoResource 对象看起来很像 Taco,但是它也能够携带链接。下面程序清单显示了 TacoResource 的样子。

程序清单 6.5 携带域数据的 taco 资源和超链接列表数据
package tacos.web.api;

import java.util.Date;
import java.util.List;
import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Ingredient;
import tacos.Taco;

public class TacoResource extends ResourceSupport {
    
    @Getter
    private final String name;
    
    @Getter
    private final Date createdAt;
    
    @Getter
    private final List<Ingredient> ingredients;
    
    public TacoResource(Taco taco) {
        this.name = taco.getName();
        this.createdAt = taco.getCreatedAt();
        this.ingredients = taco.getIngredients();
    }
}

在很多方面,TacoResource 与 Taco 域类型并没有太大的不同。它们都有 name、createAt 和 ingredients 属性。但是 TacoResource 扩展了 ResourceSupport 以继承链接对象列表和管理链接列表的方法。

另外,TacoResource 不包含 Taco 的 id 属性。这是因为不需要在 API 中公开任何特定于数据库的 id。从 API 客户机的角度来看,资源的自链接将作为资源的标识符。

注意:

域和资源:分开还是放一起?一些 Spring 开发人员可能会选择通过扩展他们的域类型 ResourceSupport,来将他们的域类型和资源类型组合成单个类型,正确的方法没有对错之分。我选择创建一个单独的资源类型,这样 Taco 就不会在不需要链接的情况下不必要地与资源链接混杂在一起。另外,通过创建一个单独的资源类型,我可以很容易地去掉 id 属性,这样就不会在 API 中暴露它。

TacoResource 只有一个构造函数,它接受一个 Taco 并将相关属性从 Taco 复制到自己的属性。这使得将单个 Taco 对象转换为 TacoResource 变得很容易。但是,如果到此为止,仍然需要循环才能将 Taco 对象列表转换为 Resources<TacoResource>。

为了帮助将 Taco 对象转换为 TacoResource 对象,还需要创建一个资源装配器,如下程序清单所示。

程序清单 6.6 装配 taco 资源的资源装配器
package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import tacos.Taco;

public class TacoResourceAssembler extends ResourceAssemblerSupport<Taco, TacoResource> {
    
    public TacoResourceAssembler() {
        super(DesignTacoController.class, TacoResource.class);
    }
    
    @Override
    protected TacoResource instantiateResource(Taco taco) {
        return new TacoResource(taco);
    }
    
    @Override
    public TacoResource toResource(Taco taco) {
        return createResourceWithId(taco.getId(), taco);
    }
}

TacoResourceAssembler 有一个默认构造函数,它通知超类(ResourceAssemblySupport),在创建 TacoResource 时,它将使用 DesignTacoController 来确定它创建的链接中的任何 url 的基本路径。

重写 instantiateResource() 方法来实例化给定 Taco 的 TacoResource。如果 TacoResource 有一个默认的构造函数,那么这个方法是可选的。但是,在本例中,TacoResource 需要使用 Taco 进行构造,因此需要覆盖它。

最后,toResource() 方法是继承 ResourceAssemblySupport 时唯一严格要求的方法。这里,它从 Taco 创建一个 TacoResource 对象,并自动给它一个自链接,该链接的 URL 来自 Taco 对象的 id 属性。

从表面上看,toResource() 似乎具有与 instantiateResource() 类似的用途,但它们的用途略有不同。虽然 instantiateResource() 仅用于实例化资源对象,但 toResource() 不仅用于创建资源对象,还用于用链接填充它。在背后,toResource() 将调用 instantiateResource()。

现在调整 recentTacos() 方法来使用 TacoResourceAssembler:

@GetMapping("/recent")
public Resources<TacoResource> recentTacos() {
    PageRequest page = PageRequest.of(
        0, 12, Sort.by("createdAt").descending());
    
    List<Taco> tacos = tacoRepo.findAll(page).getContent();
    List<TacoResource> tacoResources = new TacoResourceAssembler().toResources(tacos);
    
    Resources<TacoResource> recentResources = new Resources<TacoResource>(tacoResources);
    recentResources.add(
        linkTo(methodOn(DesignTacoController.class).recentTacos())
        .withRel("recents"));
    
    return recentResources;
}

recentTacos() 现在不是返回一个 Resources<Resource<Taco>>,而是返回一个 Resources<TacoResource>,以利用新的 TacoResource 类型。从存储库获取 Taco 之后,将 Taco 对象列表传递给 TacoResourceAssembler 上的 toResources() 方法。这个方便的方法循环遍历所有 Taco 对象,然后调用在 TacoResourceAssembler 中覆盖的 toResource() 方法来创建 TacoResource 对象列表。

通过 TacoResource 列表,可以创建一个 Resources<TacoResource> 对象,然后使用 recentTacos() 以前版本中的 recents 链接填充它。

此时,对 /design/recent 接口的 GET 请求将生成一个 taco 列表,其中每个 taco 都有一个自链接和一个 recents 链接,但这些成分之间仍然没有联系。为了解决这个问题,你需要为原料创建一个新的资源装配器:

package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import tacos.Ingredient;

class IngredientResourceAssembler extends 
    ResourceAssemblerSupport<Ingredient, IngredientResource> {
    
    public IngredientResourceAssembler() {
        super(IngredientController2.class, IngredientResource.class);
    }
    
    @Override
    public IngredientResource toResource(Ingredient ingredient) {
        return createResourceWithId(ingredient.getId(), ingredient);
    }
    
    @Override
    protected IngredientResource instantiateResource(Ingredient ingredient) {
        return new IngredientResource(ingredient);
    }
}

如你所见,IngredientResourceAssembler 很像 TacoResourceAssembler,但它使用的是 Ingredient 和 IngredientResource 对象,而不是 Taco 和 TacoResource 对象。

说到 IngredientResource,它是这样的:

package tacos.web.api;

import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Ingredient;
import tacos.Ingredient.Type;

public class IngredientResource extends ResourceSupport {
    
    @Getter
    private String name;
    
    @Getter
    private Type type;
    
    public IngredientResource(Ingredient ingredient) {
        this.name = ingredient.getName();
        this.type = ingredient.getType();
    }
}

与 TacoResource 一样,IngredientResource 继承了 ResourceSupport 并将相关属性从域类型复制到它自己的属性集中(不包括 id 属性)。

剩下要做的就是对 TacoResource 做一些轻微的修改,这样它就会携带一个 IngredientResource 对象,而不是 Ingredient 对象:

package tacos.web.api;

import java.util.Date;
import java.util.List;
import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Taco;

public class TacoResource extends ResourceSupport {
    private static final IngredientResourceAssembler
        ingredientAssembler = new IngredientResourceAssembler();
    
    @Getter
    private final String name;
    
    @Getter
    private final Date createdAt;
    
    @Getter
    private final List<IngredientResource> ingredients;
    
    public TacoResource(Taco taco) {
        this.name = taco.getName();
        this.createdAt = taco.getCreatedAt();
        this.ingredients = ingredientAssembler.toResources(taco.getIngredients());
    }
}

这个新版本的 TacoResource 创建了一个 IngredientResourceAssembly 的静态实例,并使用它的 toResource() 方法将给定 Taco 对象的 Ingredient 列表转换为 IngredientResouce 列表。

最近的 tacos 列表现在完全嵌套了超链接,不仅是为它自己(recents 链接),而且为它所有的 tacos 数据和那些taco 的 ingredient 数据。响应应该类似于程序清单 6.3 中的 JSON。你可以在这里停下来,然后继续下一个话题。但首先我要解决程序清单 6.3 中一些令人困扰的问题。

最后更新于