Skip to content

Commit c10820c

Browse files
committed
http://code.google.com/p/mybatis/issues/detail?id=110 . Implement CGLIB enhanced object serialiation into original domain object. Thanks Arkadi!
1 parent 13d9aa8 commit c10820c

File tree

6 files changed

+210
-49
lines changed

6 files changed

+210
-49
lines changed

src/main/java/org/apache/ibatis/binding/MapperProxy.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.apache.ibatis.binding;
22

3+
import java.io.Serializable;
34
import java.lang.reflect.InvocationHandler;
45
import java.lang.reflect.Method;
56
import java.lang.reflect.Proxy;
@@ -8,7 +9,9 @@
89

910
import org.apache.ibatis.session.SqlSession;
1011

11-
public class MapperProxy implements InvocationHandler {
12+
public class MapperProxy implements InvocationHandler, Serializable {
13+
14+
private static final long serialVersionUID = -6424540398559729838L;
1215

1316
private static final Set<String> OBJECT_METHODS = new HashSet<String>() {
1417
private static final long serialVersionUID = -1782950882770203582L;

src/main/java/org/apache/ibatis/executor/loader/ResultLoaderMap.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package org.apache.ibatis.executor.loader;
22

3-
import org.apache.ibatis.reflection.MetaObject;
4-
5-
import java.io.Serializable;
63
import java.sql.SQLException;
74
import java.util.HashMap;
85
import java.util.Locale;
96
import java.util.Map;
107
import java.util.Set;
118

12-
public class ResultLoaderMap implements Serializable {
9+
import org.apache.ibatis.reflection.MetaObject;
10+
11+
public class ResultLoaderMap {
1312

1413
private final Map<String, LoadPair> loaderMap = new HashMap<String, LoadPair>();
1514

@@ -18,14 +17,14 @@ public void addLoader(String property, MetaObject metaResultObject, ResultLoader
1817
loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
1918
}
2019

21-
public int size() throws SQLException {
20+
public int size() {
2221
return loaderMap.size();
2322
}
2423

25-
public boolean hasLoader(String methodName) throws SQLException {
24+
public boolean hasLoader(String methodName) {
2625
return loaderMap.containsKey(methodName.toUpperCase(Locale.ENGLISH));
2726
}
28-
27+
2928
public boolean load(String property) throws SQLException {
3029
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
3130
if (pair != null) {
Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,99 @@
11
package org.apache.ibatis.executor.loader;
22

3+
import java.io.NotSerializableException;
4+
import java.io.ObjectStreamException;
5+
import java.io.Serializable;
6+
import java.lang.reflect.Field;
7+
import java.lang.reflect.Method;
8+
import java.util.Arrays;
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
12+
import net.sf.cglib.core.Signature;
313
import net.sf.cglib.proxy.Enhancer;
14+
import net.sf.cglib.proxy.InterfaceMaker;
415
import net.sf.cglib.proxy.MethodInterceptor;
516
import net.sf.cglib.proxy.MethodProxy;
17+
18+
import org.apache.ibatis.logging.Log;
19+
import org.apache.ibatis.logging.LogFactory;
620
import org.apache.ibatis.reflection.ExceptionUtil;
21+
import org.apache.ibatis.reflection.factory.ObjectFactory;
722
import org.apache.ibatis.reflection.property.PropertyNamer;
823
import org.apache.ibatis.type.TypeHandlerRegistry;
9-
10-
import java.io.Serializable;
11-
import java.lang.reflect.Method;
12-
import java.lang.reflect.Field;
13-
import java.util.*;
24+
import org.objectweb.asm.Type;
1425

1526
public class ResultObjectProxy {
1627

28+
private static final Log log = LogFactory.getLog(ResultObjectProxy.class);
29+
1730
private static final Set<String> objectMethods = new HashSet<String>(Arrays.asList(new String[]{"equals","clone","hashCode","toString"}));
1831
private static final TypeHandlerRegistry registry = new TypeHandlerRegistry();
1932
private static final String FINALIZE_METHOD = "finalize";
33+
private static final String WRITE_REPLACE_METHOD = "writeReplace";
2034

21-
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, boolean aggressive) {
22-
return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, aggressive);
35+
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, boolean aggressive, ObjectFactory objectFactory) {
36+
return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, aggressive, objectFactory);
2337
}
2438

2539
private static class EnhancedResultObjectProxyImpl implements MethodInterceptor, Serializable {
2640

41+
private static final long serialVersionUID = 3943261828110652464L;
42+
private Class type;
2743
private ResultLoaderMap lazyLoader;
2844
private boolean aggressive;
45+
private ObjectFactory objectFactory;
2946

30-
private EnhancedResultObjectProxyImpl(ResultLoaderMap lazyLoader, boolean aggressive) {
47+
private EnhancedResultObjectProxyImpl(Class type, ResultLoaderMap lazyLoader, boolean aggressive, ObjectFactory objectFactory) {
48+
this.type = type;
3149
this.lazyLoader = lazyLoader;
3250
this.aggressive = aggressive;
51+
this.objectFactory = objectFactory;
3352
}
3453

35-
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, boolean aggressive) {
54+
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, boolean aggressive, ObjectFactory objectFactory) {
3655
final Class type = target.getClass();
3756
if (registry.hasTypeHandler(type)) {
3857
return target;
3958
} else {
40-
final Object enhanced = Enhancer.create(type, new EnhancedResultObjectProxyImpl(lazyLoader, aggressive));
41-
copyInitialState(type, target, enhanced);
59+
EnhancedResultObjectProxyImpl proxy = new EnhancedResultObjectProxyImpl(type, lazyLoader, aggressive, objectFactory);
60+
Enhancer enhancer = new Enhancer();
61+
enhancer.setCallback(proxy);
62+
enhancer.setSuperclass(type);
63+
64+
try {
65+
type.getDeclaredMethod(WRITE_REPLACE_METHOD);
66+
// ObjectOutputStream will call writeReplace of objects returned by writeReplace
67+
log.warn(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
68+
} catch (NoSuchMethodException e) {
69+
// using asm (org.objectweb.asm.Type), this will not work with cglib-nodep
70+
InterfaceMaker writeReplaceInterface = new InterfaceMaker();
71+
Signature signature = new Signature(WRITE_REPLACE_METHOD, Type.getType(Object.class), new Type[] {});
72+
writeReplaceInterface.add(signature, new Type[] { Type.getType(ObjectStreamException.class) });
73+
enhancer.setInterfaces(new Class[] { writeReplaceInterface.create() });
74+
} catch (SecurityException e) {
75+
// nothing to do here
76+
}
77+
78+
final Object enhanced = enhancer.create();
79+
copyBeanProperties(type, target, enhanced);
4280
return enhanced;
4381
}
4482
}
45-
83+
4684
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
85+
final String methodName = method.getName();
4786
try {
48-
final String methodName = method.getName();
4987
synchronized (lazyLoader) {
50-
if (lazyLoader.size() > 0) {
51-
if (!FINALIZE_METHOD.equals(methodName)) {
88+
if (WRITE_REPLACE_METHOD.equals(methodName)) {
89+
if (lazyLoader.size() > 0) {
90+
throw new NotSerializableException("Not all lazy properties have been loaded yet");
91+
}
92+
Object original = objectFactory.create(type);
93+
copyBeanProperties(type, o, original);
94+
return original;
95+
} else if (!FINALIZE_METHOD.equals(methodName)) {
96+
if (lazyLoader.size() > 0) {
5297
if (aggressive || objectMethods.contains(methodName)) {
5398
lazyLoader.loadAll();
5499
} else if (PropertyNamer.isProperty(methodName)) {
@@ -66,22 +111,22 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy meth
66111
}
67112
}
68113

69-
private static void copyInitialState(Class type, Object target, Object enhanced) {
114+
private static void copyBeanProperties(Class type, Object sourceBean, Object destinationBean) {
70115
Class parent = type;
71116
while (parent != null) {
72117
final Field[] fields = parent.getDeclaredFields();
73118
for(Field field : fields) {
74119
try {
75120
field.setAccessible(true);
76-
field.set(enhanced,field.get(target));
121+
Object val = field.get(sourceBean);
122+
field.set(destinationBean, val);
77123
} catch (Exception e) {
78124
// Nothing useful to do, will only fail on final fields, which will be ignored.
79125
}
80126
}
81127
parent = parent.getSuperclass();
82128
}
83129
}
84-
85130
}
86131

87132
}

src/main/java/org/apache/ibatis/executor/resultset/FastResultSetHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ protected void loadMappedAndUnmappedColumnNames(ResultSet rs, ResultMap resultMa
285285
protected Object createResultObject(ResultSet rs, ResultMap resultMap, ResultLoaderMap lazyLoader) throws SQLException {
286286
final Object resultObject = createResultObject(rs, resultMap);
287287
if (resultObject != null && configuration.isLazyLoadingEnabled()) {
288-
return ResultObjectProxy.createProxy(resultObject, lazyLoader, configuration.isAggressiveLazyLoading());
288+
return ResultObjectProxy.createProxy(resultObject, lazyLoader, configuration.isAggressiveLazyLoading(), objectFactory);
289289
}
290290
return resultObject;
291291
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.apache.ibatis.executor;
2+
3+
import java.io.ObjectStreamException;
4+
import java.io.Serializable;
5+
6+
public class BeanWithWriteReplace implements Serializable {
7+
8+
protected int id;
9+
protected String value;
10+
11+
public BeanWithWriteReplace() {
12+
}
13+
14+
public BeanWithWriteReplace(int id, String value) {
15+
super();
16+
this.id = id;
17+
this.value = value;
18+
}
19+
20+
public int getId() {
21+
return id;
22+
}
23+
24+
public void setId(int id) {
25+
this.id = id;
26+
}
27+
28+
public String getValue() {
29+
return value;
30+
}
31+
32+
public void setValue(String value) {
33+
this.value = value;
34+
}
35+
36+
@Override
37+
public int hashCode() {
38+
final int prime = 31;
39+
int result = 1;
40+
result = prime * result + id;
41+
result = prime * result + ((value == null) ? 0 : value.hashCode());
42+
return result;
43+
}
44+
45+
@Override
46+
public boolean equals(Object obj) {
47+
if (this == obj)
48+
return true;
49+
if (obj == null)
50+
return false;
51+
if (getClass() != obj.getClass())
52+
return false;
53+
BeanWithWriteReplace other = (BeanWithWriteReplace) obj;
54+
if (id != other.id)
55+
return false;
56+
if (value == null) {
57+
if (other.value != null)
58+
return false;
59+
} else if (!value.equals(other.value))
60+
return false;
61+
return true;
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return "BeanWithWriteReplace [id=" + id + ", value=" + value + "]";
67+
}
68+
69+
protected Object writeReplace() throws ObjectStreamException {
70+
return this;
71+
}
72+
}

src/test/java/org/apache/ibatis/executor/SerializableProxyTest.java

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,90 @@
11
package org.apache.ibatis.executor;
22

3-
import domain.blog.Author;
4-
import domain.blog.Section;
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
5+
import static org.junit.Assert.fail;
6+
7+
import java.io.ByteArrayInputStream;
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.NotSerializableException;
10+
import java.io.ObjectInputStream;
11+
import java.io.ObjectOutputStream;
12+
import java.io.Serializable;
13+
import java.lang.reflect.Method;
14+
515
import org.apache.ibatis.executor.loader.ResultLoaderMap;
616
import org.apache.ibatis.executor.loader.ResultObjectProxy;
7-
import static org.junit.Assert.assertEquals;
17+
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
818
import org.junit.Test;
919

10-
import java.io.*;
20+
import domain.blog.Author;
21+
import domain.blog.Section;
1122

1223
public class SerializableProxyTest {
1324

1425
@Test
1526
public void shouldDemonstrateSerializableEnhancer() throws Exception {
1627
Author author = new Author(999, "someone", "!@#@!#!@#", "[email protected]", "blah", Section.NEWS);
17-
Object proxy = ResultObjectProxy.createProxy(author, new ResultLoaderMap(), true);
28+
Object proxy = ResultObjectProxy.createProxy(author, new ResultLoaderMap(), true, new DefaultObjectFactory());
1829
byte[] bytes = serialize((Serializable) proxy);
1930
Object proxy2 = deserialize(bytes);
20-
assertEquals(author.toString(), proxy2.toString());
31+
// serialization/deserialization of a cglib proxy in the same execution always works
32+
// because the generated class is registerd to the jvm
33+
// we should check this in different executions of just check the class name
34+
assertEquals(author, proxy2);
35+
assertEquals(author.getClass(), proxy2.getClass());
2136
}
2237

23-
private byte[] serialize(Serializable value) {
38+
@Test
39+
public void shouldGenerateWriteReplace() throws Exception {
40+
Author author = new Author(999, "someone", "!@#@!#!@#", "[email protected]", "blah", Section.NEWS);
2441
try {
25-
ByteArrayOutputStream bos = new ByteArrayOutputStream();
26-
ObjectOutputStream oos = new ObjectOutputStream(bos);
27-
oos.writeObject(value);
28-
oos.flush();
29-
oos.close();
30-
return bos.toByteArray();
42+
author.getClass().getDeclaredMethod("writeReplace");
43+
fail("Author should not have a writeReplace method");
3144
} catch (Exception e) {
32-
throw new RuntimeException("Error serializing object. Cause: " + e, e);
45+
// ok
3346
}
47+
Object proxy = ResultObjectProxy.createProxy(author, new ResultLoaderMap(), true, new DefaultObjectFactory());
48+
// check that writeReplace method was generated
49+
proxy.getClass().getDeclaredMethod("writeReplace");
3450
}
35-
36-
private Serializable deserialize(byte[] value) {
37-
Serializable result;
51+
52+
@Test
53+
public void shouldNotGenerateWriteReplace() throws Exception {
54+
BeanWithWriteReplace beanWithWriteReplace = new BeanWithWriteReplace(999, "someone");
55+
Object proxy = ResultObjectProxy.createProxy(beanWithWriteReplace, new ResultLoaderMap(), true, new DefaultObjectFactory());
3856
try {
39-
ByteArrayInputStream bis = new ByteArrayInputStream((byte[]) value);
40-
ObjectInputStream ois = new ObjectInputStream(bis);
41-
result = (Serializable) ois.readObject();
42-
ois.close();
57+
beanWithWriteReplace.getClass().getDeclaredMethod("writeReplace");
4358
} catch (Exception e) {
44-
throw new RuntimeException("Error deserializing object. Cause: " + e, e);
59+
fail("Bean should declare a writeReplace method");
4560
}
61+
Method m = proxy.getClass().getDeclaredMethod("writeReplace");
62+
assertFalse("generated method is public so this should be protected", m.isAccessible());
63+
}
64+
65+
@Test(expected=NotSerializableException.class)
66+
public void shouldNotSerializePartiallyLoadedBean() throws Exception {
67+
BeanWithWriteReplace beanWithWriteReplace = new BeanWithWriteReplace(999, "someone");
68+
ResultLoaderMap loader = new ResultLoaderMap();
69+
loader.addLoader("property", null, null);
70+
Object proxy = ResultObjectProxy.createProxy(beanWithWriteReplace, loader, true, new DefaultObjectFactory());
71+
serialize((Serializable) proxy);
72+
}
73+
74+
private byte[] serialize(Serializable value) throws Exception {
75+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
76+
ObjectOutputStream oos = new ObjectOutputStream(bos);
77+
oos.writeObject(value);
78+
oos.flush();
79+
oos.close();
80+
return bos.toByteArray();
81+
}
82+
83+
private Serializable deserialize(byte[] value) throws Exception {
84+
ByteArrayInputStream bis = new ByteArrayInputStream((byte[]) value);
85+
ObjectInputStream ois = new ObjectInputStream(bis);
86+
Serializable result = (Serializable) ois.readObject();
87+
ois.close();
4688
return result;
4789
}
4890

0 commit comments

Comments
 (0)