为什么 ResponseBody 和 Jackson ObjectMapper 不返回相同的输出?

huangapple go评论91阅读模式
英文:

Why does ResponseBody and Jackson ObjectMapper don't return the same output?

问题

以下是您提供的代码的翻译部分:

我正在使用一个Spring Boot应用程序

在我的控制器中我有一个方法返回一些资源

@ResponseBody
@Transactional(rollbackFor = Exception.class)
@GetMapping(value="data/{itemId}/items", produces="application/json")
public Resources<DataExcerpt> listMyData(@PathVariable("debateId") UUID debateId)){

   List<DataExcerpt> dataExcerpts = dataService
            .listMyData(id)
            .stream()
            .map(d -> this.projectionFactory.createProjection(DataExcerpt.class, d))
            .collect(Collectors.toList());
    return new Resources<>(dataExcerpts);
}

这将返回以下形式的内容

{
  "_embedded" : {
    "items" : [ {
      "position" : {
        "name" : "Oui",
        "id" : "325cd3b7-1666-4c44-a55f-1e7cc936a3aa",
        "color" : "#51B63D",
        "usedForPositionType" : "FOR_CON"
      },
      "id" : "5aa48cfb-5505-43b6-b0a9-5481c895e2bf",
      "item" : [ {
        "index" : 0,
        "id" : "43c2dcd0-6bdb-43b0-be97-2a40b99bc753",
        "description" : {
          "id" : "021ad7cd-4bf1-4dce-9ea7-10980440a049",
          "title" : "Item description",
          "modificationCount" : 0
        }
      } ],
      "title" : "Item title",
      "originalMaker" : {
        "username" : "jeremieca",
        "id" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807",
        "avatarUrl" : "user-16",
        "_links" : {
          "self" : {
            "href" : "http://some-api-link"
          }
        }
      },
      "itemState" : {
        "itemState" : "LIVE",
      },
      ...
    } ]
  }
}

另一方面我还想在Redis中缓存这些类型的答案以避免每次都运行整个过程为此我使用Jackson的ObjectMapper将我的资源转换为字符串

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValueAsString(controller.listMyData(id)); // 与上面的函数相同

writeValueAsString的输出结构与第一个结构不同

"{content: [...], _links: []}"

因此当我从带有缓存内容的API返回时结构与控制器在没有缓存的情况下发送给我的结构不同

为什么会这样
Jackson无法正确将Resources Hateoas结构写入字符串吗
我漏掉了什么吗

编辑

以下是Resources.class的内容:

package org.springframework.hateoas;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.util.Assert;

@XmlRootElement(name = "entities")
public class Resources<T> extends ResourceSupport implements Iterable<T> {
    private final Collection<T> content;

    protected Resources() {
        this(new ArrayList(), (Link[])());
    }

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

    public Resources(Iterable<T> content, Iterable<Link> links) {
        Assert.notNull(content, "Content must not be null!");
        this.content = new ArrayList();
        Iterator var3 = content.iterator();

        while (var3.hasNext()) {
            T element = var3.next();
            this.content.add(element);
        }

        this.add(links);
    }

    public static <T extends Resource<S>, S> Resources<T> wrap(Iterable<S> content) {
        Assert.notNull(content, "Content must not be null!");
        ArrayList<T> resources = new ArrayList();
        Iterator var2 = content.iterator();

        while (var2.hasNext()) {
            S element = var2.next();
            resources.add(new Resource(element, new Link[0]));
        }

        return new Resources(resources, new Link[0]);
    }

    @XmlAnyElement
    @XmlElementWrapper
    @JsonProperty("content")
    public Collection<T> getContent() {
        return Collections.unmodifiableCollection(this.content);
    }

    public Iterator<T> iterator() {
        return this.content.iterator();
    }

    public String toString() {
        return String.format("Resources { content: %s, %s }", this.getContent(), super.toString());
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (obj != null && obj.getClass().equals(this.getClass())) {
            Resources<?> that = (Resources) obj;
            boolean contentEqual = this.content == null ? that.content == null : this.content.equals(that.content);
            return contentEqual ? super.equals(obj) : false;
        } else {
            return false;
        }
    }

    public int hashCode() {
        int result = super.hashCode();
        result += this.content == null ? 0 : 17 * this.content.hashCode();
        return result;
    }
}

谢谢。

英文:

I am using a Spring Boot application.

