說是解決,其實不是很完美的解決的,寫出來只是想記錄一下這個問題或者看一下有沒有哪位仁兄會的,能否知道一二。
下面說說出現問題:
問題是這樣的,當我查詢一個一對多的實體的時候,工具直接就爆了,差不多我就猜到是哪里死循環了,最后等了好久,查看原因,果然是堆溢出,再然后是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.將方法或者類上面的注解信息放入到緩存中去,而不用發每次都要提取一次
