彻底解决SSH架构中的Hibernate懒加载问题

       在客户端使用AJAX框架,服务端采用Struts2+Spring+Hibernate的架构中,经常会遇到Hibernate懒加载的问题 ,异常为:

 

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role


      相信大家经常可以在网上看到相关的技术文章,大致是在web.xml中加上openSessionFilter,代码如下:

  
    hibernateFilter
    org.springframework.orm.hibernate4.support.OpenSessionInViewFilter
  


 

        如果你真的亲自去写代码了,你会发现这个方法根本不管用,原因在于Struts2的JSON插件使用反射技术,会调用关联对象的get方法获取对象的实例,从而触发Hibernate查询数据,导致本异常。

       本文的系统所采用的架构为Hibernate4、Spring4、Struts2,客户端使用Ext JS4作为WEB界面框架,Ext JS4和Struts2交互使用JSON格式的数据,故使用了struts_json插件,Struts2的配置则采用convention插件,即注解的配置方式,Hibernate4采用JPA注解方式配置对象关系映射 ,先看一下POJO的关系映射

     1、MenuPanel,一的一方:

package com.mcs.user.pojo;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.annotations.Columns;

import com.mcs.pojo.base.GenericObject;

@Entity
@Table(name = "MENUPANEL")
public class MenuPanel extends GenericObject {
	private String text;
	private List menus=new ArrayList();

	public MenuPanel() {
	}

	public MenuPanel(String text) {
		this.text = text;
	}
	public MenuPanel(long id,String text) {
		this.text = text;
		super.setId(id+"");
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	@OneToMany(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY,mappedBy="menuPanel")
	public List getMenus() {
		return menus;
	}

	public void setMenus(List menus) {
		this.menus = menus;
	}

}

      2、menu多的一方:

package com.mcs.user.pojo;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import static org.hibernate.annotations.CascadeType.SAVE_UPDATE;
import org.hibernate.annotations.Cascade;

import com.mcs.pojo.base.GenericObject;


@Entity
@Table(name="MENU")
public class Menu extends GenericObject{
	private String text;
	private String url;
	private boolean leaf=true;//默认是叶子节点
	private MenuPanel menuPanel;
	private List children;
	private Menu menu;
	
	
	public Menu() {
	}

	public Menu(String text, String url) {
		super();
		this.text = text;
		this.url = url;
	}
	public Menu(long id,String text, String url,MenuPanel menuPanel) {
		super();
		super.setId(id+"");
		this.text = text;
		this.url = url;
		this.menuPanel=menuPanel;
	}
	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	@ManyToOne(cascade={CascadeType.PERSIST,CascadeType.REMOVE}, targetEntity=MenuPanel.class)
	@JoinColumn(name="menuPanelId",referencedColumnName="id",insertable=true,updatable=true)
	public MenuPanel getMenuPanel() {
		return menuPanel;
	}

	public void setMenuPanel(MenuPanel menuPanel) {
		this.menuPanel = menuPanel;
	}

	@Column(length=1000)
	public boolean isLeaf() {
		return leaf;
	}

	public void setLeaf(boolean leaf) {
		this.leaf = leaf;
	}

	@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="menu")
	public List getChildren() {
		return children;
	}

	public void setChildren(List children) {
		this.children = children;
	}

	@ManyToOne(cascade={CascadeType.PERSIST,CascadeType.MERGE}, targetEntity=Menu.class)
	@JoinColumn(name="parentId",referencedColumnName="id",insertable=true,updatable=true)
	public Menu getMenu() {
		return menu;
	}

	public void setMenu(Menu menu) {
		this.menu = menu;
	}
	
	
}

       manuPanel和menu是一对多的关系,manuPanel是一的一方,menu是多的一方,采用懒加载的方式,Struts2使用JSON插件进行JSON数据转换,配置如下

@Action(value="queryMenu",results = { @Result(type = "json",params={"root","pageBean"})})
	public String queryMenu() {
		
		menuPanels=menuPanelService.queryMenuPanel();
		this.pageBean.setRows(menuPanels);
		this.pageBean.setTotal(menuPanels.size());//不使用分页
		return "success";
	}

        让Struts2支持Hibernate懒加载有三种方式,第一种:配置JSON插件需要排除的字段,第二种:使用DTO或者VO对象,可以解决懒加载的问题,并且可以有效减少不相关的调用,当然,在项目中使用DTO或VO对象,取决于项目架构的,我个人比较推崇使用DTO或者VO,但发现在项目组很难实行,第三种:修改JSON转换插件,可以彻底解决懒加的问题。咱们先介绍第一种:

