jpa 多层嵌套一对多_JPA一对多循环引用的解决&&JackSon无限递归问题

本文探讨了在使用JPA时遇到的一对多嵌套导致的序列化问题,表现为Jackson的无限递归错误和堆溢出。作者尝试了几种解决方案,包括@JsonIgnore、单向多对一、动态过滤属性等,最终提出使用自定义注解结合Javassist动态创建类来解决循环引用问题,同时提到了SpringMVC中@RestController和@ResponseBody的序列化问题以及@JsonIdentityInfo和@JsonManagedReference/@JsonBackReference的使用。
摘要由CSDN通过智能技术生成

说是解决,其实不是很完美的解决的,写出来只是想记录一下这个问题或者看一下有没有哪位仁兄会的,能否知道一二。

下面说说出现问题:

问题是这样的,当我查询一个一对多的实体的时候,工具直接就爆了,差不多我就猜到是哪里死循环了,最后等了好久,查看原因,果然是堆溢出,再然后是jsckson的错误。那么必然是序列化的问题了。

这是jackson的错误:

at java.security.AccessController.doPrivileged(Native Method)

at java.net.URLClassLoader.findClass(URLClassLoader.java:354)

at java.lang.ClassLoader.loadClass(ClassLoader.java:425)

at java.lang.ClassLoader.loadClass(ClassLoader.java:412)

at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)

at java.lang.ClassLoader.loadClass(ClassLoader.java:358)

at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1617)

at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1547)

at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:691)

at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:157)

at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:656)

at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:675)

at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:157)

这是循环引用的错误:

严重: Servlet.service() for servlet [springDispatcherServlet] in context with path [/Shop] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError) (through reference chain: com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]->com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]->com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]->com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]->com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]->com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]->com.web.module.index.model.entity.Account["user"]->com.web.module.index.model.entity.User["accounts"]->org.hibernate.collection.internal.PersistentSet[0]-

j。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。还有很多的相同的错误

下面是两个实体:

User.java:

package com.web.module.index.model.entity;

import java.io.Serializable;

import java.util.HashSet;

import java.util.Set;

import javax.persistence.Entity;

import javax.persistence.FetchType;

import javax.persistence.Id;

import javax.persistence.OneToMany;

import javax.xml.bind.annotation.XmlAccessType;

import javax.xml.bind.annotation.XmlAccessorType;

import javax.xml.bind.annotation.XmlElement;

import javax.xml.bind.annotation.XmlRootElement;

import org.hibernate.validator.constraints.NotEmpty;

import com.fasterxml.jackson.annotation.JsonIgnore;

@XmlAccessorType(XmlAccessType.FIELD)

@XmlRootElement(name="user")

@Entity

public class User implements Serializable{

/**

*

*/

private static final long serialVersionUID = 1L;

@XmlElement

@Id

private String id;

/**

* validate适用于springmvc

*/

@XmlElement

//@NotEmpty

private String name;

@JsonIgnore

@OneToMany(mappedBy="user",targetEntity=Account.class,fetch=FetchType.EAGER)

private Set<Account> accounts=new HashSet<Account>();

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public Set<Account> getAccounts() {

return accounts;

}

public void setAccounts(Set<Account> accounts) {

this.accounts = accounts;

}

@Override

public String toString() {

return "User [id=" + id + ", name=" + name + ", accounts=" + accounts

+ "]";

}

}

Account.java:

package com.web.module.index.model.entity;

import java.io.Serializable;

import javax.persistence.CascadeType;

import javax.persistence.Entity;

import javax.persistence.FetchType;

import javax.persistence.Id;

import javax.persistence.JoinColumn;

import javax.persistence.ManyToOne;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity

