英文:
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="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);
}
This returns something in the form of:
{
"_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",
},
"opinionImprovements" : [ ],
"sourcesJson" : [ ],
"makers" : [ {
"username" : "jeremieca",
"id" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807",
"avatarUrl" : "user-16",
"_links" : {
"self" : {
"href" : "http://some-api-link"
}
}
} ],
"modificationsCounter" : 1,
"originalBuyer" : "fd9b68f9-7c0c-4120-869c-c63d1680e7f0",
"updateTrace" : {
"createdOn" : "2020-05-25T08:12:56.846+0000",
"createdBy" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807",
"updatedOn" : "2020-05-25T08:12:56.845+0000",
"updatedBy" : "cfae1a04-cb00-4ad4-b4e8-6971eff64807"
},
"_links" : {
"self" : {
"href" : "some-api-link",
"templated" : true
},
"newEditions" : {
"href" : "some-api-link",
"templated" : true
},
"makers" : {
"href" : "http://some-api-link"
},
"originalMaker" : {
"href" : "http://some-api-link"
}
}
} ]
}
}
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:
"{content: [...], _links: []}"
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 = "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;
}
}
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,继承hateoas
的ResourceSupport
,通过这些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("_embedded")
private ResourcesEmbeddedListJson embedded;
//getters and setters
}
Embedded "wrapper"
public class ResourcesEmbeddedListJson extends ResourceSupport {
private Collection<T> content;
//getters and setters
}
or if you want to make it less ugly, there's this org.springframework.hateoas.client.Traverson
component.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论