@Action(value="queryMenu",results = { @Result(type = "json",params={"root","pageBean","excludeProperties","^.*menus$,^.*children$"})})
	public String queryMenu() {
		
		menuPanels=menuPanelService.queryMenuPanel();
		this.pageBean.setRows(menuPanels);
		this.pageBean.setTotal(menuPanels.size());//不使用分页
		return "success";
	}


 

        上面这个代码我们用的是正则表达式进行字段匹配,要排除多个字段,用逗号隔开。好了,现在不会报错了,数据也能加载了,但是如果我们有很多POJO类都使用懒加载,每次都要这么配置,简直就是一件痛苦的事,有没有一劳永逸的解决办法呢,答案是当然有,我们通过对JSON插件进行修改,让Struts2_JSON插件支持Hibernate懒加载,这样,就不用每次在Action中配置排除属性了,

      首先,在protected void bean(Object object)方法中的Object value = accessor.invoke(object);后面加入以下代码

					Object value = accessor.invoke(object);
					//如果是PersistentBag类型的实例,则是Hibernate的持久化对象
					if (value instanceof PersistentBag) {
						// 判断bag对象是否已经初始化,如果没有初始化,则是懒加载的方式
						PersistentBag bag=(PersistentBag) value;
						boolean b=Hibernate.isInitialized(bag);
						if(b==false)
						{
							continue;							
						}
					}
					if (baseAccessor.isAnnotationPresent(JSONFieldBridge.class)) {....


     然后将protected void value(Object object, Method method)方法中的LOG.debug("Cyclic reference detected on " + object);改为

					//将object改为取class类名,避免Hibernate懒加载错误
					LOG.debug("Cyclic reference detected on " + clazz.getName());

 
  

     完整代码如下:

/*
 * $Id: JSONWriter.java 1436290 2013-01-21 11:37:16Z lukaszlenart $
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.struts2.json;

import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.struts2.json.annotations.JSON;
import org.apache.struts2.json.annotations.JSONFieldBridge;
import org.apache.struts2.json.annotations.JSONParameter;
import org.apache.struts2.json.bridge.FieldBridge;
import org.apache.struts2.json.bridge.ParameterizedBridge;
import org.apache.velocity.texen.util.PropertiesUtil;
import org.hibernate.Hibernate;
import org.hibernate.collection.internal.PersistentBag;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.StringCharacterIterator;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

/**
 * 

* Serializes an object into JavaScript Object Notation (JSON). If cyclic * references are detected they will be nulled out. *

*/ public class JSONWriter { private static final Logger LOG = LoggerFactory.getLogger(JSONWriter.class); /** * By default, enums are serialised as name=value pairs */ public static final boolean ENUM_AS_BEAN_DEFAULT = false; private static char[] hex = "0123456789ABCDEF".toCharArray(); private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE_IGNORE_HIERARCHY = new ConcurrentHashMap, BeanInfo>(); private static final ConcurrentMap, BeanInfo> BEAN_INFO_CACHE = new ConcurrentHashMap, BeanInfo>(); private StringBuilder buf = new StringBuilder(); private Stack stack = new Stack(); private boolean ignoreHierarchy = true; private Object root; private boolean buildExpr = true; private String exprStack = ""; private Collection excludeProperties; private Collection includeProperties; private DateFormat formatter; private boolean enumAsBean = ENUM_AS_BEAN_DEFAULT; private boolean excludeNullProperties; /** * @param object * Object to be serialized into JSON * @return JSON string for object * @throws JSONException */ public String write(Object object) throws JSONException { return this.write(object, null, null, false); } /** * @param object * Object to be serialized into JSON * @return JSON string for object * @throws JSONException */ public String write(Object object, Collection excludeProperties, Collection includeProperties, boolean excludeNullProperties) throws JSONException { this.excludeNullProperties = excludeNullProperties; this.buf.setLength(0); this.root = object; this.exprStack = ""; this.buildExpr = ((excludeProperties != null) && !excludeProperties .isEmpty()) || ((includeProperties != null) && !includeProperties.isEmpty()); this.excludeProperties = excludeProperties; this.includeProperties = includeProperties; this.value(object, null); return this.buf.toString(); } /** * Detect cyclic references */ protected void value(Object object, Method method) throws JSONException { if (object == null) { this.add("null"); return; } if (this.stack.contains(object)) { Class clazz = object.getClass(); // cyclic reference if (clazz.isPrimitive() || clazz.equals(String.class)) { this.process(object, method); } else { if (LOG.isDebugEnabled()) { //将object改为取class类名,避免Hibernate懒加载错误 LOG.debug("Cyclic reference detected on " + clazz.getName()); } this.add("null"); } return; } this.process(object, method); } /** * Serialize object into json */ protected void process(Object object, Method method) throws JSONException { this.stack.push(object); if (object instanceof Class) { this.string(object); } else if (object instanceof Boolean) { this.bool((Boolean) object); } else if (object instanceof Number) { this.add(object); } else if (object instanceof String) { this.string(object); } else if (object instanceof Character) { this.string(object); } else if (object instanceof Map) { this.map((Map) object, method); } else if (object.getClass().isArray()) { this.array(object, method); } else if (object instanceof Iterable) { this.array(((Iterable) object).iterator(), method); } else if (object instanceof Date) { this.date((Date) object, method); } else if (object instanceof Calendar) { this.date(((Calendar) object).getTime(), method); } else if (object instanceof Locale) { this.string(object); } else if (object instanceof Enum) { this.enumeration((Enum) object); } else { processCustom(object, method); } this.stack.pop(); } /** * Serialize custom object into json */ protected void processCustom(Object object, Method method) throws JSONException { this.bean(object); } /** * Instrospect bean and serialize its properties */ protected void bean(Object object) throws JSONException { this.add("{"); BeanInfo info; try { Class clazz = object.getClass(); info = ((object == this.root) && this.ignoreHierarchy) ? getBeanInfoIgnoreHierarchy(clazz) : getBeanInfo(clazz); PropertyDescriptor[] props = info.getPropertyDescriptors(); boolean hasData = false; for (PropertyDescriptor prop : props) { String name = prop.getName(); Method accessor = prop.getReadMethod(); Method baseAccessor = findBaseAccessor(clazz, accessor); if (baseAccessor != null) { if (baseAccessor.isAnnotationPresent(JSON.class)) { JSONAnnotationFinder jsonFinder = new JSONAnnotationFinder( baseAccessor).invoke(); if (!jsonFinder.shouldSerialize()) continue; if (jsonFinder.getName() != null) { name = jsonFinder.getName(); } } // ignore "class" and others if (this.shouldExcludeProperty(prop)) { continue; } String expr = null; if (this.buildExpr) { expr = this.expandExpr(name); if (this.shouldExcludeProperty(expr)) { continue; } expr = this.setExprStack(expr); } Object value = accessor.invoke(object); //如果是PersistentBag类型的实例,则是Hibernate的持久化对象 if (value instanceof PersistentBag) { // 判断bag对象是否已经初始化,如果没有初始化,则是懒加载的方式 PersistentBag bag=(PersistentBag) value; boolean b=Hibernate.isInitialized(bag); if(b==false) { continue; } } if (baseAccessor.isAnnotationPresent(JSONFieldBridge.class)) { value = getBridgedValue(baseAccessor, value); } boolean propertyPrinted = this.add(name, value, accessor, hasData); hasData = hasData || propertyPrinted; if (this.buildExpr) { this.setExprStack(expr); } } } // special-case handling for an Enumeration - include the name() as // a property */ if (object instanceof Enum) { Object value = ((Enum) object).name(); this.add("_name", value, object.getClass().getMethod("name"), hasData); } } catch (Exception e) { throw new JSONException(e); } this.add("}"); } protected BeanInfo getBeanInfoIgnoreHierarchy(final Class clazz) throws IntrospectionException { BeanInfo beanInfo = BEAN_INFO_CACHE_IGNORE_HIERARCHY.get(clazz); if (beanInfo != null) { return beanInfo; } beanInfo = Introspector.getBeanInfo(clazz, clazz.getSuperclass()); BEAN_INFO_CACHE_IGNORE_HIERARCHY.put(clazz, beanInfo); return beanInfo; } protected BeanInfo getBeanInfo(final Class clazz) throws IntrospectionException { BeanInfo beanInfo = BEAN_INFO_CACHE.get(clazz); if (beanInfo != null) { return beanInfo; } beanInfo = Introspector.getBeanInfo(clazz); BEAN_INFO_CACHE.put(clazz, beanInfo); return beanInfo; } protected Object getBridgedValue(Method baseAccessor, Object value) throws InstantiationException, IllegalAccessException { JSONFieldBridge fieldBridgeAnn = baseAccessor .getAnnotation(JSONFieldBridge.class); if (fieldBridgeAnn != null) { Class impl = fieldBridgeAnn.impl(); FieldBridge instance = (FieldBridge) impl.newInstance(); if (fieldBridgeAnn.params().length > 0 && ParameterizedBridge.class.isAssignableFrom(impl)) { Map params = new HashMap( fieldBridgeAnn.params().length); for (JSONParameter param : fieldBridgeAnn.params()) { params.put(param.name(), param.value()); } ((ParameterizedBridge) instance).setParameterValues(params); } value = instance.objectToString(value); } return value; } protected Method findBaseAccessor(Class clazz, Method accessor) { Method baseAccessor = null; if (clazz.getName().contains("$$EnhancerByCGLIB$$")) { try { baseAccessor = Thread.currentThread().getContextClassLoader() .loadClass( clazz.getName().substring(0, clazz.getName().indexOf("$$"))) .getMethod(accessor.getName(), accessor.getParameterTypes()); } catch (Exception ex) { LOG.debug(ex.getMessage(), ex); } } else if (clazz.getName().contains("$$_javassist")) { try { baseAccessor = Class.forName( clazz.getName().substring(0, clazz.getName().indexOf("_$$"))).getMethod( accessor.getName(), accessor.getParameterTypes()); } catch (Exception ex) { LOG.debug(ex.getMessage(), ex); } } else { return accessor; } return baseAccessor; } /** * Instrospect an Enum and serialize it as a name/value pair or as a bean * including all its own properties */ protected void enumeration(Enum enumeration) throws JSONException { if (enumAsBean) { this.bean(enumeration); } else { this.string(enumeration.name()); } } protected boolean shouldExcludeProperty(PropertyDescriptor prop) throws SecurityException, NoSuchFieldException { String name = prop.getName(); return name.equals("class") || name.equals("declaringClass") || name.equals("cachedSuperClass") || name.equals("metaClass"); } protected String expandExpr(int i) { return this.exprStack + "[" + i + "]"; } protected String expandExpr(String property) { if (this.exprStack.length() == 0) { return property; } return this.exprStack + "." + property; } protected String setExprStack(String expr) { String s = this.exprStack; this.exprStack = expr; return s; } protected boolean shouldExcludeProperty(String expr) { if (this.excludeProperties != null) { for (Pattern pattern : this.excludeProperties) { if (pattern.matcher(expr).matches()) { if (LOG.isDebugEnabled()) { LOG.debug("Ignoring property because of exclude rule: " + expr); } return true; } } } if (this.includeProperties != null) { for (Pattern pattern : this.includeProperties) { if (pattern.matcher(expr).matches()) { return false; } } if (LOG.isDebugEnabled()) { LOG .debug("Ignoring property because of include rule: " + expr); } return true; } return false; } /** * Add name/value pair to buffer */ protected boolean add(String name, Object value, Method method, boolean hasData) throws JSONException { if (excludeNullProperties && value == null) { return false; } if (hasData) { this.add(','); } this.add('"'); this.add(name); this.add("\":"); this.value(value, method); return true; } /** * Add map to buffer */ protected void map(Map map, Method method) throws JSONException { this.add("{"); Iterator it = map.entrySet().iterator(); boolean warnedNonString = false; // one report per map boolean hasData = false; while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); if (excludeNullProperties && entry.getValue() == null) { continue; } Object key = entry.getKey(); if (key == null) { LOG.error("Cannot build expression for null key in #0", exprStack); continue; } String expr = null; if (this.buildExpr) { expr = this.expandExpr(key.toString()); if (this.shouldExcludeProperty(expr)) { continue; } expr = this.setExprStack(expr); } if (hasData) { this.add(','); } hasData = true; if (!warnedNonString && !(key instanceof String)) { if (LOG.isWarnEnabled()) { LOG .warn( "JavaScript doesn't support non-String keys, using toString() on #0", key.getClass().getName()); } warnedNonString = true; } this.value(key.toString(), method); this.add(":"); this.value(entry.getValue(), method); if (this.buildExpr) { this.setExprStack(expr); } } this.add("}"); } /** * Add date to buffer */ protected void date(Date date, Method method) { JSON json = null; if (method != null) json = method.getAnnotation(JSON.class); if (this.formatter == null) this.formatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT); DateFormat formatter = (json != null) && (json.format().length() > 0) ? new SimpleDateFormat( json.format()) : this.formatter; this.string(formatter.format(date)); } /** * Add array to buffer */ protected void array(Iterator it, Method method) throws JSONException { this.add("["); boolean hasData = false; for (int i = 0; it.hasNext(); i++) { String expr = null; if (this.buildExpr) { expr = this.expandExpr(i); if (this.shouldExcludeProperty(expr)) { it.next(); continue; } expr = this.setExprStack(expr); } if (hasData) { this.add(','); } hasData = true; this.value(it.next(), method); if (this.buildExpr) { this.setExprStack(expr); } } this.add("]"); } /** * Add array to buffer */ protected void array(Object object, Method method) throws JSONException { this.add("["); int length = Array.getLength(object); boolean hasData = false; for (int i = 0; i < length; ++i) { String expr = null; if (this.buildExpr) { expr = this.expandExpr(i); if (this.shouldExcludeProperty(expr)) { continue; } expr = this.setExprStack(expr); } if (hasData) { this.add(','); } hasData = true; this.value(Array.get(object, i), method); if (this.buildExpr) { this.setExprStack(expr); } } this.add("]"); } /** * Add boolean to buffer */ protected void bool(boolean b) { this.add(b ? "true" : "false"); } /** * escape characters */ protected void string(Object obj) { this.add('"'); CharacterIterator it = new StringCharacterIterator(obj.toString()); for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) { if (c == '"') { this.add("\\\""); } else if (c == '\\') { this.add("\\\\"); } else if (c == '/') { this.add("\\/"); } else if (c == '\b') { this.add("\\b"); } else if (c == '\f') { this.add("\\f"); } else if (c == '\n') { this.add("\\n"); } else if (c == '\r') { this.add("\\r"); } else if (c == '\t') { this.add("\\t"); } else if (Character.isISOControl(c)) { this.unicode(c); } else { this.add(c); } } this.add('"'); } /** * Add object to buffer */ protected void add(Object obj) { this.buf.append(obj); } /** * Add char to buffer */ protected void add(char c) { this.buf.append(c); } /** * Represent as unicode * * @param c * character to be encoded */ protected void unicode(char c) { this.add("\\u"); int n = c; for (int i = 0; i < 4; ++i) { int digit = (n & 0xf000) >> 12; this.add(hex[digit]); n <<= 4; } } public void setIgnoreHierarchy(boolean ignoreHierarchy) { this.ignoreHierarchy = ignoreHierarchy; } /** * If true, an Enum is serialized as a bean with a special property * _name=name() as all as all other properties defined within the enum.
* If false, an Enum is serialized as a name=value pair (name=name()) * * @param enumAsBean * true to serialize an enum as a bean instead of as a name=value * pair (default=false) */ public void setEnumAsBean(boolean enumAsBean) { this.enumAsBean = enumAsBean; } protected static class JSONAnnotationFinder { private boolean serialize = true; private Method accessor; private String name; public JSONAnnotationFinder(Method accessor) { this.accessor = accessor; } public boolean shouldSerialize() { return serialize; } public String getName() { return name; } public JSONAnnotationFinder invoke() { JSON json = accessor.getAnnotation(JSON.class); serialize = json.serialize(); if (serialize && json.name().length() > 0) { name = json.name(); } return this; } } }


      在你的工程中新建org.apache.struts2.json包,把以上代码复制到你的工程里就可以了,要注意的是,如果是在WEBLOGIC中部署,最好把这个类重新打包到struts2-json-plugin-2.3.15.3.jar包中,避免不必要的麻烦。


你可能感兴趣的:(JAVA,EE)