De-proxying Hibernate objects
Rishabh Joshi, a product engineer, a friend and a once in a blue-moon blogger, encountered a requirement to create clones of database objects such that even if their relationships get modified the cloning process remains unaffected. Here is his article and the code he wrote for de-proxying hibernate objects and visiting the entire object tree.
The Requirement:
The business requirement was to get an object from the database, create its copy (or clone it), modify the required fields (through-out the object tree), and save as a new row. An, added requirement was, to be able to specify the fields one wants to clone (again, through-out the object tree) and ignore the rest.
Problems faced:
Simple bean copy would not work for us, because, if the object changed (i.e. a relationship is added or removed), it would require us to modify/re-write the code again, else, the flow would break. So, we needed a system that would be unaffected by the changes in the relationship.
Solution:
I used annotations to mark the fields in various classes that I need to clone and used the following code to achieve the requirement. So, in case a relationship is added, it should be annotated, else it would be ignored. This ensures that the normal flow does not break.
Code:
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
public class Cloner {
private static final Log LOG = LogFactory.getLog(Cloner.class);
private static Class<? extends Annotation> filter = null;
private static Map<Integer,Object> visited = new HashMap<Integer,Object>();
/**
* Returns a copy of the given {@code source} bean.<br>
* It performs a deep copy.<br>
* All the fields are copied, including from the super classes, if any.<br>
* If an annotation is provided, then only those field that are annotated
* with the given annotation are copied, rest are marked null.
*
* @param source
* The object whose copy is required.
* @param sourceClazz
* The {@link Class} object of the {@code source} object.
* @param filter
* The {@link Annotation} describing the fields that need to be
* copied. If it is {@code null}, then all fields are copied.
* @return The copy of the {@code source} object. It needs to be cast back
* to the original type.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static Object copyBean(Object source,
Class<? extends Object> sourceClazz,
Class<? extends Annotation> filter)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
// De-proxy the object
if (source instanceof HibernateProxy) {
Hibernate.initialize(source);
HibernateProxy proxy = (HibernateProxy) source;
LazyInitializer li = proxy.getHibernateLazyInitializer();
sourceClazz = li.getImplementation().getClass();
source = sourceClazz.cast(((HibernateProxy) source)
.getHibernateLazyInitializer().getImplementation());
}
LOG.info("Copying object: " + source + ", Class: " + sourceClazz);
// Setting the filter as shown below will not give issues,
// as the same filter used across all objects.
Cloner.filter = filter;
LOG.info("Filtering on annotation: " + filter);
Object target = sourceClazz.newInstance();
if (visited.containsKey(source.hashCode())) {
return visited.get(source.hashCode());
}
else{
visited.put(source.hashCode(), target);
}
LOG.debug("Getting all fields of \"" + source + "\" object");
Map<String, Field> allSourceFields = new HashMap<String, Field>();
getAllFields(source, source.getClass(), allSourceFields);
LOG.debug("Getting all fields of \"" + target + "\" object");
Map<String, Field> allTargetFields = new HashMap<String, Field>();
getAllFields(target, target.getClass(), allTargetFields);
Set<String> keySet = allSourceFields.keySet();
for (String key : keySet) {
LOG.debug("Copying field: " + key + ", of: " + source);
Field srcField = allSourceFields.get(key);
Object value = getValue(srcField, source);
Field trgField = allTargetFields.get(key);
trgField.set(target, value);
}
return target;
}
/**
* Adds all the {@link Fields} of the {@code source} to the {@code fields}
* map.
*
* @param source
* The source object.
* @param sourceClazz
* The {@code source} object's Class.
* @param fields
* The {@link Map} object containing all the fields of {@code
* source}.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private static void getAllFields(Object source, Class<?> sourceClazz,
Map<String, Field> fields) throws IllegalArgumentException,
IllegalAccessException {
if (sourceClazz.isInstance(Object.class)) {
return;
} else {
getAllFields(source, sourceClazz.getSuperclass(), fields);
}
Field[] sourceFields = sourceClazz.getDeclaredFields();
for (int i = 0; i < sourceFields.length; i++) {
sourceFields[i].setAccessible(true);
if (null == filter) {
LOG.debug("Adding field: " + sourceFields[i].getName()
+ ", to the field map.");
fields.put(sourceFields[i].getName(), sourceFields[i]);
} else if (sourceFields[i].isAnnotationPresent(filter)) {
LOG.debug("Adding field: " + sourceFields[i].getName()
+ ", to the field map.");
fields.put(sourceFields[i].getName(), sourceFields[i]);
}
}
}
/**
* Method to get the value of a {@link Field}.
*
* @param field
* The {@link Field} whose value is required.
* @param source
* The {@code source} object which contains the given field.
* @return The value of the {@code field}.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private static Object getValue(Field field, Object source)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
Object value = field.get(source);
return performSmartCopy(value);
}
/**
* This method identifies the data-type of the value the {@code field}
* holds, and returns its copy.
*
* @param value
* The value the {@code field} holds.
* @return The copy of the given {@code value}.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private static Object performSmartCopy(Object value)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
if (null != value) {
if (isPrimitiveOrWrapperOrStringOrDate(value)) {
return value;
} else if (isPrimitiveArray(value)) {
return value;
} else if (value instanceof Object[]) {
return copyArray(value);
} else if (value instanceof Set<?>) {
return copySet(value);
} else if (value instanceof Map<?, ?>) {
return copyMap(value);
} else if (value instanceof List<?>) {
return copyList(value);
} else {
return copyBean(value, value.getClass(), filter);
}
}
return null;
}
/**
* Method to return a copy of a {@code List} object.
*
* @param value
* A {@code List} object.
* @return A copy of the {@code List} object.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private static Object copyList(Object value)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
List<?> valueList = (List<?>) value;
List<Object> result = new ArrayList<Object>(valueList.size());
for (Object obj : valueList) {
result.add(performSmartCopy(obj));
}
return result;
}
/**
* Method to return a copy of a {@code Map} object.
*
* @param value
* A {@code Map} object.
* @return A copy of the {@code Map} object.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private static Object copyMap(Object value)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
Map<?, ?> valueMap = (Map<?, ?>) value;
Map<Object, Object> result = new HashMap<Object, Object>();
for (Object obj : valueMap.keySet()) {
result.put(performSmartCopy(obj), performSmartCopy(valueMap
.get(obj)));
}
return result;
}
/**
* Method to return a copy of a {@code Set} object.
*
* @param value
* A {@code Set} object.
* @return A copy of the {@code Set} object.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private static Object copySet(Object value)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
Set<?> valueSet = (Set<?>) value;
Set<Object> result = new HashSet<Object>();
for (Object obj : valueSet) {
result.add(performSmartCopy(obj));
}
return result;
}
/**
* Method to return a copy of an {@code array}.
*
* @param value
* The {@code array} object.
* @return A copy of the {@code array} object.
*
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InstantiationException
*/
private static Object copyArray(Object value)
throws IllegalArgumentException, IllegalAccessException,
InstantiationException {
Class<?> componentType = value.getClass().getComponentType();
Object[] valueArray = (Object[]) value;
Object[] result = (Object[]) Array.newInstance(componentType,
valueArray.length);
for (int i = 0; i < valueArray.length; i++) {
result[i] = performSmartCopy(valueArray[i]);
}
return result;
}
/**
* Method to identify if the given object, is an array of a primitive
* data-type.
*
* @param value
* The given object.
* @return {@code true}: if the {@code value} is an array of a primitive
* data-type.<br/> {@code false}: Otherwise.
*/
private static boolean isPrimitiveArray(Object value) {
if ((value instanceof int[]) || (value instanceof long[])
|| (value instanceof boolean[]) || (value instanceof float[])
|| (value instanceof double[]) || (value instanceof byte[])
|| (value instanceof short[])) {
return true;
}
return false;
}
/**
* Method to identify if the given object is of one of the following
* data-types:<br/>
* 1. Primitives ({@code int}, {@code long}, {@code float}, etc.)<br/>
* 2. Wrapper ({@link Integer}, {@link Long}, {@link Float}, etc.)<br/>
* 3. {@link String}<br/>
* 4. {@link Date}<br/>
*
* @param value
* The given object.
* @return {@code true}: if the {@code value} is one of the above given
* data-types.<br/> {@code false}: Otherwise.
*/
private static boolean isPrimitiveOrWrapperOrStringOrDate(Object value) {
if ((value.getClass().isPrimitive()) || (value instanceof Integer)
|| (value instanceof Long) || (value instanceof Boolean)
|| (value instanceof Float) || (value instanceof Double)
|| (value instanceof String) || (value instanceof Character)
|| (value instanceof Byte) || (value instanceof Short)
|| (value instanceof Date)) {
return true;
}
return false;
}
}

3 Comments, Comment or Ping
ZAREMA
Avtoru Spasibo bolsoe.
Mar 19th, 2010
WP Themes
Nice post and this post helped me alot in my college assignement. Gratefulness you seeking your information.
May 1st, 2010
Andrew
Good article! Thanks! But there are some things that I would like to emphasize.
If you have Entity with a lot of fields, then put the annotations to them that rather takes many time.
However, we can pass as the third parameter null. Then will be copied to all fields, including private static final long serialVersionUID. In the end we get an exception. IMHO best way change the logic:
else if (sourceFields [i]. isAnnotationPresent (filter)) (
LOG.debug (“Adding field:” sourceFields [i]. GetName () “, to the field map.”);
fields.put (sourceFields [i]. getName (), sourceFields [i]);
)
change
else if (!sourceFields [i]. isAnnotationPresent (filter)) (
LOG.debug (“Adding field:” sourceFields [i]. GetName () “, to the field map.”);
fields.put (sourceFields [i]. getName (), sourceFields [i]);
)
continue to use the annotation to the fields that should not clone
For example @ NotClonable
Jul 22nd, 2010
Reply to “De-proxying Hibernate objects”