public class Account implements Serializable{

/**

*

*/

private static final long serialVersionUID = 1L;

@Id

private String id;

private String code;

private String password;

@JsonIgnore

@JoinColumn(name="user_id")

@ManyToOne(targetEntity=User.class,fetch=FetchType.EAGER)

private User user;

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getCode() {

return code;

}

public void setCode(String code) {

this.code = code;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public User getUser() {

return user;

}

public void setUser(User user) {

this.user = user;

}

@Override

public String toString() {

return "Account [id=" + id + ", code=" + code + ", password="

+ password + ", user=" + user + "]";

}

}

后来去网上看了一下,这个问题很多人遇到。解决方案也有很多.

1.在关联的实体上面设置@JsonIgnore,这个注解的意思是表示在序列化的时候,忽略这个属性.但是我现在的逻辑是在页面中必须使用到这个关联实体中的属性,所以就不能这么做了,不然在页面中是取不出这个数据的。

Uncaught TypeError: Cannot read property 'name' of undefined(1,2都会出现)

2.采用单向多对一的形式,这样就不会出现循环的问题,这个确实是个方案,但是如果在一的那边需要使用到多的这边的话,就不好搞了。所以感觉还是不是很满意。

3.后来想了想,既然是这样,要不我在一的那边使用@JsonIgnore吧。目前在页面中没使用。其实这个是第二个是差不多的,有点不同的是除了页面展示的时候不能够显示多的那面的数据,在其他的业务中还是能够使用的。这也是我在前面说不是很满意的解决办法。

4.第四种解决就是前面的3差不多,当我们使用多的一边的时候,可以正确的显示,但是在我们使用一的那一端的时候,我们可以使用List自己拼装,有点像下面的代码:

@RequestMapping(value="result/{id}",method=RequestMethod.GET)public @ResponseBody List> result(@PathVariable("id") String id){

System.out.println(id);

List> list=Lists.newArrayList();//Map map=new HashMap();

Map map=null;

Random r=newRandom();

DecimalFormat dfmt=new DecimalFormat("#,###.00");for(int i=0;i<4;i++){int price=r.nextInt(10)+1;int number=r.nextInt(100000)+10000;

map=new HashMap();

map.put("tradegoods", "煤"+i);

map.put("units", "顿");

map.put("consumer", "XX物流"+id);

map.put("unitPrice", dfmt.format(price));

map.put("number", dfmt.format(number));

map.put("count", dfmt.format(price*number));

list.add(map);

}//设置日期格式

returnlist;

}

这样jackson序列化的时候,就不会出错了,而且使用起来就不用像A.B.name这样了,而且使用起来也更加的简单。我们在JS里面就可以这样使用:

if(id!=""&&id){

$.ajax({

type:'GET',

url: $ctx+ '/example/demo/result/'+id,

dataType:'json',

success: function(data) {for(var i=0;i

data[i].num=i+1;

}//alert(JSON.stringify(data));

viewModel.result(data);

$(".notice-hide").show();

$(".notice-show").hide();

},

error: function(req, textStatus, errorThrown){

}

});

html:

这样就完美的解决了这个问题。

5添加Filter的方式进行动态的过滤属性 ,上面的解决方法还是或多或少的影响到我们正常的使用类,下面说的方法是不会影响放到原有的类的。

jsckson的ObjectMapper有一个

public final void addMixInAnnotations(Class> target, Class>mixinSource)

{

_mixInAnnotations.put(newClassKey(target), mixinSource);

}

public final Class<?> findMixInClassFor(Class<?> cls) {

return (_mixInAnnotations == null) ? null : _mixInAnnotations.get(new ClassKey(cls));

}

public final int mixInCount() {

return (_mixInAnnotations == null) ? 0 : _mixInAnnotations.size();

}

这样的方法,这个方法的使用就要结合JsonIgnoreProperties注解一起来进行使用。我们需要定义一个接口,这个接口的作用是用来专门的过滤属性的。

还是针对上面的例子,我们要解决问题的话  ,我们需要在定义一个接口:

package com.hotusm.jackson;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown=true,value={"user"})public interfaceAccountFilter {

}

这个接口非常简单,就是一个注解,注解其中的value就是表示的是我们需要将那些属性给忽略掉,增加了这么一个接口后,我们就可以使用上面提到的方法。

objectMapper.addMixInAnnotations(Account.class, AccountFilter.class);

之后再使用这个objectmapper的时候,在account类上面的user就不会被忽略掉了,通过这种方式,我们不用修改原来类的任何地方。但是这种方式需要我们重新创建一个接口,所以下面一种就是解决这种每次都要创建的痛苦了。

6.利用自定义注解的方式来进行过滤,这种方式也是看到其他人使用,感觉非常好,也就做一个简单的总结。

大概的讲一下思路

1.还是使用addMixInAnnotations方法,但是不需要我们每次都创建一个接口而是采用全注解的形式来。也许会很奇怪,前面的方法命名

是传入两个class啊 ,我们不手动创建的话,那该怎样的去调用呢。这里我们使用字节码技术Javassist来动态的创建class。

2.大概的思路就是我们自定义方法级别注解,注解上面可以指定某些类上的哪些属性需要忽略。然后对这些方法进行增强,增强逻辑中获取到这些注解中的类以及这个类上面忽略的

下面是上面理论的一个简单的实践:

第一步:自定义注解:

package com.hotusm.jackson.annotation;

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Inherited;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target(ElementType.METHOD)

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Inherited

public@interface IgnoreProperty {/

* 指定类

*/Class>pojo();

/**

*指定上面的类那些属性需要过滤的

*/

String[] value();

}

上面这个注解就是我们后面要使用到的动态的在方法上面直接指定类需要忽略的属性。

第二步:对ObjectMapper进行装饰(写的例子,不是很优雅)

package com.hotusm.jackson.annotation;

import java.lang.reflect.Method;

import java.util.Collection;

import java.util.HashSet;

import javassist.CannotCompileException;

import javassist.ClassPool;

import javassist.CtClass;

import javassist.bytecode.AnnotationsAttribute;

import javassist.bytecode.ClassFile;

import javassist.bytecode.ConstPool;

import javassist.bytecode.annotation.Annotation;

import javassist.bytecode.annotation.ArrayMemberValue;

import javassist.bytecode.annotation.BooleanMemberValue;

import javassist.bytecode.annotation.MemberValue;

import javassist.bytecode.annotation.StringMemberValue;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import com.fasterxml.jackson.databind.ObjectMapper;

public classObjectMapperBuilder {

public ObjectMapper build(Method method) throws CannotCompileException{

IgnoreProperty ignoreProperty = method.getAnnotation(IgnoreProperty.class);

String[] value = ignoreProperty.value();

Class<?> pojo = ignoreProperty.pojo();

checkParamter(method,value,pojo);

Class<?> clazz=doBuild(value);

ObjectMapper objectMapper=new ObjectMapper();

objectMapper.addMixInAnnotations(pojo, clazz);

return objectMapper;

}

/**

* 根据传入的参数构造一个class

* @throws CannotCompileException

*/

public Class<?> doBuild(String[] values) throws CannotCompileException{

ClassPool pool = ClassPool.getDefault();

CtClass cc = pool.makeInterface("ProxyMixInAnnotation" + System.currentTimeMillis());

ClassFile classFile = cc.getClassFile();

ConstPool cp = classFile.getConstPool();

AnnotationsAttribute attr = new AnnotationsAttribute(cp,

AnnotationsAttribute.visibleTag);

Annotation jsonIgnorePropertiesAnnotation = new Annotation(

JsonIgnoreProperties.class.getName(), cp);

BooleanMemberValue ignoreUnknownMemberValue = new BooleanMemberValue(false, cp);

//

ArrayMemberValue arrayMemberValue = newArrayMemberValue(cp);

Collection memberValues = new HashSet();for(int i=0;i

StringMemberValue memberValue= new StringMemberValue(cp);//将name值设入注解内

memberValue.setValue(values[i]);

memberValues.add(memberValue);

}

arrayMemberValue.setValue(memberValues.toArray(newMemberValue[]{}));

jsonIgnorePropertiesAnnotation.addMemberValue("value", arrayMemberValue);

jsonIgnorePropertiesAnnotation.addMemberValue("ignoreUnknown", ignoreUnknownMemberValue);

attr.addAnnotation(jsonIgnorePropertiesAnnotation);

classFile.addAttribute(attr);

Class clazz=cc.toClass();returnclazz;

}protected voidcheckParamter(Object... objs){

boolean isTrue=true;if(objsnull||objs.length<=0){

isTrue=false;

}for(Object obj:objs){if(objnull){

isTrue=false;

}

}if(!isTrue){throw new RuntimeException("参数出现错误");

}

}

}

上面这一步我们已经看到了熟悉的addMixInAnnotations。后面的参数就是我们使用javassist根据value数组创建的动态类,这个动态类增加了一个很重要的注解就是JsonIgnoreProperties(这个注解就是我们6中讲的过滤属性的),现在通过build方法返回的ObjectMapper已经满足了动态的过滤属性的。

下面是一个测试:

@Test

@IgnoreProperty(pojo=Article.class,value={"user"})public voidtestJacksonAnnotation(){

User user=newUser();

user.setName("hotusm");

Article a1=newArticle();

a1.setTitle("t1");

a1.setUser(user);

Article a2=newArticle();

a2.setTitle("t2");

a2.setUser(user);

Article a3=newArticle();

a3.setTitle("t3");

a3.setUser(user);

List as=new ArrayList();as.add(a1);as.add(a2);as.add(a3);

user.setArticles(as);

ObjectMapper objectMapper;try{

objectMapper= new ObjectMapperBuilder().build(Main.class.getMethod("testJacksonAnnotation"));

String str=objectMapper.writeValueAsString(user);

System.out.println(str);

}catch(Exception e) {

e.printStackTrace();

}

}

在打印出来的json数据我们就可以明显的看出来已经把Article中的user属性给过滤掉了。(注意,user和article是一对多的关系)

总结:因为上面写的一个例子只是为了显示出问题,并没有进行代码的优化,以及功能的完善,如果是要在生产过程中使用的话,我们完全可以这样做:1.注解可以在类或者是方法上面2.所有多出来的操作都应该是对客户端程序员来说是透明的,我们可以通过方法的增强以及对ObjectMapper进行装饰。3.将方法或者类上面的注解信息放入到缓存中去,而不用发每次都要提取一次

2019-06-20更新

(1)在平时使用SpringMVC的时候也会出现这个问题,主要是SpringMVC的“@RestController”和“@ResponseBody”注解的序列化都调用了Jackson,因此只要是双向关系都会出现无限递归。感觉这个框架就是Hinbernate换了名字,我个人立场是十分不喜欢这种框架的。虽然看起来简单,不用写SQL但是,梳理关系和SQL的优化成本太高,不好干预。

(2)最好的解决方案使用JackSon2中的循环引用的序列化问题----使用"@JsonIdentityInfo"注解。这个注解如果每个实体的"property"的值都是"id"的话就会有问题,因此还得加一个"scope"属性才能正常使用,比如"@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = ServicioDTO.class))"。但是关于循环引用(recursive reference)问题如果用该annotion来解决的话要去映射关系中没有集合,有集合的话得采用其他方式,见下一条。。

(3)如果上面依旧不起作用,那么我们可以用"@JsonIgnore"注解来替代。这个方案就是方面3的方案,也经过试验了是可以的。

(4)针对映射实体中的复杂集合类型,可以用"@JsonManagedReference"和"@JsonBackReference"注解,这个也经过了试验是可以的.其实原理也比较相似就是将"@JsonBackReference"注解的对象序列化的时候忽略掉。Jackson看来对于递归引用的序列化问题还是没有彻底解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值