JPA一對多循環引用的解決&&JackSon無限遞歸問題


說是解決,其實不是很完美的解決的,寫出來只是想記錄一下這個問題或者看一下有沒有哪位仁兄會的,能否知道一二。

下面說說出現問題:

問題是這樣的,當我查詢一個一對多的實體的時候,工具直接就爆了,差不多我就猜到是哪里死循環了,最后等了好久,查看原因,果然是堆溢出,再然后是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</span><span style="color: #0000ff;">&lt;</span><span style="color: #800000;">Account</span><span style="color: #0000ff;">&gt;</span> accounts=new HashSet<span style="color: #0000ff;">&lt;</span><span style="color: #800000;">Account</span><span style="color: #0000ff;">&gt;</span><span style="color: #000000;">();

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</span><span style="color: #0000ff;">&lt;</span><span style="color: #800000;">Account</span><span style="color: #0000ff;">&gt;</span><span style="color: #000000;"> getAccounts() {
    return accounts;
}

public void setAccounts(Set</span><span style="color: #0000ff;">&lt;</span><span style="color: #800000;">Account</span><span style="color: #0000ff;">&gt;</span><span style="color: #000000;"> 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<Map<String,Object>> list=Lists.newArrayList();
        //Map<String,Object> map=new HashMap<String,Object>();
        Map<String,Object> map=null;
        Random r=new Random();
        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<String,Object>();
            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);
        }
        //設置日期格式  
        return list;
    }
復制代碼

這樣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.length;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:

復制代碼
                <tbody data-bind="foreach: result">
                <tr>
                    <td data-bind="text:num"></td>
                    <td data-bind="text:tradegoods"></td>
                    <td data-bind="text:units"></td>
                    <td data-bind="text:consumer"></td>
                    <td data-bind="text:unitPrice" class="format_"></td>
                    <td data-bind="text:number" class="format_"></td>
                    <td data-bind="text:count" class="format_"></td>
                </tr>
            </tbody>
復制代碼

這樣就完美的解決了這個問題。

 5添加Filter的方式進行動態的過濾屬性 ,上面的解決方法還是或多或少的影響到我們正常的使用類,下面說的方法是不會影響放到原有的類的。

jsckson的ObjectMapper有一個

復制代碼
    public final void addMixInAnnotations(Class<?> target, Class<?> mixinSource)
    {
        _mixInAnnotations.put(new ClassKey(target), mixinSource);
    }
</span><span style="color: #0000ff;">public</span> final Class&lt;?&gt; findMixInClassFor(Class&lt;?&gt;<span style="color: #000000;"> cls) {
    </span><span style="color: #0000ff;">return</span> (_mixInAnnotations == <span style="color: #0000ff;">null</span>) ? <span style="color: #0000ff;">null</span> : _mixInAnnotations.<span style="color: #0000ff;">get</span>(<span style="color: #0000ff;">new</span><span style="color: #000000;"> ClassKey(cls));
}

</span><span style="color: #0000ff;">public</span> final <span style="color: #0000ff;">int</span><span style="color: #000000;"> mixInCount() {
    </span><span style="color: #0000ff;">return</span> (_mixInAnnotations == <span style="color: #0000ff;">null</span>) ? <span style="color: #800080;">0</span><span style="color: #000000;"> : _mixInAnnotations.size();
}</span></pre>
復制代碼

這樣的方法,這個方法的使用就要結合JsonIgnoreProperties注解一起來進行使用。我們需要定義一個接口,這個接口的作用是用來專門的過濾屬性的。

還是針對上面的例子,我們要解決問題的話  ,我們需要在定義一個接口:

復制代碼
package com.hotusm.jackson;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

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

復制代碼

這個接口非常簡單,就是一個注解,注解其中的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();

