【问题标题】:Remove _embedded in HATEOAS link using spring-boot-2.2.1使用 spring-boot-2.2.1 删除 HATEOAS 链接中的 _embedded
【发布时间】:2020-05-11 02:21:20
【问题描述】:

我正在使用 spring-boot-2.2.1 和 spring-HATEOAS。超媒体链接工作正常,但 我在返回链接时看到_embedded属性,请在github中找到以下代码以供参考和项目here

端点:

a) 将返回 CollectionModel => localhost:8099/api/v1/capability/list/noembedded

b) 将返回 List> localhost:8099/api/v1/capability/list/

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-boot-unittest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-unittest</name>
    <description>Demo project for Spring Boot unit test</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-mongodb</artifactId>
            <version>4.1.4</version>
        </dependency>

        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>4.1.4</version>
        </dependency>
        <!-- Embedded  MongoDB for Testing -->
        <dependency>
            <groupId>de.flapdoodle.embed</groupId>
            <artifactId>de.flapdoodle.embed.mongo</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>1.2.21</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <!--<processor>com.querydsl.mongodb.morphia.MorphiaAnnotationProcessor</processor>-->
                            <processor>org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Controller.Java

@RestController
@RequestMapping(value = "/api/v1/capability")
@RequiredArgsConstructor
@CrossOrigin
public class CapabilityController {

    private final CapabilityService capabilityService;
    private final CapabilityResourceAssembler capabilityResourceAssembler;

    @GetMapping(value = "/list")
    public CollectionModel<EntityModel<Capability>> getAllCapabilities() {
       List<EntityModel<Capability>> capabilities = capabilityService.listCapabilities().stream()
               .map(capability -> new EntityModel<>(capability,
                       linkTo(methodOn(CapabilityController.class).getCapabilityById(capability.getId())).withRel("getThisCapability"),
                       linkTo(methodOn(CapabilityController.class).getAllCapabilities()).withRel("getAllCapabilities")
               )).collect(Collectors.toList());
        return new CollectionModel<>(capabilities);
    }
}

实际反应

{
  "_embedded": {
    "capabilityList": [
      {
        "id": "sample",
        "techStack": "Java",
        "numOfDevelopers": 25,
        "numOfAvailableDevelopers": 10,
        "_links": {
          "getThisCapability": {
            "href": "http://localhost:8099/api/v1/capability/sample"
          },
          "getAllCapabilities": {
            "href": "http://localhost:8099/api/v1/capability/list"
          },
          "deleteThisCapability": {
            "href": "http://localhost:8099/api/v1/capability/sample"
          },
          "createCapability": {
            "href": "localhost:8099/api/v1/capability"
          }
        }
      }
    ]
  }
}

预期响应:

[
  {
    "id": "sample",
    "techStack": "Java",
    "numOfDevelopers": 25,
    "numOfAvailableDevelopers": 10,
    "_links": {
      "getThisCapability": {
        "href": "http://localhost:8099/api/v1/capability/sample"
      },
      "getAllCapabilities": {
        "href": "http://localhost:8099/api/v1/capability/list"
      }
    }
  }
]

我试过了

spring.data.rest.defaultMediaType = 应用程序/json

spring.hateoas.use-hal-as-default-json-media-type=false

但我仍然无法在响应中看到_embedded 属性。 谁能帮我找出问题所在。

我在使用spring-boot-1.5.10 之前我可以在没有_embedded 的情况下正确呈现链接,请参考here

在主类中添加下面的注释后,如果我返回列表,它工作正常

@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)

localhost:9771/api/v1/capability/list

这会产生以下结果:

[
  {
    "capabilityId": "sample",
    "techStack": "Java",
    "numOfDevelopers": 25,
    "numOfAvailableDevelopers": 10,
    "_links": {
      "getThisCapability": {
        "href": "http://localhost:9771/api/v1/capability/sample"
      },
      "getAllCapabilities": {
        "href": "http://localhost:9771/api/v1/capability/list"
      }
    }
  }
]

很遗憾,它不适用于最新版本。 任何帮助将不胜感激。

【问题讨论】:

  • 示例代码似乎无法重现您问题中的行为。运行 SpringBootUnittestApplication 并访问 :8099/api/v1/capability/list 会返回一个空的 JSON 对象。我猜是因为数据存储是空的?您能否更新示例以使其尽可能小,例如通过删除对 Mongo 的依赖并对某些数据进行硬编码?
  • @AndyWilkinson 非常感谢您的回复。当然,让我更新 Github。
  • @AndyWilkinson 通过禁用 MongoDB 在两个 github 存储库中推送更改。请检查这两个项目的端点 localhost:/api/v1/capability/list/noembedded。在 spring-boot-unittest 中,使用 spring-boot-2.x 的 repo 链接将无法正确呈现,而在使用 spring-boot.15.x 的 spring-hypermedia 中,链接将被正确呈现。非常感谢您的帮助。

标签: java spring spring-boot spring-data-mongodb spring-hateoas


【解决方案1】:

如果我正确理解HAL specification,那将是无效的HAL,这就是为什么只要您在请求中返回CollectionModel,Spring HATEOAS 就不会产生此结果。请注意,该集合也可能具有链接,该链接位于 _links 属性中的 _embedded 属性旁边,如 this example document 中所述。