I have a method in my controller that returns some Resources:

    @ResponseBody
    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value=&quot;data/{itemId}/items&quot;, produces=&quot;application/json&quot;)
    public Resources&lt;DataExcerpt&gt; listMyData(@PathVariable(&quot;debateId&quot;) UUID debateId)){

       List&lt;DataExcerpt&gt; dataExcerpts = dataService
                .listMyData(id)
                .stream()
                .map(d -&gt; this.projectionFactory.createProjection(DataExcerpt.class, d))
                .collect(Collectors.toList());
        return new Resources&lt;&gt;(dataExcerpts);
    }

This returns something in the form of:

{
  &quot;_embedded&quot; : {
    &quot;items&quot; : [ {
      &quot;position&quot; : {
        &quot;name&quot; : &quot;Oui&quot;,
        &quot;id&quot; : &quot;325cd3b7-1666-4c44-a55f-1e7cc936a3aa&quot;,
        &quot;color&quot; : &quot;#51B63D&quot;,
        &quot;usedForPositionType&quot; : &quot;FOR_CON&quot;
      },
      &quot;id&quot; : &quot;5aa48cfb-5505-43b6-b0a9-5481c895e2bf&quot;,
      &quot;item&quot; : [ {
        &quot;index&quot; : 0,
        &quot;id&quot; : &quot;43c2dcd0-6bdb-43b0-be97-2a40b99bc753&quot;,
        &quot;description&quot; : {
          &quot;id&quot; : &quot;021ad7cd-4bf1-4dce-9ea7-10980440a049&quot;,
          &quot;title&quot; : &quot;Item description&quot;,
          &quot;modificationCount&quot; : 0
        }
      } ],
      &quot;title&quot; : &quot;Item title&quot;,
      &quot;originalMaker&quot; : {
        &quot;username&quot; : &quot;jeremieca&quot;,
        &quot;id&quot; : &quot;cfae1a04-cb00-4ad4-b4e8-6971eff64807&quot;,
        &quot;avatarUrl&quot; : &quot;user-16&quot;,
        &quot;_links&quot; : {
          &quot;self&quot; : {
            &quot;href&quot; : &quot;http://some-api-link&quot;
          }
        }
      },
      &quot;itemState&quot; : {
        &quot;itemState&quot; : &quot;LIVE&quot;,
      },
      &quot;opinionImprovements&quot; : [ ],
      &quot;sourcesJson&quot; : [ ],
      &quot;makers&quot; : [ {
        &quot;username&quot; : &quot;jeremieca&quot;,
        &quot;id&quot; : &quot;cfae1a04-cb00-4ad4-b4e8-6971eff64807&quot;,
        &quot;avatarUrl&quot; : &quot;user-16&quot;,
        &quot;_links&quot; : {
          &quot;self&quot; : {
            &quot;href&quot; : &quot;http://some-api-link&quot;
          }
        }
      } ],
      &quot;modificationsCounter&quot; : 1,
      &quot;originalBuyer&quot; : &quot;fd9b68f9-7c0c-4120-869c-c63d1680e7f0&quot;,
      &quot;updateTrace&quot; : {
        &quot;createdOn&quot; : &quot;2020-05-25T08:12:56.846+0000&quot;,
        &quot;createdBy&quot; : &quot;cfae1a04-cb00-4ad4-b4e8-6971eff64807&quot;,
        &quot;updatedOn&quot; : &quot;2020-05-25T08:12:56.845+0000&quot;,
        &quot;updatedBy&quot; : &quot;cfae1a04-cb00-4ad4-b4e8-6971eff64807&quot;
      },
      &quot;_links&quot; : {
        &quot;self&quot; : {
          &quot;href&quot; : &quot;some-api-link&quot;,
          &quot;templated&quot; : true
        },
        &quot;newEditions&quot; : {
          &quot;href&quot; : &quot;some-api-link&quot;,
          &quot;templated&quot; : true
        },
        &quot;makers&quot; : {
          &quot;href&quot; : &quot;http://some-api-link&quot;
        },
        &quot;originalMaker&quot; : {
          &quot;href&quot; : &quot;http://some-api-link&quot;
        }
      }
    } ]
  }
}

On the other end, I also want to cache these sort of answers inside Redis to avoid running the whole process every time. To do that, I am using Jackson's ObjectMapper to convert my Resources to a string

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValueAsString(controller.listMyData(id)); // the same function as above

writeValueAsString output's structure is different from the first one:

&quot;{content: [...], _links: []}&quot;

So, when I return from my API with the cache content, the structure is not the same from the structure the controller sends me without the cache.

