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 将确保 detaildetailAgain 是相同的实例。

这两个注解是 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@ XmlIDREFOuter.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?的主要内容,如果未能解决你的问题,请参考以下文章

如何覆盖与响应 django rest framework 序列化程序一起返回的返回序列化程序对象

Spring Boot 深拷贝对象

Java深拷贝与浅拷贝

克隆_浅拷贝和深拷贝

Java 深拷贝浅拷贝 与 序列化

Java克隆方式避免频繁创建对象优化方案