From 1b9cdc941f3902fca5ef74fe14216e20c42418e2 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 14 Oct 2014 10:22:04 +0200 Subject: [PATCH] DATACMNS-578 - Speed up instance creation by generating factory classes with ASM. Introduced BytecodeGeneratingEntityInstantiator which dynamically generates classes to speed up the dynamic instantiation of objects. Since we cannot support every use case with byte code generation we gracefully fallback to the ReflectiveEntityInstantiator. BytecodeGeneratingEntityInstantiator has support for unboxing of constructor parameters and more robust handling for cases where we cannot generate a custom ObjectInstantiator. Original pull request: #98. --- .../BytecodeGeneratingEntityInstantiator.java | 489 ++++++++++++++++++ .../data/convert/EntityInstantiators.java | 5 +- .../data/mapping/PersistentEntity.java | 2 +- ...GeneratingEntityInstantiatorUnitTests.java | 414 +++++++++++++++ .../convert/EntityInstantiatorsUnitTests.java | 14 +- template.mf | 1 + 6 files changed, 914 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java create mode 100644 src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java 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,