如果您真的想摆脱_embedded 属性并生成EntityModel&lt;T&gt;s 的列表,那么如果您修改代码以返回List&lt;EntityModel&lt;Capability&gt;&gt;,它应该可以工作。然后,您将丢失 Spring HATEOAS 生成的 _embedded_links 属性。这是您修改后的代码 sn-p:

@RestController
@RequestMapping(value = "/api/v1/capability")
@RequiredArgsConstructor
@CrossOrigin
public class CapabilityController {

    private final CapabilityService capabilityService;
    private final CapabilityResourceAssembler capabilityResourceAssembler;

    @GetMapping(value = "/list")
    public List<EntityModel<Capability>> getAllCapabilities() {
       List<EntityModel<Capability>> capabilities = capabilityService.listCapabilities().stream()
               .map(capability -> new EntityModel<>(capability,
                       linkTo(methodOn(CapabilityController.class).getCapabilityById(capability.getId())).withRel("getThisCapability"),
                       linkTo(methodOn(CapabilityController.class).getAllCapabilities()).withRel("getAllCapabilities")
               )).collect(Collectors.toList());
        return capabilities;
    }
}

我强烈建议您不要这样做,因为您会失去 Spring HATEOAS 的优势。

此外,我建议使用您的 CapabilityResourceAssembler 并通过使用 capabilityResourceAssembler.toModel(...) 创建 EntityModel&lt;Capability&gt; 实例,这样您就不必重复在 .map(...) 函数中实现的代码。

【讨论】:

  • 我知道这个解决方案。我已经将它用于我的一个 API。但是,如果我们将 EntityModel 用于列表,那么我们将失去 _links 以及您提到的。我想维护_links。
  • 目前这在 Spring HATEOAS 中似乎是不可能的。它还会与您返回的任何具有_links 属性的类(可能不是List)发生冲突。
  • 能否请您再次检查我的问题。我已经更新了它。它在旧版本上运行良好。
  • @VelNaga 查看您更新的问题和您提供的代码,我立即注意到您返回的是List&lt;Capability&gt;github.com/VelDeveloper/spring-hypermedia/blob/master/src/main/…),而不是Resources&lt;Capability&gt;,这将是返回的正确方法资源的收集。因此,在旧项目中,您已经完成了与我在上面提出的相同的操作 - 您应该无法在通过 GitHub 提供的代码中看到任何与集合相关的链接。你能检查一下吗?
  • Capability extends RecourseSupport 所以 List 表示资源列表,因为我不希望 _embedded 在响应中。如果我返回Resources&lt;Capability&gt;,那么我的回复将包含_embedded。在旧 API 中需要注意的主要事项是,我可以在响应中看到 _links,并且响应中的链接正确呈现,这在新 API 中不起作用。
【解决方案2】:

在使用 Spring Boot 1.5 时,您依赖于 Spring HATEOAS 的限制,这导致 Jackson ObjectMapper 被用作应用程序范围内的 ObjectMapper,该 HAL 特定于 HAL。这种污染意味着 HAL 格式在不应该被应用到响应时。

该限制已在 Spring HATEOAS 1.0 中得到解决,其 HAL 特定的 ObjectMapper 不再污染整个应用程序。如果您希望主应用程序 ObjectMapper 应用 HAL 样式的序列化,您可以通过自定义选择重新加入:

@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder,
                                 HypermediaMappingInformation mappingInformation) {
    ObjectMapper objectMapper = builder.build();
    mappingInformation.configureObjectMapper(objectMapper);
    return objectMapper;
}

虽然我认为上述方法可行,但我会回应 Daniel 在他们的回答中提出的关于响应格式和符合 HAL 规范的担忧。

【讨论】:

  • 它工作正常!!!非常感谢您的留言。
【解决方案3】:

我建议使用带有注释 @JsonValue 的集合类型字段扩展 CollectionModel,然后在您的汇编程序中覆盖 toCollectionModel 以返回自定义 CollectionModel

public class CustomCollectionModel<T> extends CollectionModel<CollectionModel<T>>  {
@JsonValue
private Collection<T> content;

protected CustomCollectionModel() {
    this(new ArrayList<>());
}

public CustomCollectionModel(Iterable<T> content, Link... links) {
    this(content, Arrays.asList(links));
}


public CustomCollectionModel(Iterable<T> content, Iterable<Link> links) {

    Assert.notNull(content, "Content must not be null!");

    this.content = new ArrayList<>();

    for (T element : content) {
        this.content.add(element);
    }

    this.add(links);
}}

然后在你的汇编器中

  @Override
public CustomCollectionModel toCollectionModel(Iterable<? extends T> entities) {
    List<EntityModel<T>> resourceList = new ArrayList<>();

    for (T entity : entities) {
        resourceList.add(toModel(entity));
    }
    return new CustomCollectionModel(resourceList);
}

【讨论】:

    【解决方案4】:

    这对您来说仍然是个问题吗? 我可以通过将 false 作为参数传递给 RepositoryRestConfiguration 类中的 useHalAsDefaultJsonMediaType 方法来删​​除 _embedded 属性。

    【讨论】:

      猜你喜欢
      • 2015-05-02
      • 2019-12-05
      • 2018-11-11
      • 2015-11-30
      • 1970-01-01
      • 2015-02-08
      • 2016-10-15
      • 1970-01-01
      • 2019-05-21
      相关资源
      最近更新 更多