在客户端使用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包中,避免不必要的麻烦。