diff --git a/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java new file mode 100644 index 000000000..4cd16ddf8 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java @@ -0,0 +1,489 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed 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.springframework.data.convert; + +import static org.springframework.asm.Opcodes.*; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.asm.ClassWriter; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.Type; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.model.MappingInstantiationException; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * An {@link EntityInstantiator} that can generate byte code to speed-up dynamic object instantiation. + *

+ * Uses the {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an instance of the entity by + * dynamically generating factory methods with appropriate constructor invocations via ASM. + *

+ * If we cannot generate byte code for a type, we gracefully fall-back to the {@link ReflectionEntityInstantiator}. + * + * @author Thomas Darimont + * @since 1.10 + */ +public enum BytecodeGeneratingEntityInstantiator implements EntityInstantiator { + + INSTANCE; + + private final ObjectInstantiatorClassGenerator classGenerator = ObjectInstantiatorClassGenerator.INSTANCE; + + private volatile Map, EntityInstantiator> entityInstantiators = new HashMap, EntityInstantiator>( + 32); + + /* (non-Javadoc) + * @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) + */ + @Override + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + EntityInstantiator ei = this.entityInstantiators.get(entity.getTypeInformation()); + + if (ei != null) { + return ei.createInstance(entity, provider); + } + + ei = potentiallyCreateAndRegisterEntityInstantiator(entity); + + return ei.createInstance(entity, provider); + } + + /** + * @param entity + * @return + */ + private synchronized EntityInstantiator potentiallyCreateAndRegisterEntityInstantiator(PersistentEntity entity) { + + Map, EntityInstantiator> map = this.entityInstantiators; + + EntityInstantiator ei = map.get(entity.getTypeInformation()); + if (ei != null) { + return ei; + } + + ei = createEntityInstantiator(entity); + + map = new HashMap, EntityInstantiator>(map); + map.put(entity.getTypeInformation(), ei); + + this.entityInstantiators = map; + + return ei; + } + + /** + * @param entity + * @return + */ + private EntityInstantiator createEntityInstantiator(PersistentEntity entity) { + + if (shoudUseReflectionEntityInstantiator(entity)) { + return ReflectionEntityInstantiator.INSTANCE; + } + + try { + return new EntityInstantiatorAdapter(createObjectInstantiator(entity)); + } catch (Throwable ex) { + return ReflectionEntityInstantiator.INSTANCE; + } + } + + /** + * @param entity + * @return + */ + private boolean shoudUseReflectionEntityInstantiator(PersistentEntity entity) { + + Class type = entity.getType(); + + if (type.isInterface() // + || type.isArray() // + || !Modifier.isPublic(type.getModifiers()) // + || (type.isMemberClass() && !Modifier.isStatic(type.getModifiers())) // + || ClassUtils.isCglibProxyClass(type)) { // + return true; + } + + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + if (persistenceConstructor == null || !Modifier.isPublic(persistenceConstructor.getConstructor().getModifiers())) { + return true; + } + + return false; + } + + /** + * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link InstantiatorKey}. There will always + * be exactly one {@link ObjectInstantiator} instance per {@link PersistentEntity}. + *

+ * + * @param entity + * @return + */ + private ObjectInstantiator createObjectInstantiator(PersistentEntity entity) { + try { + return (ObjectInstantiator) classGenerator.generateCustomInstantiatorClass(entity).newInstance(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * A {@link ClassLoader} that can load classes from {@code byte[]} representations. + * + * @author Thomas Darimont + */ + private static class ByteArrayClassLoader extends ClassLoader { + + public ByteArrayClassLoader(ClassLoader parent) { + super(parent); + } + + /** + * Tries to load a class given {@code byte[]}. + * + * @param name must not be {@literal null} + * @param bytes must not be {@literal null} + * @return + */ + public Class loadClass(String name, byte[] bytes) { + + Assert.notNull(name, "name must not be null"); + Assert.notNull(bytes, "bytes must not be null"); + + try { + Class clazz = findClass(name); + if (clazz != null) { + return clazz; + } + } catch (ClassNotFoundException ignore) {} + + return defineClass(name, bytes, 0, bytes.length); + } + } + + /** + * @author Thomas Darimont + */ + private static class EntityInstantiatorAdapter implements EntityInstantiator { + + private static final Object[] EMPTY_ARRAY = new Object[0]; + + private final ObjectInstantiator instantiator; + + /** + * @param instantiator + */ + public EntityInstantiatorAdapter(ObjectInstantiator instantiator) { + this.instantiator = instantiator; + } + + @Override + @SuppressWarnings("unchecked") + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + Object[] params = extractInvocationArguments(provider, entity.getPersistenceConstructor()); + try { + return (T) instantiator.newInstance(params); + } catch (Exception e) { + throw new MappingInstantiationException(entity, Arrays.asList(params), e); + } + } + + /** + * @param provider + * @param prefCtor + * @return + */ + private

, T> Object[] extractInvocationArguments( + ParameterValueProvider

provider, PreferredConstructor prefCtor) { + + if (provider == null || prefCtor == null || !prefCtor.hasParameters()) { + return EMPTY_ARRAY; + } + + List params = new ArrayList(); + for (Parameter parameter : prefCtor.getParameters()) { + params.add(provider.getParameterValue(parameter)); + } + + return params.toArray(); + } + } + + /** + * @author Thomas Darimont + */ + public static interface ObjectInstantiator { + + Object newInstance(Object... args); + } + + /** + * Generates a new {@link ObjectInstantiator} class for the given custom class. + *

+ * This code generator will generate a custom factory class implementing the {@link ObjectInstantiator} interface for + * every publicly accessed constructor variant. + *

+ * Given a class {@code ObjCtor1ParamString} like: + * + *

+	 * {
+	 * 	@code
+	 * 	public class ObjCtor1ParamString extends ObjCtorNoArgs {
+	 * 
+	 * 		public final String param1;
+	 * 
+	 * 		public ObjCtor1ParamString(String param1) {
+	 * 			this.param1 = param1;
+	 * 		}
+	 * 	}
+	 * }
+	 * 
+ * + * The following factory class {@code ObjCtor1ParamString_Instantiator_asdf} is generated: + * + *
+	 * {
+	 * 	@code
+	 * 	public class ObjCtor1ParamString_Instantiator_asdf implements ObjectInstantiator {
+	 * 
+	 * 		public Object newInstance(Object... args) {
+	 * 			return new ObjCtor1ParamString((String) args[0]);
+	 * 		}
+	 * 	}
+	 * }
+	 * 
+ * + * @author Thomas Darimont + */ + enum ObjectInstantiatorClassGenerator { + + INSTANCE; + + private static final String INIT = ""; + private static final String TAG = "_Instantiator_"; + private static final String JAVA_LANG_OBJECT = "java/lang/Object"; + private static final String CREATE_METHOD_NAME = "newInstance"; + + private static final String[] IMPLEMENTED_INTERFACES = new String[] { Type + .getInternalName(ObjectInstantiator.class) }; + + private final ByteArrayClassLoader classLoader; + + private ObjectInstantiatorClassGenerator() { + + this.classLoader = AccessController.doPrivileged(new PrivilegedAction() { + public ByteArrayClassLoader run() { + return new ByteArrayClassLoader(ObjectInstantiatorClassGenerator.class.getClassLoader()); + } + }); + } + + /** + * Generate a new class for the given {@link InstantiatorKey}. + * + * @param key + * @return + */ + public Class generateCustomInstantiatorClass(PersistentEntity entity) { + + String className = generateClassName(entity); + byte[] bytecode = generateBytecode(className, entity); + + return classLoader.loadClass(className, bytecode); + } + + /** + * @param key + * @return + */ + private String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + } + + /** + * Generate a new class for the given {@link InstantiatorKey}. + * + * @param key + * @return + */ + public byte[] generateBytecode(String internalClassName, PersistentEntity entity) { + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + + cw.visit(Opcodes.V1_6, ACC_PUBLIC + ACC_SUPER, internalClassName.replace('.', '/'), null, JAVA_LANG_OBJECT, + IMPLEMENTED_INTERFACES); + + visitDefaultConstructor(cw); + + visitCreateMethod(cw, entity); + + cw.visitEnd(); + + return cw.toByteArray(); + } + + private void visitDefaultConstructor(ClassWriter cw) { + + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, INIT, "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, JAVA_LANG_OBJECT, INIT, "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(0, 0); // (0, 0) = computed via ClassWriter.COMPUTE_MAXS + mv.visitEnd(); + } + + /** + * Inserts the bytecode definition for the create method for the given {@link PersistentEntity}. + * + * @param cw + * @param entity + */ + private void visitCreateMethod(ClassWriter cw, PersistentEntity entity) { + + String entityTypeResourcePath = Type.getInternalName(entity.getType()); + + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, CREATE_METHOD_NAME, + "([Ljava/lang/Object;)Ljava/lang/Object;", null, null); + mv.visitCode(); + mv.visitTypeInsn(NEW, entityTypeResourcePath); + mv.visitInsn(DUP); + + Constructor ctor = entity.getPersistenceConstructor().getConstructor(); + Class[] parameterTypes = ctor.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + mv.visitVarInsn(ALOAD, 1); + + visitArrayIndex(mv, i); + + mv.visitInsn(AALOAD); + + if (parameterTypes[i].isPrimitive()) { + insertUnboxInsns(mv, Type.getType(parameterTypes[i]).toString().charAt(0), ""); + } else { + mv.visitTypeInsn(CHECKCAST, Type.getInternalName(parameterTypes[i])); + } + } + mv.visitMethodInsn(INVOKESPECIAL, entityTypeResourcePath, INIT, Type.getConstructorDescriptor(ctor), false); + + mv.visitInsn(ARETURN); + mv.visitMaxs(0, 0); // (0, 0) = computed via ClassWriter.COMPUTE_MAXS + mv.visitEnd(); + } + + /** + * Insert an appropriate value on the stack for the given index value {@code idx}. + * + * @param mv + * @param idx + */ + private static void visitArrayIndex(MethodVisitor mv, int idx) { + + if (idx >= 0 && idx < 6) { + mv.visitInsn(ICONST_0 + idx); + return; + } + + mv.visitLdcInsn(idx); + } + + /** + * Insert any necessary cast and value call to convert from a boxed type to a primitive value. + *

+ * Taken from Spring Expression 4.1.2 {@code org.springframework.expression.spel.CodeFlow#insertUnboxInsns}. + * + * @param mv the method visitor into which instructions should be inserted + * @param ch the primitive type desired as output + * @param stackDescriptor the descriptor of the type on top of the stack + */ + private static void insertUnboxInsns(MethodVisitor mv, char ch, String stackDescriptor) { + + switch (ch) { + case 'Z': + if (!stackDescriptor.equals("Ljava/lang/Boolean")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + break; + case 'B': + if (!stackDescriptor.equals("Ljava/lang/Byte")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Byte"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false); + break; + case 'C': + if (!stackDescriptor.equals("Ljava/lang/Character")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Character"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false); + break; + case 'D': + if (!stackDescriptor.equals("Ljava/lang/Double")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Double"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false); + break; + case 'F': + if (!stackDescriptor.equals("Ljava/lang/Float")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Float"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false); + break; + case 'I': + if (!stackDescriptor.equals("Ljava/lang/Integer")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Integer"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); + break; + case 'J': + if (!stackDescriptor.equals("Ljava/lang/Long")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Long"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false); + break; + case 'S': + if (!stackDescriptor.equals("Ljava/lang/Short")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Short"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false); + break; + default: + throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + ch + "'"); + } + } + } +} diff --git a/src/main/java/org/springframework/data/convert/EntityInstantiators.java b/src/main/java/org/springframework/data/convert/EntityInstantiators.java index 93595078a..c88025d1a 100644 --- a/src/main/java/org/springframework/data/convert/EntityInstantiators.java +++ b/src/main/java/org/springframework/data/convert/EntityInstantiators.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.util.Assert; * default one. * * @author Oliver Gierke + * @author Thomas Darimont */ public class EntityInstantiators { @@ -54,7 +55,7 @@ public class EntityInstantiators { * @param customInstantiators must not be {@literal null}. */ public EntityInstantiators(Map, EntityInstantiator> customInstantiators) { - this(ReflectionEntityInstantiator.INSTANCE, customInstantiators); + this(BytecodeGeneratingEntityInstantiator.INSTANCE, customInstantiators); } /** diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java index 543fe39b7..9ab078e43 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java @@ -41,7 +41,7 @@ public interface PersistentEntity> { * Returns the {@link PreferredConstructor} to be used to instantiate objects of this {@link PersistentEntity}. * * @return {@literal null} in case no suitable constructor for automatic construction can be found. This usually - * indicates that the instantiation of the object of tthat persistent entity is done through either a customer + * indicates that the instantiation of the object of that persistent entity is done through either a customer * {@link EntityInstantiator} or handled by custom conversion mechanisms entirely. */ PreferredConstructor getPersistenceConstructor(); diff --git a/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java new file mode 100644 index 000000000..f0e6a41ac --- /dev/null +++ b/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java @@ -0,0 +1,414 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed 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.springframework.data.convert; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.convert.BytecodeGeneratingEntityInstantiator.*; +import static org.springframework.data.util.ClassTypeInformation.*; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.convert.BytecodeGeneratingEntityInstantiatorUnitTests.Outer.Inner; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.mapping.model.MappingInstantiationException; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; + +/** + * Unit tests for {@link BytecodeGeneratingEntityInstantiator}. + * + * @author Thomas Darimont + */ +@RunWith(MockitoJUnitRunner.class) +public class BytecodeGeneratingEntityInstantiatorUnitTests

> { + + @Mock PersistentEntity entity; + @Mock ParameterValueProvider

provider; + @Mock PreferredConstructor constructor; + @Mock Parameter parameter; + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiatesSimpleObjectCorrectly() { + + when(entity.getType()).thenReturn((Class) Object.class); + INSTANCE.createInstance(entity, provider); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiatesArrayCorrectly() { + + when(entity.getType()).thenReturn((Class) String[][].class); + INSTANCE.createInstance(entity, provider); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiatesTypeWithPreferredConstructorUsingParameterValueProvider() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer(Foo.class).getConstructor(); + + when(entity.getType()).thenReturn((Class) Foo.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + Object instance = INSTANCE.createInstance(entity, provider); + + assertTrue(instance instanceof Foo); + verify(provider, times(1)).getParameterValue((Parameter) constructor.getParameters().iterator().next()); + } + + /** + * @see DATACMNS-300 + */ + @Test(expected = MappingInstantiationException.class) + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void throwsExceptionOnBeanInstantiationException() { + + when(entity.getPersistenceConstructor()).thenReturn(null); + when(entity.getType()).thenReturn((Class) PersistentEntity.class); + + INSTANCE.createInstance(entity, provider); + } + + /** + * @see DATACMNS-134 + */ + @Test + public void createsInnerClassInstanceCorrectly() { + + BasicPersistentEntity entity = new BasicPersistentEntity(from(Inner.class)); + PreferredConstructor constructor = entity.getPersistenceConstructor(); + Parameter parameter = constructor.getParameters().iterator().next(); + + final Object outer = new Outer(); + + when(provider.getParameterValue(parameter)).thenReturn(outer); + final Inner instance = INSTANCE.createInstance(entity, provider); + + assertThat(instance, is(notNullValue())); + + // Hack to check syntheic field as compiles create different field names (e.g. this$0, this$1) + ReflectionUtils.doWithFields(Inner.class, new FieldCallback() { + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + if (field.isSynthetic() && field.getName().startsWith("this$")) { + ReflectionUtils.makeAccessible(field); + assertThat(ReflectionUtils.getField(field, instance), is(outer)); + } + } + }); + } + + /** + * @see DATACMNS-283 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void capturesContextOnInstantiationException() throws Exception { + + PersistentEntity entity = new BasicPersistentEntity(from(Sample.class)); + + when(provider.getParameterValue(Mockito.any(Parameter.class))).thenReturn("FOO"); + + Constructor constructor = Sample.class.getConstructor(Long.class, String.class); + List parameters = Arrays.asList((Object) "FOO", (Object) "FOO"); + + try { + + INSTANCE.createInstance(entity, provider); + fail("Expected MappingInstantiationException!"); + + } catch (MappingInstantiationException o_O) { + + assertThat(o_O.getConstructor(), is(constructor)); + assertThat(o_O.getConstructorArguments(), is(parameters)); + assertEquals(Sample.class, o_O.getEntityType()); + + assertThat(o_O.getMessage(), containsString(Sample.class.getName())); + assertThat(o_O.getMessage(), containsString(Long.class.getName())); + assertThat(o_O.getMessage(), containsString(String.class.getName())); + assertThat(o_O.getMessage(), containsString("FOO")); + } + } + + /** + * @see DATACMNS-578 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiateObjCtorDefault() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer(ObjCtorDefault.class) + .getConstructor(); + + when(entity.getType()).thenReturn((Class) ObjCtorDefault.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + for (int i = 0; i < 2; i++) { + Object instance = INSTANCE.createInstance(entity, provider); + assertTrue(instance instanceof ObjCtorDefault); + } + } + + /** + * @see DATACMNS-578 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiateObjCtorNoArgs() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer(ObjCtorNoArgs.class) + .getConstructor(); + + when(entity.getType()).thenReturn((Class) ObjCtorNoArgs.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + for (int i = 0; i < 2; i++) { + Object instance = INSTANCE.createInstance(entity, provider); + assertTrue(instance instanceof ObjCtorNoArgs); + assertTrue(((ObjCtorNoArgs) instance).ctorInvoked); + } + } + + /** + * @see DATACMNS-578 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiateObjCtor1ParamString() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer( + ObjCtor1ParamString.class).getConstructor(); + + when(entity.getType()).thenReturn((Class) ObjCtor1ParamString.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + when(provider.getParameterValue(Mockito.any(Parameter.class))).thenReturn("FOO"); + + for (int i = 0; i < 2; i++) { + Object instance = INSTANCE.createInstance(entity, provider); + assertTrue(instance instanceof ObjCtor1ParamString); + assertTrue(((ObjCtor1ParamString) instance).ctorInvoked); + assertThat(((ObjCtor1ParamString) instance).param1, is("FOO")); + } + } + + /** + * @see DATACMNS-578 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiateObjCtor2ParamStringString() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer( + ObjCtor2ParamStringString.class).getConstructor(); + + when(entity.getType()).thenReturn((Class) ObjCtor2ParamStringString.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + for (int i = 0; i < 2; i++) { + when(provider.getParameterValue(Mockito.any(Parameter.class))).thenReturn("FOO").thenReturn("BAR"); + + Object instance = INSTANCE.createInstance(entity, provider); + assertTrue(instance instanceof ObjCtor2ParamStringString); + assertTrue(((ObjCtor2ParamStringString) instance).ctorInvoked); + assertThat(((ObjCtor2ParamStringString) instance).param1, is("FOO")); + assertThat(((ObjCtor2ParamStringString) instance).param2, is("BAR")); + } + } + + /** + * @see DATACMNS-578 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiateObjectCtor1ParamInt() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer( + ObjectCtor1ParamInt.class).getConstructor(); + + when(entity.getType()).thenReturn((Class) ObjectCtor1ParamInt.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + for (int i = 0; i < 2; i++) { + when(provider.getParameterValue(Mockito.any(Parameter.class))).thenReturn(42); + + Object instance = INSTANCE.createInstance(entity, provider); + assertTrue(instance instanceof ObjectCtor1ParamInt); + assertTrue("matches", ((ObjectCtor1ParamInt) instance).param1 == 42); + } + } + + /** + * @see DATACMNS-578 + */ + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiateObjectCtor7ParamsString5IntsString() { + + PreferredConstructor constructor = new PreferredConstructorDiscoverer( + ObjectCtor7ParamsString5IntsString.class).getConstructor(); + + when(entity.getType()).thenReturn((Class) ObjectCtor7ParamsString5IntsString.class); + when(entity.getPersistenceConstructor()).thenReturn(constructor); + + for (int i = 0; i < 2; i++) { + when(provider.getParameterValue(Mockito.any(Parameter.class))).thenReturn("A").thenReturn(1).thenReturn(2).thenReturn(3).thenReturn(4).thenReturn(5).thenReturn("B"); + + Object instance = INSTANCE.createInstance(entity, provider); + assertTrue(instance instanceof ObjectCtor7ParamsString5IntsString); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param1, is("A")); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param2, is(1)); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param3, is(2)); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param4, is(3)); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param5, is(4)); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param6, is(5)); + assertThat(((ObjectCtor7ParamsString5IntsString) instance).param7, is("B")); + } + } + + static class Foo { + + Foo(String foo) { + + } + } + + static class Outer { + + class Inner { + + } + } + + static class Sample { + + final Long id; + final String name; + + public Sample(Long id, String name) { + + this.id = id; + this.name = name; + } + } + + /** + * @author Thomas Darimont + * + */ + public static class ObjCtorDefault {} + + /** + * @author Thomas Darimont + * + */ + public static class ObjCtorNoArgs { + + public boolean ctorInvoked; + + public ObjCtorNoArgs() { + ctorInvoked = true; + } + } + + /** + * @author Thomas Darimont + * + */ + public static class ObjCtor1ParamString { + + public boolean ctorInvoked; + public String param1; + + public ObjCtor1ParamString(String param1) { + this.param1 = param1; + this.ctorInvoked = true; + } + } + + /** + * @author Thomas Darimont + * + */ + public static class ObjCtor2ParamStringString { + + public boolean ctorInvoked; + public String param1; + public String param2; + + public ObjCtor2ParamStringString(String param1, String param2) { + this.ctorInvoked = true; + this.param1 = param1; + this.param2 = param2; + } + } + + /** + * @author Thomas Darimont + * + */ + public static class ObjectCtor1ParamInt{ + + public int param1; + + public ObjectCtor1ParamInt(int param1) { + this.param1 = param1; + } + } + + /** + * @author Thomas Darimont + * + */ + public static class ObjectCtor7ParamsString5IntsString{ + + public String param1; + public int param2; + public int param3; + public int param4; + public int param5; + public int param6; + public String param7; + + public ObjectCtor7ParamsString5IntsString(String param1, int param2, int param3, int param4, int param5, + int param6, String param7) { + this.param1 = param1; + this.param2 = param2; + this.param3 = param3; + this.param4 = param4; + this.param5 = param5; + this.param6 = param6; + this.param7 = param7; + } + } +} diff --git a/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java b/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java index 18eaf449b..13233fee9 100644 --- a/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java +++ b/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java @@ -15,9 +15,9 @@ */ package org.springframework.data.convert; -import static org.mockito.Mockito.*; -import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Collections; import java.util.Map; @@ -36,11 +36,8 @@ import org.springframework.data.mapping.PersistentEntity; @RunWith(MockitoJUnitRunner.class) public class EntityInstantiatorsUnitTests { - @Mock - PersistentEntity entity; - - @Mock - EntityInstantiator customInstantiator; + @Mock PersistentEntity entity; + @Mock EntityInstantiator customInstantiator; @Test(expected = IllegalArgumentException.class) public void rejectsNullFallbackInstantiator() { @@ -51,7 +48,8 @@ public class EntityInstantiatorsUnitTests { public void usesReflectionEntityInstantiatorAsDefaultFallback() { EntityInstantiators instantiators = new EntityInstantiators(); - assertThat(instantiators.getInstantiatorFor(entity), is((EntityInstantiator) ReflectionEntityInstantiator.INSTANCE)); + assertThat(instantiators.getInstantiatorFor(entity), + is((EntityInstantiator) BytecodeGeneratingEntityInstantiator.INSTANCE)); } @Test diff --git a/template.mf b/template.mf index 96686f8fe..abe2cad8f 100644 --- a/template.mf +++ b/template.mf @@ -15,6 +15,7 @@ Import-Template: javax.xml.bind.*;version="0";resolution:=optional, javax.xml.transform.*;version="0";resolution:=optional, org.springframework.aop.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional, + org.springframework.asm.*;version="${spring:[=.=.=,+1.1.0)}", org.springframework.beans.*;version="${spring:[=.=.=,+1.1.0)}", org.springframework.core.*;version="${spring:[=.=.=,+1.1.0)}", org.springframework.context.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional,