diff --git a/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java index 75711a23a..352f757e4 100644 --- a/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiator.java @@ -15,31 +15,10 @@ */ 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 @@ -50,15 +29,14 @@ import org.springframework.util.ClassUtils; * @author Thomas Darimont * @author Oliver Gierke * @since 1.10 + * @deprecated since 1.11 in favor of {@link ClassGeneratingEntityInstantiator} */ public enum BytecodeGeneratingEntityInstantiator implements EntityInstantiator { INSTANCE; - private static final ObjectInstantiatorClassGenerator GENERATOR = ObjectInstantiatorClassGenerator.INSTANCE; + private final ClassGeneratingEntityInstantiator delegate = new ClassGeneratingEntityInstantiator(); - 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) @@ -67,434 +45,15 @@ public enum BytecodeGeneratingEntityInstantiator implements EntityInstantiator { public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { - EntityInstantiator instantiator = this.entityInstantiators.get(entity.getTypeInformation()); - - if (instantiator == null) { - instantiator = potentiallyCreateAndRegisterEntityInstantiator(entity); - } - - return instantiator.createInstance(entity, provider); - } - - /** - * @param entity - * @return - */ - private synchronized EntityInstantiator potentiallyCreateAndRegisterEntityInstantiator(PersistentEntity entity) { - - Map, EntityInstantiator> map = this.entityInstantiators; - EntityInstantiator instantiator = map.get(entity.getTypeInformation()); - - if (instantiator != null) { - return instantiator; - } - - instantiator = createEntityInstantiator(entity); - - map = new HashMap, EntityInstantiator>(map); - map.put(entity.getTypeInformation(), instantiator); - - this.entityInstantiators = map; - - return instantiator; - } - - /** - * @param entity - * @return - */ - private EntityInstantiator createEntityInstantiator(PersistentEntity entity) { - - if (shouldUseReflectionEntityInstantiator(entity)) { - return ReflectionEntityInstantiator.INSTANCE; - } - - try { - return new EntityInstantiatorAdapter(createObjectInstantiator(entity)); - } catch (Throwable ex) { - return ReflectionEntityInstantiator.INSTANCE; - } - } - - /** - * @param entity - * @return - */ - private boolean shouldUseReflectionEntityInstantiator(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) GENERATOR.generateCustomInstantiatorClass(entity).newInstance(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * Adapter to forward an invocation of the {@link EntityInstantiator} API to an {@link ObjectInstantiator}. - * - * @author Thomas Darimont - * @author Oliver Gierke - */ - private static class EntityInstantiatorAdapter implements EntityInstantiator { - - private static final Object[] EMPTY_ARRAY = new Object[0]; - - private final ObjectInstantiator instantiator; - - /** - * Creates a new {@link EntityInstantiatorAdapter} for the given {@link ObjectInstantiator}. - * - * @param instantiator must not be {@literal null}. - */ - public EntityInstantiatorAdapter(ObjectInstantiator instantiator) { - this.instantiator = instantiator; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) - */ - @Override - @SuppressWarnings("unchecked") - public , P extends PersistentProperty

> T createInstance(E entity, - ParameterValueProvider

provider) { - - Object[] params = extractInvocationArguments(entity.getPersistenceConstructor(), provider); - - try { - return (T) instantiator.newInstance(params); - } catch (Exception e) { - throw new MappingInstantiationException(entity, Arrays.asList(params), e); - } - } - - /** - * Extracts the arguments required to invoce the given constructor from the given {@link ParameterValueProvider}. - * - * @param constructor can be {@literal null}. - * @param provider can be {@literal null}. - * @return - */ - private

, T> Object[] extractInvocationArguments( - PreferredConstructor constructor, ParameterValueProvider

provider) { - - if (provider == null || constructor == null || !constructor.hasParameters()) { - return EMPTY_ARRAY; - } - - List params = new ArrayList(); - - for (Parameter parameter : constructor.getParameters()) { - params.add(provider.getParameterValue(parameter)); - } - - return params.toArray(); - } + return this.delegate.createInstance(entity, provider); } /** * @author Thomas Darimont + * @deprecated */ public 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(ClassUtils.getDefaultClassLoader()); - } - }); - } - - /** - * 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 + "'"); - } - } - - /** - * A {@link ClassLoader} that can load classes from {@code byte[]} representations. - * - * @author Thomas Darimont - */ - private 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); - } - } - } } diff --git a/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java new file mode 100644 index 000000000..1aaa12e23 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java @@ -0,0 +1,501 @@ +package org.springframework.data.convert; +/* + * Copyright 2015 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. + */ + +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 + * @author Oliver Gierke + * @since 1.10 + */ +public class ClassGeneratingEntityInstantiator implements EntityInstantiator { + + private final ObjectInstantiatorClassGenerator generator; + + private volatile Map, EntityInstantiator> entityInstantiators = new HashMap, EntityInstantiator>( + 32); + + public ClassGeneratingEntityInstantiator() { + this.generator = new ObjectInstantiatorClassGenerator(); + } + + /* (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 instantiator = this.entityInstantiators.get(entity.getTypeInformation()); + + if (instantiator == null) { + instantiator = potentiallyCreateAndRegisterEntityInstantiator(entity); + } + + return instantiator.createInstance(entity, provider); + } + + /** + * @param entity + * @return + */ + private synchronized EntityInstantiator potentiallyCreateAndRegisterEntityInstantiator( + PersistentEntity entity) { + + Map, EntityInstantiator> map = this.entityInstantiators; + EntityInstantiator instantiator = map.get(entity.getTypeInformation()); + + if (instantiator != null) { + return instantiator; + } + + instantiator = createEntityInstantiator(entity); + + map = new HashMap, EntityInstantiator>(map); + map.put(entity.getTypeInformation(), instantiator); + + this.entityInstantiators = map; + + return instantiator; + } + + /** + * @param entity + * @return + */ + private EntityInstantiator createEntityInstantiator(PersistentEntity entity) { + + if (shouldUseReflectionEntityInstantiator(entity)) { + return ReflectionEntityInstantiator.INSTANCE; + } + + try { + return new EntityInstantiatorAdapter(createObjectInstantiator(entity)); + } catch (Throwable ex) { + return ReflectionEntityInstantiator.INSTANCE; + } + } + + /** + * @param entity + * @return + */ + private boolean shouldUseReflectionEntityInstantiator(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) this.generator.generateCustomInstantiatorClass(entity).newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Adapter to forward an invocation of the {@link EntityInstantiator} API to an {@link ObjectInstantiator}. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private static class EntityInstantiatorAdapter implements EntityInstantiator { + + private static final Object[] EMPTY_ARRAY = new Object[0]; + + private final ObjectInstantiator instantiator; + + /** + * Creates a new {@link EntityInstantiatorAdapter} for the given {@link ObjectInstantiator}. + * + * @param instantiator must not be {@literal null}. + */ + public EntityInstantiatorAdapter(ObjectInstantiator instantiator) { + this.instantiator = instantiator; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) + */ + @Override + @SuppressWarnings("unchecked") + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + Object[] params = extractInvocationArguments(entity.getPersistenceConstructor(), provider); + + try { + return (T) instantiator.newInstance(params); + } catch (Exception e) { + throw new MappingInstantiationException(entity, Arrays.asList(params), e); + } + } + + /** + * Extracts the arguments required to invoce the given constructor from the given {@link ParameterValueProvider}. + * + * @param constructor can be {@literal null}. + * @param provider can be {@literal null}. + * @return + */ + private

, T> Object[] extractInvocationArguments( + PreferredConstructor constructor, ParameterValueProvider

provider) { + + if (provider == null || constructor == null || !constructor.hasParameters()) { + return EMPTY_ARRAY; + } + + List params = new ArrayList(); + + for (Parameter parameter : constructor.getParameters()) { + params.add(provider.getParameterValue(parameter)); + } + + return params.toArray(); + } + } + + /** + * @author Thomas Darimont + */ + private 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 + */ + static class ObjectInstantiatorClassGenerator { + + 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(ClassUtils.getDefaultClassLoader()); + } + }); + } + + /** + * 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 + "'"); + } + } + + /** + * A {@link ClassLoader} that can load classes from {@code byte[]} representations. + * + * @author Thomas Darimont + */ + private 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); + } + } + } +} diff --git a/src/main/java/org/springframework/data/convert/EntityInstantiators.java b/src/main/java/org/springframework/data/convert/EntityInstantiators.java index c88025d1a..94b1d0413 100644 --- a/src/main/java/org/springframework/data/convert/EntityInstantiators.java +++ b/src/main/java/org/springframework/data/convert/EntityInstantiators.java @@ -55,7 +55,7 @@ public class EntityInstantiators { * @param customInstantiators must not be {@literal null}. */ public EntityInstantiators(Map, EntityInstantiator> customInstantiators) { - this(BytecodeGeneratingEntityInstantiator.INSTANCE, customInstantiators); + this(new ClassGeneratingEntityInstantiator(), customInstantiators); } /** diff --git a/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java index 17916b70a..b4a10f835 100644 --- a/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/convert/BytecodeGeneratingEntityInstantiatorUnitTests.java @@ -50,6 +50,7 @@ import org.springframework.util.ReflectionUtils.FieldCallback; * @author Oliver Gierke */ @RunWith(MockitoJUnitRunner.class) +@Deprecated public class BytecodeGeneratingEntityInstantiatorUnitTests

> { @Mock PersistentEntity entity; diff --git a/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java new file mode 100644 index 000000000..86c6441c8 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java @@ -0,0 +1,412 @@ +/* + * 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.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.ClassGeneratingEntityInstantiatorUnitTests.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 ClassGeneratingEntityInstantiator}. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class ClassGeneratingEntityInstantiatorUnitTests

> { + + ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator(); + + @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); + this.instance.createInstance(entity, provider); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void instantiatesArrayCorrectly() { + + when(entity.getType()).thenReturn((Class) String[][].class); + this.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 = this.instance.createInstance(entity, provider); + + assertTrue(instance instanceof Foo); + verify(provider, times(1)).getParameterValue((Parameter) constructor.getParameters().iterator().next()); + } + + /** + * @see DATACMNS-300, DATACMNS-578 + */ + @Test(expected = MappingInstantiationException.class) + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void throwsExceptionOnBeanInstantiationException() { + + when(entity.getPersistenceConstructor()).thenReturn(null); + when(entity.getType()).thenReturn((Class) PersistentEntity.class); + + this.instance.createInstance(entity, provider); + } + + /** + * @see DATACMNS-134, DATACMNS-578 + */ + @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 = this.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, DATACMNS-578 + */ + @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 { + + this.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 = this.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 = this.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 = this.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 = this.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 = this.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 = this.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 13233fee9..64f278bee 100644 --- a/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java +++ b/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java @@ -49,7 +49,7 @@ public class EntityInstantiatorsUnitTests { EntityInstantiators instantiators = new EntityInstantiators(); assertThat(instantiators.getInstantiatorFor(entity), - is((EntityInstantiator) BytecodeGeneratingEntityInstantiator.INSTANCE)); + instanceOf(ClassGeneratingEntityInstantiator.class)); } @Test