</span><span style="color: #008000;">/*</span><span style="color: #008000;">*
 *指定上面的類那些屬性需要過濾的 
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
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 class ObjectMapperBuilder {

</span><span style="color: #0000ff;">public</span><span style="color: #000000;"> ObjectMapper build(Method method) throws CannotCompileException{
    IgnoreProperty ignoreProperty </span>= method.getAnnotation(IgnoreProperty.<span style="color: #0000ff;">class</span><span style="color: #000000;">);
    String[] value </span>=<span style="color: #000000;"> ignoreProperty.value();
    Class</span>&lt;?&gt; pojo =<span style="color: #000000;"> ignoreProperty.pojo();
    checkParamter(method,value,pojo);
    Class</span>&lt;?&gt; clazz=<span style="color: #000000;">doBuild(value);
    ObjectMapper objectMapper</span>=<span style="color: #0000ff;">new</span><span style="color: #000000;"> ObjectMapper();
    objectMapper.addMixInAnnotations(pojo, clazz);
    </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> objectMapper;
}
</span><span style="color: #008000;">/*</span><span style="color: #008000;">*
 * 根據傳入的參數構造一個class
 * @throws CannotCompileException 
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">public</span> Class&lt;?&gt;<span style="color: #000000;"> doBuild(String[] values) throws CannotCompileException{
    ClassPool pool </span>=<span style="color: #000000;"> ClassPool.getDefault();
    CtClass cc </span>= pool.makeInterface(<span style="color: #800000;">"</span><span style="color: #800000;">ProxyMixInAnnotation</span><span style="color: #800000;">"</span> +<span style="color: #000000;"> System.currentTimeMillis());
    ClassFile classFile </span>=<span style="color: #000000;"> cc.getClassFile();
    ConstPool cp </span>=<span style="color: #000000;"> classFile.getConstPool();
    AnnotationsAttribute attr </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> AnnotationsAttribute(cp,
            AnnotationsAttribute.visibleTag);
    Annotation jsonIgnorePropertiesAnnotation </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Annotation(
            JsonIgnoreProperties.</span><span style="color: #0000ff;">class</span><span style="color: #000000;">.getName(), cp);
    BooleanMemberValue ignoreUnknownMemberValue </span>= <span style="color: #0000ff;">new</span> BooleanMemberValue(<span style="color: #0000ff;">false</span><span style="color: #000000;">, cp);
    </span><span style="color: #008000;">//

ArrayMemberValue arrayMemberValue = new ArrayMemberValue(cp);
Collection
<MemberValue> memberValues = new HashSet<MemberValue>();
for(int i=0;i<values.length;i++){
StringMemberValue memberValue
= new StringMemberValue(cp);// 將name值設入注解內
memberValue.setValue(values[i]);
memberValues.add(memberValue);
}
arrayMemberValue.setValue(memberValues.toArray(
new MemberValue[]{}));
jsonIgnorePropertiesAnnotation.addMemberValue(
"value", arrayMemberValue);
jsonIgnorePropertiesAnnotation.addMemberValue(
"ignoreUnknown", ignoreUnknownMemberValue);
attr.addAnnotation(jsonIgnorePropertiesAnnotation);
classFile.addAttribute(attr);
Class clazz
= cc.toClass();
return clazz;
}
protected void checkParamter(Object... objs){
boolean isTrue
=true;
if(objsnull||objs.length<=0){
isTrue
=false;
}
for(Object obj:objs){
if(obj
null){
isTrue
=false;
}
}
if(!isTrue){
throw new RuntimeException("參數出現錯誤");
}
}
}

復制代碼

上面這一步我們已經看到了熟悉的addMixInAnnotations。后面的參數就是我們使用javassist根據value數組創建的動態類,這個動態類增加了一個很重要的注解就是JsonIgnoreProperties(這個注解就是我們6中講的過濾屬性的),現在通過build方法返回的ObjectMapper已經滿足了動態的過濾屬性的。

下面是一個測試:

復制代碼
@Test
    @IgnoreProperty(pojo=Article.class,value={"user"})
    public void testJacksonAnnotation(){
         User user=new User();
            user.setName("hotusm");
            Article a1=new Article();
            a1.setTitle("t1");
            a1.setUser(user);
            Article a2=new Article();
            a2.setTitle("t2");
            a2.setUser(user);
            Article a3=new Article();
            a3.setTitle("t3");
            a3.setUser(user);
            List<Article> as=new ArrayList<Article>();
            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看來對於遞歸引用的序列化問題還是沒有徹底解決。

原文地址:https://www.cnblogs.com/zr520/archive/2016/04/06/5357459.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM