JPA一对多循环引用的解决

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

下面说说出现问题:

问题是这样的,当我查询一个一对多的实体的时候,工具直接就爆了,差不多我就猜到是哪里死循环了,最后等了好久,查看原因,果然是堆溢出,再然后是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<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);
    }

    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 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();
    
    /**
     *指定上面的类那些属性需要过滤的 
     */
    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 {
    
    
    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 = 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(objs==null||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.将方法或者类上面的注解信息放入到缓存中去,而不用发每次都要提取一次

posted @ 2016-04-06 13:50  Hotsum  阅读(16195)  评论(0编辑  收藏  举报