REST:如何以“浅”的方式将 java 对象序列化为 JSON?
Posted
技术标签:
【中文标题】REST:如何以“浅”的方式将 java 对象序列化为 JSON?【英文标题】:REST: how to serialize a java object to JSON in a "shallow" way? 【发布时间】:2016-12-02 00:58:00 【问题描述】:假设我有以下 JPA 实体:
@Entity
public class Inner
@Id private Long id;
private String name;
// getters/setters
@Entity
public class Outer
@Id private Long id;
private String name;
@ManyToOne private Inner inner;
// getters/setters
Spring 和 java EE 都具有带有默认序列化程序的 REST 实现,这些序列化程序将在不进一步编码的情况下将实体编组到 JSON 或从 JSON 编组。但是在将 Outer
转换为 JSON 时,Spring 和 EE 都在其中嵌套了 Inner
的完整副本:
// Outer
"id": "1234",
"name": "MyOuterName",
"inner":
"id": "4321",
"name": "MyInnerName"
这是正确的行为,但对我的 Web 服务来说是有问题的,因为对象图可能会变得很深/很复杂,并且可能包含循环引用。有什么方法可以配置提供的编组器以“浅”方式编组 POJO/实体,而不必为每个序列化器创建自定义 JSON 序列化程序?一种适用于所有实体的自定义序列化程序就可以了。理想情况下,我会喜欢这样的:
// Outer
"id": "1234",
"name": "MyOuterName",
"innerId": "4321"
我还希望它能够将 JSON “解组”回等效的 java 对象。如果该解决方案同时适用于 Spring 和 java EE,则值得称赞。谢谢!
【问题讨论】:
这就是为什么在 REST API 中使用持久性实体可能不是一个好主意。看看这个answer。是否可以选择使用定制的 DTO? @CássioMazzochiMolin:我喜欢这种方法,但该项目现在已经相当成熟了。在这一点上,将所有现有服务(及其消费者)切换为使用 DTO 而不是实体是不切实际的,我不想“混搭”。 【参考方案1】:在序列化 Java 对象时,通过 FLEXJSON 库智能地包含/排除嵌套类层次结构。
flexjson.JSONSerializer
的示例展示了here
【讨论】:
正如我在问题中提到的,我不想为每个对象创建一个自定义序列化程序。 我绝对认为您可以在一个地方创建一个自定义序列化程序,使其扩展JSONSerializer
并放置一个通配符表达式并覆盖 serialize()
方法。不要指定include()
方法的用法。这将有助于确保serializer
在任何时候都不会包含嵌套类。然后您可以尝试调用它来序列化不同的对象。尝试合并它,如果它仍然不能解决您的目的,请忽略。【参考方案2】:
对于这个问题有两种解决方案。 1-使用jackson json视图 2-为内部实体创建两个映射类。其中一个包含自定义字段,另一个包含所有字段...
我认为 jackson json 视图是更好的解决方案 ...
【讨论】:
【参考方案3】:我遇到了同样的问题,最终在我的实体上使用杰克逊注释来控制序列化:
您需要的是 @JsonIdentityReference(alwaysAsId=true) 来指示 bean 序列化程序该引用应该只是一个 ID。你可以在我的 repo 上看到一个例子:
https://github.com/sashokbg/company-rest-service/blob/master/src/main/java/bg/alexander/model/Order.java
@OneToMany(mappedBy="order", fetch=FetchType.EAGER)
@JsonIdentityReference(alwaysAsId=true) // otherwise first ref as POJO, others as id
private Set<OrderDetail> orderDetails;
如果您想完全控制实体如何表示为 JSON,您可以使用 JsonView 来定义与您的视图相关的序列化字段。
@JsonView(Views.Public.class) 公共 int id;
@JsonView(Views.Public.class)
public String itemName;
@JsonView(Views.Internal.class)
public String ownerName;
http://www.baeldung.com/jackson-json-view-annotation
干杯!
【讨论】:
【参考方案4】:使用 jaxb @XmlID
和 @XmlIDREF
来解读复杂的对象图。
public class JSONTestCase
@XmlRootElement
public static final class Entity
private String id;
private String someInfo;
private DetailEntity detail;
@XmlIDREF
private DetailEntity detailAgain;
public Entity(String id, String someInfo, DetailEntity detail)
this.id = id;
this.someInfo = someInfo;
this.detail = detail;
this.detailAgain = detail;
// default constructor, getters, setters
public static final class DetailEntity
@XmlID
private String id;
private String someDetailInfo;
// constructors, getters, setters
@Test
public void testMarshalling() throws JAXBException
Entity e = new Entity( "42", "info", new DetailEntity("47","detailInfo") );
JAXBContext context = org.eclipse.persistence.jaxb.JAXBContextFactory.createContext(new Class[]Entity.class, null);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
m.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
m.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
m.marshal(e, System.out);
这将导致以下 json-fragment
"detailAgain" : "47",
"detail" :
"id" : "47",
"someDetailInfo" : "detailInfo"
,
"id" : "42",
"someInfo" : "info"
解组此 json 将确保 detail
和 detailAgain
是相同的实例。
这两个注解是 jaxb 的一部分,所以它可以在 Spring 和 java EE 中工作。编组到 json 不是标准的一部分,所以我在示例中使用了 moxy。
更新
在 JAX-RS 资源中不需要显式使用 moxy。以下片段完美地在 java-EE-7 容器(glassfish 4.1.1)上运行并生成上述 json-fragment:
@Stateless
@Path("/entities")
public class EntityResource
@GET
@Produces(MediaType.APPLICATION_JSON)
public Entity getEntity()
return new Entity( "42", "info", new DetailEntity("47","detailInfo") );
【讨论】:
这种方法需要我在每个端点手动创建一个 jaxb 编组器并使用它来代替 Spring/EE 中内置的编组器,这是很多额外的样板。如前所述,Java EE 和 Spring 都会自动编组/解组端点中的对象,无需额外代码,因此我想利用它。问题的重点是避免这种样板——否则我只会为每个对象创建一个自定义的杰克逊编组器。 换句话说,我不想用额外的代码弄乱我的端点——尤其是那些与手头的任务没有直接关系的代码。我也不想为每个类创建/注册自定义编组器,因为在它们演变时保持它们与对象同步会让人头疼。我只想改变提供的编组器的行为,使其不嵌入嵌套对象,而只是给出它们的 id。 显式使用 moxy 是这个简单测试用例的产物。默认情况下,Jax-RS 能够编组为 json。您不必手动创建 jaxb 编组器。只需将注释@XmlID
和 @XmlIDREF
添加到您的实体。
我试过了,但它似乎不起作用(也就是说,它继续为内部对象嵌套 JSON 而不仅仅是 ID)。我将@XmlID
添加到Inner.id
和@ XmlIDREF
到Outer.inner
。
@Alvin 我不知道为什么它不适合你。我在上面的文本中添加了一个示例 JAX-RS 资源,它在标准 glassfish 4.1.1 上运行良好。【参考方案5】:
在遇到许多问题后,我向 Cássio Mazzochi Molin 解释说“在 REST API 中使用实体持久性不是一个好主意”
我会这样做,业务层将持久性实体转换为 DTO。
您可以使用 mapstruct 之类的库轻松完成此操作
如果您仍想继续这种不好的做法,您可以使用jackson 和customize your jackson mapper
【讨论】:
正如我所提到的,我宁愿不必为每个实体创建自定义序列化程序。由于我们有很多实体并且它们经常变化,这将是一个令人头疼的维护问题。 使用我提到的库没问题。映射器由注释创建。您唯一应该做的就是重写注释。不过,编组库有什么用?你能用杰克逊吗? 我指的是您关于自定义杰克逊映射器的建议。 DTO 是一种很好的方法,但此时更改代码以使用它们为时已晚,因此 Mapstruct 不是入门者。【参考方案6】:您可以在序列化之前分离 JPA 实体,如果您使用延迟加载,则可以避免加载子对象。
另一种方式,但依赖于 JSON 序列化程序 API,您可以使用“瞬态”或具体注释。
Why does JPA have a @Transient annotation?
一个不好的方法是使用像推土机这样的工具将 JPA 对象复制到另一个类中,只需要 json 的属性(但它可以工作......内存、CPU 和时间的开销很小......)
@Entity
public class Outer
@Id private Long id;
private String name;
@ManyToOne private Inner inner;
//load manually inner.id
private final Long innerId;
// getters/setters
【讨论】:
我尝试了类似的方法,但是如果您在序列化之前分离实体,它不会序列化,而是在序列化程序尝试访问inner
时抛出“实体分离”异常。我还想过在inner
上使用@JsonIgnore
并添加一个单独的innerId
字段,类似于您的建议,但这需要每个类都需要大量样板代码才能使它们保持同步。
ok 第一种方法行不通。对不起。 EclipseLink 给出了它的解决方案eclipse.org/eclipselink/documentation/2.6/solutions/… 使用 mappedBy 避免循环引用并使用引用 (_link) 提供嵌套对象的 url
不幸的是,mappedBy
允许您指定在 JPA 中定义关系的位置,但不会影响实际对象。
也许创建一个 HATEOAS Rest 服务可以解决您的问题。 en.wikipedia.org/wiki/HATEOAS。我不知道客户端可以使用数据“innerId”:“4321”做什么,因此您也可以在 ManyToOne 上使用瞬态而不添加“innerId”以上是关于REST:如何以“浅”的方式将 java 对象序列化为 JSON?的主要内容,如果未能解决你的问题,请参考以下文章