Why is that?
Is Jackson not able to correctly write as string the Resources Hateoas structures?
Am I missing something?

EDIT:

Here is the Resources.class:

package org.springframework.hateoas;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.util.Assert;

@XmlRootElement(name = &quot;entities&quot;)
public class Resources&lt;T&gt; extends ResourceSupport implements Iterable&lt;T&gt; {
    private final Collection&lt;T&gt; content;

    protected Resources() {
        this(new ArrayList(), (Link[])());
    }

    public Resources(Iterable&lt;T&gt; content, Link... links) {
        this(content, (Iterable) Arrays.asList(links));
    }

    public Resources(Iterable&lt;T&gt; content, Iterable&lt;Link&gt; links) {
        Assert.notNull(content, &quot;Content must not be null!&quot;);
        this.content = new ArrayList();
        Iterator var3 = content.iterator();

        while (var3.hasNext()) {
            T element = var3.next();
            this.content.add(element);
        }

        this.add(links);
    }

    public static &lt;T extends Resource&lt;S&gt;, S&gt; Resources&lt;T&gt; wrap(Iterable&lt;S&gt; content) {
        Assert.notNull(content, &quot;Content must not be null!&quot;);
        ArrayList&lt;T&gt; resources = new ArrayList();
        Iterator var2 = content.iterator();

        while (var2.hasNext()) {
            S element = var2.next();
            resources.add(new Resource(element, new Link[0]));
        }

        return new Resources(resources, new Link[0]);
    }

    @XmlAnyElement
    @XmlElementWrapper
    @JsonProperty(&quot;content&quot;)
    public Collection&lt;T&gt; getContent() {
        return Collections.unmodifiableCollection(this.content);
    }

    public Iterator&lt;T&gt; iterator() {
        return this.content.iterator();
    }

    public String toString() {
        return String.format(&quot;Resources { content: %s, %s }&quot;, this.getContent(), super.toString());
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (obj != null &amp;&amp; obj.getClass().equals(this.getClass())) {
            Resources&lt;?&gt; that = (Resources) obj;
            boolean contentEqual = this.content == null ? that.content == null : this.content.equals(that.content);
            return contentEqual ? super.equals(obj) : false;
        } else {
            return false;
        }
    }

    public int hashCode() {
        int result = super.hashCode();
        result += this.content == null ? 0 : 17 * this.content.hashCode();
        return result;
    }
}

Thank you.

答案1

得分: 6

原因是当Spring为您的MVC或Spring Boot应用程序配置HATEOAS时,除其他事项外,它还会配置自定义的Jackson模块,用于处理API公开的Resources类和整个对象模型的序列化和反序列化过程。

如果您想获得类似的结果,可以尝试以下操作:

import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;

// ...

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jackson2HalModule());
objectMapper.writeValueAsString(controller.listMyData(id));
英文:

The reason is that when Spring configures your MVC or Spring Boot application with HATEOAS, among other things, it will configure custom Jackson modules for handling the serialization and deserialization process of the Resources class and the rest of the object model exposed by the API.

If you want to obtain a similar result, you can do something like the following:

import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;

// ...

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jackson2HalModule());
objectMapper.writeValueAsString(controller.listMyData(id));

答案2

得分: 0

以下是您要翻译的内容:

我的建议是提供POJO,继承hateoasResourceSupport,通过这些POJO进行(反)序列化,例如:

资源JSON(根元素)

public class ResourcesJson extends ResourceSupport {
    @JsonProperty("_embedded")
    private ResourcesEmbeddedListJson embedded;

    // 获取器和设置器
}

嵌入的“包装器”

public class ResourcesEmbeddedListJson extends ResourceSupport {
    private Collection<T> content;

    // 获取器和设置器
}

或者如果您想让它更加清晰,还有这个org.springframework.hateoas.client.Traverson组件。

英文:

My advice would be to provide POJOs, extending hateoas' ResourceSupport, through which the (de-)serialization would go through, eg.

ResourcesJson (the root element)

public class ResourcesJson extends ResourceSupport {
@JsonProperty(&quot;_embedded&quot;)
private ResourcesEmbeddedListJson  embedded;
//getters and setters
}

Embedded "wrapper"

public class ResourcesEmbeddedListJson extends ResourceSupport {
private Collection&lt;T&gt; content;
//getters and setters
}

or if you want to make it less ugly, there's this org.springframework.hateoas.client.Traverson component.

huangapple
  • 本文由 发表于 2020年8月14日 23:09:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/63415381.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定