From 1d2b81bec89d5899e0019bcbf185001753bef98c Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Tue, 17 Dec 2019 13:43:51 +0100 Subject: [PATCH] DATACMNS-1639 - Introduce InstantiationAwarePropertyAccessor. We now ship and use a PropertyAccessor implementation by default that will fall back to use the persistence constructor and creating a new instance to "set" a property. This allows completely immutable types, ommitting previously needed wither methods, e.g. to populate automatically generated identifier values. This is implemented by using the EntityInstantiator available and a ParameterValueProvider that returns the value of the properties of the current instance but replacing the one for the value to be set. This requires moving EntityInstantiators, the EntityInstantiator SPI as well as all existing implementations (reflective, class generating and the Kotlin one) into the mapping.model package. Legacy instances stay around in deprecated form to avoid breaking existing clients (other store modules as well as user implementations of EntityInstantiator). The newly introduced implementations stay package protected as their sole users are now colocated in the same package. Access from the legacy types is handled via InternalEntityInstantiatorFactory, which is introduced in deprecated from for removal alongside the actual deprecations. --- src/main/asciidoc/object-mapping.adoc | 7 +- .../ClassGeneratingEntityInstantiator.java | 485 +--------------- .../data/convert/EntityInstantiator.java | 17 +- .../convert/EntityInstantiatorAdapter.java | 43 ++ .../data/convert/EntityInstantiators.java | 51 +- ...tlinClassGeneratingEntityInstantiator.java | 237 +------- .../convert/ReflectionEntityInstantiator.java | 49 +- .../context/AbstractMappingContext.java | 11 +- .../ClassGeneratingEntityInstantiator.java | 526 ++++++++++++++++++ .../mapping/model/EntityInstantiator.java | 38 ++ .../mapping/model/EntityInstantiators.java | 99 ++++ .../InstantiationAwarePropertyAccessor.java | 137 +++++ ...antiationAwarePropertyAccessorFactory.java | 55 ++ .../InternalEntityInstantiatorFactory.java | 40 ++ ...tlinClassGeneratingEntityInstantiator.java | 258 +++++++++ .../data/mapping/model/KotlinDefaultMask.java | 10 +- .../model/ReflectionEntityInstantiator.java | 82 +++ ...rePersistentPropertyAccessorUnitTests.java | 57 ++ .../model/BasicPersistentEntityUnitTests.java | 11 +- ...GeneratingEntityInstantiatorUnitTests.java | 16 +- ...gPropertyAccessorFactoryDatatypeTests.java | 5 +- ...eneratingPropertyAccessorFactoryTests.java | 9 +- .../model}/EntityInstantiatorsUnitTests.java | 12 +- ...ameterizedKotlinInstantiatorUnitTests.java | 18 +- ...ReflectionEntityInstantiatorUnitTests.java | 11 +- 25 files changed, 1443 insertions(+), 841 deletions(-) create mode 100644 src/main/java/org/springframework/data/convert/EntityInstantiatorAdapter.java create mode 100644 src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java create mode 100644 src/main/java/org/springframework/data/mapping/model/EntityInstantiator.java create mode 100644 src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java create mode 100644 src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java create mode 100644 src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessorFactory.java create mode 100644 src/main/java/org/springframework/data/mapping/model/InternalEntityInstantiatorFactory.java create mode 100644 src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java create mode 100644 src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java create mode 100644 src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java rename src/test/java/org/springframework/data/{convert => mapping/model}/ClassGeneratingEntityInstantiatorUnitTests.java (95%) rename src/test/java/org/springframework/data/{convert => mapping/model}/EntityInstantiatorsUnitTests.java (88%) rename src/test/java/org/springframework/data/{convert => mapping/model}/ParameterizedKotlinInstantiatorUnitTests.java (87%) rename src/test/java/org/springframework/data/{convert => mapping/model}/ReflectionEntityInstantiatorUnitTests.java (90%) diff --git a/src/main/asciidoc/object-mapping.adoc b/src/main/asciidoc/object-mapping.adoc index acdc5103d..1930b0110 100644 --- a/src/main/asciidoc/object-mapping.adoc +++ b/src/main/asciidoc/object-mapping.adoc @@ -75,7 +75,9 @@ For that we use the following algorithm: 1. If the property is immutable but exposes a `with…` method (see below), we use the `with…` method to create a new entity instance with the new property value. 2. If property access (i.e. access through getters and setters) is defined, we're invoking the setter method. -3. By default, we set the field value directly. +3. If the property is mutable we set the field directly. +3. If the property is immutable we're using the constructor to be used by persistence operations (see <>) to create a copy of the instance. +4. By default, we set the field value directly. [[mapping.property-population.details]] .Property population internals @@ -194,6 +196,7 @@ class Person { The class exposes a `withId(…)` method that's used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated. The original `Person` instance stays unchanged as a new one is created. The same pattern is usually applied for other properties that are store managed but might have to be changed for persistence operations. +The wither method is optional as the persistence constructor (see 6) is effectively a copy constructor and setting the property will be translated into creating a fresh instance with the new identifier value applied. <2> The `firstname` and `lastname` properties are ordinary immutable properties potentially exposed through getters. <3> The `age` property is an immutable but derived one from the `birthday` property. With the design shown, the database value will trump the defaulting as Spring Data uses the only declared constructor. @@ -214,7 +217,7 @@ Constructor-only materialization is up to 30% faster than properties population. * _Use factory methods instead of overloaded constructors to avoid ``@PersistenceConstructor``_ -- With an all-argument constructor needed for optimal performance, we usually want to expose more application use case specific constructors that omit things like auto-generated identifiers etc. It's an established pattern to rather use static factory methods to expose these variants of the all-args constructor. * _Make sure you adhere to the constraints that allow the generated instantiator and property accessor classes to be used_ -- -* _For identifiers to be generated, still use a final field in combination with a `with…` method_ -- +* _For identifiers to be generated, still use a final field in combination with an all-arguments persistence constructor (preferred) or a `with…` method_ -- * _Use Lombok to avoid boilerplate code_ -- As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repetition of boilerplate parameter to field assignments that can best be avoided by using Lombok's `@AllArgsConstructor`. [[mapping.kotlin]] diff --git a/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java index 9de562202..8a0851bc2 100644 --- a/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java @@ -15,29 +15,11 @@ */ package org.springframework.data.convert; -import static org.springframework.asm.Opcodes.*; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -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.cglib.core.ReflectUtils; 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.InternalEntityInstantiatorFactory; import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * An {@link EntityInstantiator} that can generate byte code to speed-up dynamic object instantiation. Uses the @@ -51,479 +33,28 @@ import org.springframework.util.ClassUtils; * @author Christoph Strobl * @author Mark Paluch * @since 1.11 + * @deprecated since 2.3, use {@link org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator} instead */ +@Deprecated public class ClassGeneratingEntityInstantiator implements EntityInstantiator { - private static final Object[] EMPTY_ARGS = new Object[0]; - - private final ObjectInstantiatorClassGenerator generator; - - private volatile Map, EntityInstantiator> entityInstantiators = new HashMap<>(32); + private final org.springframework.data.mapping.model.EntityInstantiator delegate = InternalEntityInstantiatorFactory + .getClassGeneratingEntityInstantiator(); /** * Creates a new {@link ClassGeneratingEntityInstantiator}. */ public ClassGeneratingEntityInstantiator() { - this.generator = new ObjectInstantiatorClassGenerator(); + super(); } /* * (non-Javadoc) - * @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) + * @see org.springframework.data.mapping.model.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<>(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 doCreateEntityInstantiator(entity); - } catch (Throwable ex) { - return ReflectionEntityInstantiator.INSTANCE; - } - } - - /** - * @param entity - * @return - */ - protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { - return new EntityInstantiatorAdapter(createObjectInstantiator(entity, entity.getPersistenceConstructor())); - } - - /** - * @param entity - * @return - */ - boolean shouldUseReflectionEntityInstantiator(PersistentEntity entity) { - - Class type = entity.getType(); - - if (type.isInterface() // - || type.isArray() // - || Modifier.isPrivate(type.getModifiers()) // - || (type.isMemberClass() && !Modifier.isStatic(type.getModifiers())) // - || ClassUtils.isCglibProxyClass(type)) { // - return true; - } - - PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); - if (persistenceConstructor == null || Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) { - return true; - } - - if (!ClassUtils.isPresent(ObjectInstantiator.class.getName(), type.getClassLoader())) { - return true; - } - - return false; - } - - /** - * Allocates an object array for instance creation. - * - * @param argumentCount - * @return - * @since 2.0 - */ - static Object[] allocateArguments(int argumentCount) { - return argumentCount == 0 ? EMPTY_ARGS : new Object[argumentCount]; - } - - /** - * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and - * {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per - * {@link PersistentEntity}. - * - * @param entity - * @param constructor - * @return - */ - ObjectInstantiator createObjectInstantiator(PersistentEntity entity, - @Nullable PreferredConstructor constructor) { - - try { - return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).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 - * @author Mark Paluch - */ - private static class EntityInstantiatorAdapter implements EntityInstantiator { - - 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 invoke 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( - @Nullable PreferredConstructor constructor, ParameterValueProvider

provider) { - - if (constructor == null || !constructor.hasParameters()) { - return allocateArguments(0); - } - - Object[] params = allocateArguments(constructor.getConstructor().getParameterCount()); - - int index = 0; - for (Parameter parameter : constructor.getParameters()) { - params[index++] = provider.getParameterValue(parameter); - } - - return params; - } - } - - /** - * Needs to be public as otherwise the implementation class generated does not see the interface from the classloader. - * - * @author Thomas Darimont - * @author Oliver Gierke - */ - 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 - * @author Mark Paluch - */ - 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) }; - - - /** - * Generate a new class for the given {@link PersistentEntity}. - * - * @param entity - * @param constructor - * @return - */ - public Class generateCustomInstantiatorClass(PersistentEntity entity, - @Nullable PreferredConstructor constructor) { - - String className = generateClassName(entity); - byte[] bytecode = generateBytecode(className, entity, constructor); - - Class type = entity.getType(); - - try { - return ReflectUtils.defineClass(className, bytecode, type.getClassLoader(), type.getProtectionDomain(), type); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - /** - * @param entity - * @return - */ - private String generateClassName(PersistentEntity entity) { - return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); - } - - /** - * Generate a new class for the given {@link PersistentEntity}. - * - * @param internalClassName - * @param entity - * @param constructor - * @return - */ - public byte[] generateBytecode(String internalClassName, PersistentEntity entity, - @Nullable PreferredConstructor constructor) { - - 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, constructor); - - 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 - * @param constructor - */ - private void visitCreateMethod(ClassWriter cw, PersistentEntity entity, - @Nullable PreferredConstructor constructor) { - - 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); - - if (constructor != null) { - - Constructor ctor = constructor.getConstructor(); - Class[] parameterTypes = ctor.getParameterTypes(); - List> parameters = constructor.getParameters(); - - for (int i = 0; i < parameterTypes.length; i++) { - - mv.visitVarInsn(ALOAD, 1); - - visitArrayIndex(mv, i); - - mv.visitInsn(AALOAD); - - if (parameterTypes[i].isPrimitive()) { - - mv.visitInsn(DUP); - String parameterName = parameters.size() > i ? parameters.get(i).getName() : null; - - insertAssertNotNull(mv, parameterName == null ? String.format("at index %d", i) : parameterName); - 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 not-{@literal null} assertion for a parameter. - * - * @param mv the method visitor into which instructions should be inserted - * @param parameterName name of the parameter to create the appropriate assertion message. - */ - private static void insertAssertNotNull(MethodVisitor mv, String parameterName) { - - // Assert.notNull(property) - mv.visitLdcInsn(String.format("Parameter %s must not be null!", parameterName)); - mv.visitMethodInsn(INVOKESTATIC, "org/springframework/util/Assert", "notNull", - String.format("(%s%s)V", String.format("L%s;", JAVA_LANG_OBJECT), "Ljava/lang/String;"), false); - } - - /** - * 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 + "'"); - } - } + return delegate.createInstance(entity, provider); } } diff --git a/src/main/java/org/springframework/data/convert/EntityInstantiator.java b/src/main/java/org/springframework/data/convert/EntityInstantiator.java index d9017cc58..b8a0583a3 100644 --- a/src/main/java/org/springframework/data/convert/EntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/EntityInstantiator.java @@ -16,23 +16,12 @@ package org.springframework.data.convert; import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.model.ParameterValueProvider; /** * SPI to abstract strategies to create instances for {@link PersistentEntity}s. * * @author Oliver Gierke + * @since 2.3, use {@link org.springframework.data.mapping.model.EntityInstantiator} instead. */ -public interface EntityInstantiator { - - /** - * Creates a new instance of the given entity using the given source to pull data from. - * - * @param entity will not be {@literal null}. - * @param provider will not be {@literal null}. - * @return - */ - , P extends PersistentProperty

> T createInstance(E entity, - ParameterValueProvider

provider); -} +@Deprecated +public interface EntityInstantiator extends org.springframework.data.mapping.model.EntityInstantiator {} diff --git a/src/main/java/org/springframework/data/convert/EntityInstantiatorAdapter.java b/src/main/java/org/springframework/data/convert/EntityInstantiatorAdapter.java new file mode 100644 index 000000000..965daebb8 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/EntityInstantiatorAdapter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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 + * + * https://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 lombok.RequiredArgsConstructor; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.model.ParameterValueProvider; + +/** + * Adapter to bridge calls from the new APIs back to the legacy ones. + * + * @author Oliver Drotbohm + */ +@RequiredArgsConstructor +class EntityInstantiatorAdapter implements EntityInstantiator { + + private final org.springframework.data.mapping.model.EntityInstantiator delegate; + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) + */ + @Override + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + return delegate.createInstance(entity, provider); + } +} diff --git a/src/main/java/org/springframework/data/convert/EntityInstantiators.java b/src/main/java/org/springframework/data/convert/EntityInstantiators.java index 41bb60b7d..4eb45d05f 100644 --- a/src/main/java/org/springframework/data/convert/EntityInstantiators.java +++ b/src/main/java/org/springframework/data/convert/EntityInstantiators.java @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.Map; import org.springframework.data.mapping.PersistentEntity; -import org.springframework.util.Assert; +import org.springframework.data.mapping.model.InternalEntityInstantiatorFactory; /** * Simple value object allowing access to {@link EntityInstantiator} instances for a given type falling back to a @@ -29,17 +29,16 @@ import org.springframework.util.Assert; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @deprecated since 2.3, use {@link org.springframework.data.mapping.model.EntityInstantiators} instead. */ -public class EntityInstantiators { - - private final EntityInstantiator fallback; - private final Map, EntityInstantiator> customInstantiators; +@Deprecated +public class EntityInstantiators extends org.springframework.data.mapping.model.EntityInstantiators { /** * Creates a new {@link EntityInstantiators} using the default fallback instantiator and no custom ones. */ public EntityInstantiators() { - this(Collections.emptyMap()); + super(); } /** @@ -47,8 +46,8 @@ public class EntityInstantiators { * * @param fallback must not be {@literal null}. */ - public EntityInstantiators(EntityInstantiator fallback) { - this(fallback, Collections.emptyMap()); + public EntityInstantiators(org.springframework.data.mapping.model.EntityInstantiator fallback) { + super(fallback, Collections.emptyMap()); } /** @@ -56,8 +55,9 @@ public class EntityInstantiators { * * @param customInstantiators must not be {@literal null}. */ - public EntityInstantiators(Map, EntityInstantiator> customInstantiators) { - this(new KotlinClassGeneratingEntityInstantiator(), customInstantiators); + public EntityInstantiators( + Map, org.springframework.data.mapping.model.EntityInstantiator> customInstantiators) { + super(InternalEntityInstantiatorFactory.getKotlinClassGeneratingEntityInstantiator(), customInstantiators); } /** @@ -67,32 +67,17 @@ public class EntityInstantiators { * @param defaultInstantiator must not be {@literal null}. * @param customInstantiators must not be {@literal null}. */ - public EntityInstantiators(EntityInstantiator defaultInstantiator, - Map, EntityInstantiator> customInstantiators) { - - Assert.notNull(defaultInstantiator, "DefaultInstantiator must not be null!"); - Assert.notNull(customInstantiators, "CustomInstantiators must not be null!"); - - this.fallback = defaultInstantiator; - this.customInstantiators = customInstantiators; + public EntityInstantiators(org.springframework.data.mapping.model.EntityInstantiator defaultInstantiator, + Map, org.springframework.data.mapping.model.EntityInstantiator> customInstantiators) { + super(defaultInstantiator, customInstantiators); } - /** - * Returns the {@link EntityInstantiator} to be used to create the given {@link PersistentEntity}. - * - * @param entity must not be {@literal null}. - * @return will never be {@literal null}. + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.EntityInstantiators#getInstantiatorFor(org.springframework.data.mapping.PersistentEntity) */ + @Override public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { - - Assert.notNull(entity, "Entity must not be null!"); - Class type = entity.getType(); - - if (!customInstantiators.containsKey(type)) { - return fallback; - } - - EntityInstantiator instantiator = customInstantiators.get(entity.getType()); - return instantiator == null ? fallback : instantiator; + return new EntityInstantiatorAdapter(super.getInstantiatorFor(entity)); } } diff --git a/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java index e4becca06..b1e4b3181 100644 --- a/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java @@ -15,24 +15,10 @@ */ package org.springframework.data.convert; -import kotlin.reflect.KFunction; -import kotlin.reflect.KParameter; -import kotlin.reflect.jvm.ReflectJvmMapping; - -import java.lang.reflect.Constructor; -import java.util.Arrays; -import java.util.List; -import java.util.stream.IntStream; - 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.KotlinDefaultMask; -import org.springframework.data.mapping.model.MappingInstantiationException; +import org.springframework.data.mapping.model.InternalEntityInstantiatorFactory; import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.util.ReflectionUtils; -import org.springframework.lang.Nullable; /** * Kotlin-specific extension to {@link ClassGeneratingEntityInstantiator} that adapts Kotlin constructors with @@ -41,221 +27,22 @@ import org.springframework.lang.Nullable; * @author Mark Paluch * @author Oliver Gierke * @since 2.0 + * @deprecated since 2.3, use {@link org.springframework.data.mapping.model.KotlinClassGeneratingEntityInstantiator} + * instead. */ -public class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInstantiator { +@Deprecated +public class KotlinClassGeneratingEntityInstantiator implements EntityInstantiator { + + private final org.springframework.data.mapping.model.EntityInstantiator delegate = InternalEntityInstantiatorFactory + .getKotlinClassGeneratingEntityInstantiator(); /* * (non-Javadoc) - * @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity) + * @see org.springframework.data.mapping.model.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) */ @Override - protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { - - PreferredConstructor constructor = entity.getPersistenceConstructor(); - - if (ReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) { - - PreferredConstructor defaultConstructor = new DefaultingKotlinConstructorResolver(entity) - .getDefaultConstructor(); - - if (defaultConstructor != null) { - - ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor); - - return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor); - } - } - - return super.doCreateEntityInstantiator(entity); - } - - /** - * Resolves a {@link PreferredConstructor} to a synthetic Kotlin constructor accepting the same user-space parameters - * suffixed by Kotlin-specifics required for defaulting and the {@code kotlin.jvm.internal.DefaultConstructorMarker}. - * - * @since 2.0 - * @author Mark Paluch - */ - static class DefaultingKotlinConstructorResolver { - - private final @Nullable PreferredConstructor defaultConstructor; - - @SuppressWarnings("unchecked") - DefaultingKotlinConstructorResolver(PersistentEntity entity) { - - Constructor hit = resolveDefaultConstructor(entity); - PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); - - if (hit != null && persistenceConstructor != null) { - this.defaultConstructor = new PreferredConstructor<>(hit, - persistenceConstructor.getParameters().toArray(new Parameter[0])); - } else { - this.defaultConstructor = null; - } - } - - @Nullable - private static Constructor resolveDefaultConstructor(PersistentEntity entity) { - - PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); - - if (persistenceConstructor == null) { - return null; - } - - Constructor hit = null; - Constructor constructor = persistenceConstructor.getConstructor(); - - for (Constructor candidate : entity.getType().getDeclaredConstructors()) { - - // use only synthetic constructors - if (!candidate.isSynthetic()) { - continue; - } - - // candidates must contain at least two additional parameters (int, DefaultConstructorMarker). - // Number of defaulting masks derives from the original constructor arg count - int syntheticParameters = KotlinDefaultMask.getMaskCount(constructor.getParameterCount()) - + /* DefaultConstructorMarker */ 1; - - if (constructor.getParameterCount() + syntheticParameters != candidate.getParameterCount()) { - continue; - } - - java.lang.reflect.Parameter[] constructorParameters = constructor.getParameters(); - java.lang.reflect.Parameter[] candidateParameters = candidate.getParameters(); - - if (!candidateParameters[candidateParameters.length - 1].getType().getName() - .equals("kotlin.jvm.internal.DefaultConstructorMarker")) { - continue; - } - - if (parametersMatch(constructorParameters, candidateParameters)) { - hit = candidate; - break; - } - } - - return hit; - } - - private static boolean parametersMatch(java.lang.reflect.Parameter[] constructorParameters, - java.lang.reflect.Parameter[] candidateParameters) { - - return IntStream.range(0, constructorParameters.length) - .allMatch(i -> constructorParameters[i].getType().equals(candidateParameters[i].getType())); - } - - @Nullable - PreferredConstructor getDefaultConstructor() { - return defaultConstructor; - } - } - - /** - * Entity instantiator for Kotlin constructors that apply parameter defaulting. Kotlin constructors that apply - * argument defaulting are marked with {@link kotlin.jvm.internal.DefaultConstructorMarker} and accept additional - * parameters besides the regular (user-space) parameters. Additional parameters are: - *

    - *
  • defaulting bitmask ({@code int}), a bit mask slot for each 32 parameters
  • - *
  • {@code DefaultConstructorMarker} (usually null)
  • - *
- * Defaulting bitmask - *

- * The defaulting bitmask is a 32 bit integer representing which positional argument should be defaulted. Defaulted - * arguments are passed as {@literal null} and require the appropriate positional bit set ( {@code 1 << 2} for the 2. - * argument)). Since the bitmask represents only 32 bit states, it requires additional masks (slots) if more than 32 - * arguments are represented. - * - * @author Mark Paluch - * @since 2.0 - */ - static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantiator { - - private final ObjectInstantiator instantiator; - private final KFunction constructor; - private final List kParameters; - private final Constructor synthetic; - - DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator, PreferredConstructor constructor) { - - KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor.getConstructor()); - - if (kotlinConstructor == null) { - throw new IllegalArgumentException( - "No corresponding Kotlin constructor found for " + constructor.getConstructor()); - } - - this.instantiator = instantiator; - this.constructor = kotlinConstructor; - this.kParameters = kotlinConstructor.getParameters(); - this.synthetic = constructor.getConstructor(); - } - - /* - * (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); - } - } - - private

, T> Object[] extractInvocationArguments( - @Nullable PreferredConstructor preferredConstructor, ParameterValueProvider

provider) { - - if (preferredConstructor == null) { - throw new IllegalArgumentException("PreferredConstructor must not be null!"); - } - - Object[] params = allocateArguments( - synthetic.getParameterCount() + KotlinDefaultMask.getMaskCount(synthetic.getParameterCount()) + /* DefaultConstructorMarker */1); - int userParameterCount = kParameters.size(); - - List> parameters = preferredConstructor.getParameters(); - - // Prepare user-space arguments - for (int i = 0; i < userParameterCount; i++) { - - Parameter parameter = parameters.get(i); - params[i] = provider.getParameterValue(parameter); - } - - KotlinDefaultMask defaultMask = KotlinDefaultMask.from(constructor, it -> { - - int index = kParameters.indexOf(it); - - Parameter parameter = parameters.get(index); - Class type = parameter.getType().getType(); - - if (it.isOptional() && params[index] == null) { - if (type.isPrimitive()) { - - // apply primitive defaulting to prevent NPE on primitive downcast - params[index] = ReflectionUtils.getPrimitiveDefault(type); - } - return false; - } - - return true; - }); - - int[] defaulting = defaultMask.getDefaulting(); - // append nullability masks to creation arguments - for (int i = 0; i < defaulting.length; i++) { - params[userParameterCount + i] = defaulting[i]; - } - - return params; - } + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + return delegate.createInstance(entity, provider); } } diff --git a/src/main/java/org/springframework/data/convert/ReflectionEntityInstantiator.java b/src/main/java/org/springframework/data/convert/ReflectionEntityInstantiator.java index 286e3bfb9..c4fbf4581 100644 --- a/src/main/java/org/springframework/data/convert/ReflectionEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/ReflectionEntityInstantiator.java @@ -15,18 +15,10 @@ */ package org.springframework.data.convert; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; - -import org.springframework.beans.BeanInstantiationException; -import org.springframework.beans.BeanUtils; 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.InternalEntityInstantiatorFactory; import org.springframework.data.mapping.model.ParameterValueProvider; /** @@ -35,50 +27,15 @@ import org.springframework.data.mapping.model.ParameterValueProvider; * * @author Oliver Gierke * @author Mark Paluch + * @deprecated since 2.3, use {@link org.springframework.data.mapping.model.ReflectionEntityInstantiator} instead. */ public enum ReflectionEntityInstantiator implements EntityInstantiator { INSTANCE; - private static final Object[] EMPTY_ARGS = new Object[0]; - - @SuppressWarnings("unchecked") public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { - PreferredConstructor constructor = entity.getPersistenceConstructor(); - - if (constructor == null) { - - try { - Class clazz = entity.getType(); - if (clazz.isArray()) { - Class ctype = clazz; - int dims = 0; - while (ctype.isArray()) { - ctype = ctype.getComponentType(); - dims++; - } - return (T) Array.newInstance(clazz, dims); - } else { - return BeanUtils.instantiateClass(entity.getType()); - } - } catch (BeanInstantiationException e) { - throw new MappingInstantiationException(entity, Collections.emptyList(), e); - } - } - int parameterCount = constructor.getConstructor().getParameterCount(); - - Object[] params = parameterCount == 0 ? EMPTY_ARGS : new Object[parameterCount]; - int i = 0; - for (Parameter parameter : constructor.getParameters()) { - params[i++] = provider.getParameterValue(parameter); - } - - try { - return BeanUtils.instantiateClass(constructor.getConstructor(), params); - } catch (BeanInstantiationException e) { - throw new MappingInstantiationException(entity, new ArrayList<>(Arrays.asList(params)), e); - } + return InternalEntityInstantiatorFactory.getReflectionEntityInstantiator().createInstance(entity, provider); } } diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index ae196304b..0ca8755bf 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -48,6 +48,8 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.InstantiationAwarePropertyAccessorFactory; import org.springframework.data.mapping.model.MutablePersistentEntity; import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory; import org.springframework.data.mapping.model.Property; @@ -87,7 +89,7 @@ public abstract class AbstractMappingContext NONE = Optional.empty(); private final Map, Optional> persistentEntities = new HashMap<>(); - private final PersistentPropertyAccessorFactory persistentPropertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory(); + private final PersistentPropertyAccessorFactory persistentPropertyAccessorFactory; private final PersistentPropertyPathFactory persistentPropertyPathFactory; private @Nullable ApplicationEventPublisher applicationEventPublisher; @@ -102,7 +104,14 @@ public abstract class AbstractMappingContext(this); + + EntityInstantiators instantiators = new EntityInstantiators(); + ClassGeneratingPropertyAccessorFactory accessorFactory = new ClassGeneratingPropertyAccessorFactory(); + + this.persistentPropertyAccessorFactory = new InstantiationAwarePropertyAccessorFactory(accessorFactory, + instantiators); } /* diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java new file mode 100644 index 000000000..d24186290 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java @@ -0,0 +1,526 @@ +/* + * Copyright 2015-2020 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 + * + * https://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.mapping.model; + +import static org.springframework.asm.Opcodes.*; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +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.cglib.core.ReflectUtils; +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.util.TypeInformation; +import org.springframework.lang.Nullable; +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 + * @author Phillip Webb + * @author Christoph Strobl + * @author Mark Paluch + * @since 1.11 + */ +class ClassGeneratingEntityInstantiator implements EntityInstantiator { + + private static final Object[] EMPTY_ARGS = new Object[0]; + + private final ObjectInstantiatorClassGenerator generator; + + private volatile Map, EntityInstantiator> entityInstantiators = new HashMap<>(32); + + /** + * Creates a new {@link ClassGeneratingEntityInstantiator}. + */ + 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<>(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 doCreateEntityInstantiator(entity); + } catch (Throwable ex) { + return ReflectionEntityInstantiator.INSTANCE; + } + } + + /** + * @param entity + * @return + */ + protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { + return new EntityInstantiatorAdapter(createObjectInstantiator(entity, entity.getPersistenceConstructor())); + } + + /** + * @param entity + * @return + */ + boolean shouldUseReflectionEntityInstantiator(PersistentEntity entity) { + + Class type = entity.getType(); + + if (type.isInterface() // + || type.isArray() // + || Modifier.isPrivate(type.getModifiers()) // + || type.isMemberClass() && !Modifier.isStatic(type.getModifiers()) // + || ClassUtils.isCglibProxyClass(type)) { // + return true; + } + + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + if (persistenceConstructor == null || Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) { + return true; + } + + if (!ClassUtils.isPresent(ObjectInstantiator.class.getName(), type.getClassLoader())) { + return true; + } + + return false; + } + + /** + * Allocates an object array for instance creation. + * + * @param argumentCount + * @return + * @since 2.0 + */ + static Object[] allocateArguments(int argumentCount) { + return argumentCount == 0 ? EMPTY_ARGS : new Object[argumentCount]; + } + + /** + * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and + * {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per + * {@link PersistentEntity}. + * + * @param entity + * @param constructor + * @return + */ + ObjectInstantiator createObjectInstantiator(PersistentEntity entity, + @Nullable PreferredConstructor constructor) { + + try { + return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).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 + * @author Mark Paluch + */ + private static class EntityInstantiatorAdapter implements EntityInstantiator { + + 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 invoke 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( + @Nullable PreferredConstructor constructor, ParameterValueProvider

provider) { + + if (constructor == null || !constructor.hasParameters()) { + return allocateArguments(0); + } + + Object[] params = allocateArguments(constructor.getConstructor().getParameterCount()); + + int index = 0; + for (Parameter parameter : constructor.getParameters()) { + params[index++] = provider.getParameterValue(parameter); + } + + return params; + } + } + + /** + * Needs to be public as otherwise the implementation class generated does not see the interface from the classloader. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + 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 + * @author Mark Paluch + */ + 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) }; + + /** + * Generate a new class for the given {@link PersistentEntity}. + * + * @param entity + * @param constructor + * @return + */ + public Class generateCustomInstantiatorClass(PersistentEntity entity, + @Nullable PreferredConstructor constructor) { + + String className = generateClassName(entity); + byte[] bytecode = generateBytecode(className, entity, constructor); + + Class type = entity.getType(); + + try { + return ReflectUtils.defineClass(className, bytecode, type.getClassLoader(), type.getProtectionDomain(), type); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * @param entity + * @return + */ + private String generateClassName(PersistentEntity entity) { + return entity.getType().getName() + TAG + Integer.toString(entity.hashCode(), 36); + } + + /** + * Generate a new class for the given {@link PersistentEntity}. + * + * @param internalClassName + * @param entity + * @param constructor + * @return + */ + public byte[] generateBytecode(String internalClassName, PersistentEntity entity, + @Nullable PreferredConstructor constructor) { + + 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, constructor); + + 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 + * @param constructor + */ + private void visitCreateMethod(ClassWriter cw, PersistentEntity entity, + @Nullable PreferredConstructor constructor) { + + 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); + + if (constructor != null) { + + Constructor ctor = constructor.getConstructor(); + Class[] parameterTypes = ctor.getParameterTypes(); + List> parameters = constructor.getParameters(); + + for (int i = 0; i < parameterTypes.length; i++) { + + mv.visitVarInsn(ALOAD, 1); + + visitArrayIndex(mv, i); + + mv.visitInsn(AALOAD); + + if (parameterTypes[i].isPrimitive()) { + + mv.visitInsn(DUP); + String parameterName = parameters.size() > i ? parameters.get(i).getName() : null; + + insertAssertNotNull(mv, parameterName == null ? String.format("at index %d", i) : parameterName); + 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 not-{@literal null} assertion for a parameter. + * + * @param mv the method visitor into which instructions should be inserted + * @param parameterName name of the parameter to create the appropriate assertion message. + */ + private static void insertAssertNotNull(MethodVisitor mv, String parameterName) { + + // Assert.notNull(property) + mv.visitLdcInsn(String.format("Parameter %s must not be null!", parameterName)); + mv.visitMethodInsn(INVOKESTATIC, "org/springframework/util/Assert", "notNull", + String.format("(%s%s)V", String.format("L%s;", JAVA_LANG_OBJECT), "Ljava/lang/String;"), false); + } + + /** + * 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/mapping/model/EntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/EntityInstantiator.java new file mode 100644 index 000000000..e522bbfa1 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/EntityInstantiator.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.mapping.model; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; + +/** + * SPI to abstract strategies to create instances for {@link PersistentEntity}s. + * + * @author Oliver Drotbohm + * @since 2.3 + */ +public interface EntityInstantiator { + + /** + * Creates a new instance of the given entity using the given source to pull data from. + * + * @param entity will not be {@literal null}. + * @param provider will not be {@literal null}. + * @return + */ + , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider); +} diff --git a/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java b/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java new file mode 100644 index 000000000..f686848ef --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/EntityInstantiators.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.mapping.model; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.util.Assert; + +/** + * Simple value object allowing access to {@link EntityInstantiator} instances for a given type falling back to a + * default one. + * + * @author Oliver Drotbohm + * @author Thomas Darimont + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.3 + */ +public class EntityInstantiators { + + private final EntityInstantiator fallback; + private final Map, EntityInstantiator> customInstantiators; + + /** + * Creates a new {@link EntityInstantiators} using the default fallback instantiator and no custom ones. + */ + public EntityInstantiators() { + this(Collections.emptyMap()); + } + + /** + * Creates a new {@link EntityInstantiators} using the given {@link EntityInstantiator} as fallback. + * + * @param fallback must not be {@literal null}. + */ + public EntityInstantiators(EntityInstantiator fallback) { + this(fallback, Collections.emptyMap()); + } + + /** + * Creates a new {@link EntityInstantiators} using the default fallback instantiator and the given custom ones. + * + * @param customInstantiators must not be {@literal null}. + */ + public EntityInstantiators(Map, EntityInstantiator> customInstantiators) { + this(new KotlinClassGeneratingEntityInstantiator(), customInstantiators); + } + + /** + * Creates a new {@link EntityInstantiator} using the given fallback {@link EntityInstantiator} and the given custom + * ones. + * + * @param defaultInstantiator must not be {@literal null}. + * @param customInstantiators must not be {@literal null}. + */ + public EntityInstantiators(EntityInstantiator defaultInstantiator, + Map, EntityInstantiator> customInstantiators) { + + Assert.notNull(defaultInstantiator, "DefaultInstantiator must not be null!"); + Assert.notNull(customInstantiators, "CustomInstantiators must not be null!"); + + this.fallback = defaultInstantiator; + this.customInstantiators = customInstantiators; + } + + /** + * Returns the {@link EntityInstantiator} to be used to create the given {@link PersistentEntity}. + * + * @param entity must not be {@literal null}. + * @return will never be {@literal null}. + */ + public EntityInstantiator getInstantiatorFor(PersistentEntity entity) { + + Assert.notNull(entity, "Entity must not be null!"); + Class type = entity.getType(); + + if (!customInstantiators.containsKey(type)) { + return fallback; + } + + EntityInstantiator instantiator = customInstantiators.get(entity.getType()); + return instantiator == null ? fallback : instantiator; + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java new file mode 100644 index 000000000..3035053a3 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java @@ -0,0 +1,137 @@ +/* + * Copyright 2019 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 + * + * https://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.mapping.model; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link PersistentPropertyAccessor} that will use an entity's {@link PersistenceConstructor} to create a new + * instance of it to apply a new value for a given {@link PersistentProperty}. Will only be used if the + * {@link PersistentProperty} is to be applied on a completely immutable entity type exposing a persistence constructor. + * + * @author Oliver Drotbohm + */ +public class InstantiationAwarePropertyAccessor implements PersistentPropertyAccessor { + + private static final String NO_SETTER_OR_CONSTRUCTOR = "Cannot set property %s because no setter, wither or copy constructor exists for %s!"; + private static final String NO_CONSTRUCTOR_PARAMETER = "Cannot set property %s because no setter, no wither and it's not part of the persistence constructor %s!"; + + private final PersistentPropertyAccessor delegate; + private final EntityInstantiators instantiators; + + private T bean; + + /** + * Creates an {@link InstantiationAwarePropertyAccessor} using the given delegate {@link PersistentPropertyAccessor} + * and {@link EntityInstantiators}. + * + * @param delegate must not be {@literal null}. + * @param instantiators must not be {@literal null}. + */ + public InstantiationAwarePropertyAccessor(PersistentPropertyAccessor delegate, EntityInstantiators instantiators) { + + Assert.notNull(delegate, "Delegate PersistenPropertyAccessor must not be null!"); + Assert.notNull(instantiators, "EntityInstantiators must not be null!"); + + this.delegate = delegate; + this.instantiators = instantiators; + this.bean = delegate.getBean(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentPropertyAccessor#setProperty(org.springframework.data.mapping.PersistentProperty, java.lang.Object) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public void setProperty(PersistentProperty property, @Nullable Object value) { + + PersistentEntity owner = property.getOwner(); + + if (!property.isImmutable() || property.getWither() != null || ReflectionUtils.isKotlinClass(owner.getType())) { + + delegate.setProperty(property, value); + this.bean = delegate.getBean(); + + return; + } + + PreferredConstructor constructor = owner.getPersistenceConstructor(); + + if (constructor == null) { + throw new IllegalStateException(String.format(NO_SETTER_OR_CONSTRUCTOR, property.getName(), owner.getType())); + } + + if (!constructor.isConstructorParameter(property)) { + throw new IllegalStateException( + String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), constructor.getConstructor())); + } + + constructor.getParameters().stream().forEach(it -> { + + if (it.getName() == null) { + throw new IllegalStateException( + String.format("Cannot detect parameter names of copy constructor of %s!", owner.getType())); + } + }); + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(owner); + + this.bean = (T) instantiator.createInstance(owner, new ParameterValueProvider() { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + @Nullable + @SuppressWarnings("null") + public Object getParameterValue(Parameter parameter) { + + return property.getName().equals(parameter.getName()) // + ? value + : delegate.getProperty(owner.getRequiredPersistentProperty(parameter.getName())); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentPropertyAccessor#getProperty(org.springframework.data.mapping.PersistentProperty) + */ + @Nullable + @Override + public Object getProperty(PersistentProperty property) { + return delegate.getProperty(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentPropertyAccessor#getBean() + */ + @Override + public T getBean() { + return this.bean; + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessorFactory.java b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessorFactory.java new file mode 100644 index 000000000..56011c86d --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessorFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2020 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 + * + * https://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.mapping.model; + +import lombok.RequiredArgsConstructor; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; + +/** + * Delegating {@link PersistentPropertyAccessorFactory} decorating the {@link PersistentPropertyAccessor}s created with + * an {@link InstantiationAwarePropertyAccessor} to allow the handling of purely immutable types. + * + * @author Oliver Drotbohm + */ +@RequiredArgsConstructor +public class InstantiationAwarePropertyAccessorFactory implements PersistentPropertyAccessorFactory { + + private final PersistentPropertyAccessorFactory delegate; + private final EntityInstantiators instantiators; + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.PersistentPropertyAccessorFactory#getPropertyAccessor(org.springframework.data.mapping.PersistentEntity, java.lang.Object) + */ + @Override + public PersistentPropertyAccessor getPropertyAccessor(PersistentEntity entity, T bean) { + + PersistentPropertyAccessor accessor = delegate.getPropertyAccessor(entity, bean); + + return new InstantiationAwarePropertyAccessor<>(accessor, instantiators); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.PersistentPropertyAccessorFactory#isSupported(org.springframework.data.mapping.PersistentEntity) + */ + @Override + public boolean isSupported(PersistentEntity entity) { + return delegate.isSupported(entity); + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/InternalEntityInstantiatorFactory.java b/src/main/java/org/springframework/data/mapping/model/InternalEntityInstantiatorFactory.java new file mode 100644 index 000000000..11e80128c --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/InternalEntityInstantiatorFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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 + * + * https://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.mapping.model; + +/** + * Factory to expose the new package-protected {@link EntityInstantiator} implementations for the legacy types. To be + * removed in 2.4. + * + * @author Oliver Drotbohm + * @since 2.3 + * @deprecated since 2.3 as it's only a bridge from the legacy types towards the new, package-protected implementation. + */ +@Deprecated +public class InternalEntityInstantiatorFactory { + + public static EntityInstantiator getClassGeneratingEntityInstantiator() { + return new ClassGeneratingEntityInstantiator(); + } + + public static EntityInstantiator getKotlinClassGeneratingEntityInstantiator() { + return new KotlinClassGeneratingEntityInstantiator(); + } + + public static EntityInstantiator getReflectionEntityInstantiator() { + return ReflectionEntityInstantiator.INSTANCE; + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java new file mode 100644 index 000000000..4b93c9009 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java @@ -0,0 +1,258 @@ +/* + * Copyright 2017-2020 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 + * + * https://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.mapping.model; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +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.util.ReflectionUtils; +import org.springframework.lang.Nullable; + +/** + * Kotlin-specific extension to {@link ClassGeneratingEntityInstantiator} that adapts Kotlin constructors with + * defaulting. + * + * @author Mark Paluch + * @author Oliver Gierke + * @since 2.0 + */ +class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInstantiator { + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity) + */ + @Override + protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { + + PreferredConstructor constructor = entity.getPersistenceConstructor(); + + if (ReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) { + + PreferredConstructor defaultConstructor = new DefaultingKotlinConstructorResolver(entity) + .getDefaultConstructor(); + + if (defaultConstructor != null) { + + ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor); + + return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor); + } + } + + return super.doCreateEntityInstantiator(entity); + } + + /** + * Resolves a {@link PreferredConstructor} to a synthetic Kotlin constructor accepting the same user-space parameters + * suffixed by Kotlin-specifics required for defaulting and the {@code kotlin.jvm.internal.DefaultConstructorMarker}. + * + * @since 2.0 + * @author Mark Paluch + */ + static class DefaultingKotlinConstructorResolver { + + private final @Nullable PreferredConstructor defaultConstructor; + + @SuppressWarnings("unchecked") + DefaultingKotlinConstructorResolver(PersistentEntity entity) { + + Constructor hit = resolveDefaultConstructor(entity); + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + + if (hit != null && persistenceConstructor != null) { + this.defaultConstructor = new PreferredConstructor<>(hit, + persistenceConstructor.getParameters().toArray(new Parameter[0])); + } else { + this.defaultConstructor = null; + } + } + + @Nullable + private static Constructor resolveDefaultConstructor(PersistentEntity entity) { + + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + + if (persistenceConstructor == null) { + return null; + } + + Constructor hit = null; + Constructor constructor = persistenceConstructor.getConstructor(); + + for (Constructor candidate : entity.getType().getDeclaredConstructors()) { + + // use only synthetic constructors + if (!candidate.isSynthetic()) { + continue; + } + + // candidates must contain at least two additional parameters (int, DefaultConstructorMarker). + // Number of defaulting masks derives from the original constructor arg count + int syntheticParameters = KotlinDefaultMask.getMaskCount(constructor.getParameterCount()) + + /* DefaultConstructorMarker */ 1; + + if (constructor.getParameterCount() + syntheticParameters != candidate.getParameterCount()) { + continue; + } + + java.lang.reflect.Parameter[] constructorParameters = constructor.getParameters(); + java.lang.reflect.Parameter[] candidateParameters = candidate.getParameters(); + + if (!candidateParameters[candidateParameters.length - 1].getType().getName() + .equals("kotlin.jvm.internal.DefaultConstructorMarker")) { + continue; + } + + if (parametersMatch(constructorParameters, candidateParameters)) { + hit = candidate; + break; + } + } + + return hit; + } + + private static boolean parametersMatch(java.lang.reflect.Parameter[] constructorParameters, + java.lang.reflect.Parameter[] candidateParameters) { + + return IntStream.range(0, constructorParameters.length) + .allMatch(i -> constructorParameters[i].getType().equals(candidateParameters[i].getType())); + } + + @Nullable + PreferredConstructor getDefaultConstructor() { + return defaultConstructor; + } + } + + /** + * Entity instantiator for Kotlin constructors that apply parameter defaulting. Kotlin constructors that apply + * argument defaulting are marked with {@link kotlin.jvm.internal.DefaultConstructorMarker} and accept additional + * parameters besides the regular (user-space) parameters. Additional parameters are: + *

    + *
  • defaulting bitmask ({@code int}), a bit mask slot for each 32 parameters
  • + *
  • {@code DefaultConstructorMarker} (usually null)
  • + *
+ * Defaulting bitmask + *

+ * The defaulting bitmask is a 32 bit integer representing which positional argument should be defaulted. Defaulted + * arguments are passed as {@literal null} and require the appropriate positional bit set ( {@code 1 << 2} for the 2. + * argument)). Since the bitmask represents only 32 bit states, it requires additional masks (slots) if more than 32 + * arguments are represented. + * + * @author Mark Paluch + * @since 2.0 + */ + static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantiator { + + private final ObjectInstantiator instantiator; + private final KFunction constructor; + private final List kParameters; + private final Constructor synthetic; + + DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator, PreferredConstructor constructor) { + + KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor.getConstructor()); + + if (kotlinConstructor == null) { + throw new IllegalArgumentException( + "No corresponding Kotlin constructor found for " + constructor.getConstructor()); + } + + this.instantiator = instantiator; + this.constructor = kotlinConstructor; + this.kParameters = kotlinConstructor.getParameters(); + this.synthetic = constructor.getConstructor(); + } + + /* + * (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); + } + } + + private

, T> Object[] extractInvocationArguments( + @Nullable PreferredConstructor preferredConstructor, ParameterValueProvider

provider) { + + if (preferredConstructor == null) { + throw new IllegalArgumentException("PreferredConstructor must not be null!"); + } + + Object[] params = allocateArguments(synthetic.getParameterCount() + + KotlinDefaultMask.getMaskCount(synthetic.getParameterCount()) + /* DefaultConstructorMarker */1); + int userParameterCount = kParameters.size(); + + List> parameters = preferredConstructor.getParameters(); + + // Prepare user-space arguments + for (int i = 0; i < userParameterCount; i++) { + + Parameter parameter = parameters.get(i); + params[i] = provider.getParameterValue(parameter); + } + + KotlinDefaultMask defaultMask = KotlinDefaultMask.from(constructor, it -> { + + int index = kParameters.indexOf(it); + + Parameter parameter = parameters.get(index); + Class type = parameter.getType().getType(); + + if (it.isOptional() && params[index] == null) { + if (type.isPrimitive()) { + + // apply primitive defaulting to prevent NPE on primitive downcast + params[index] = ReflectionUtils.getPrimitiveDefault(type); + } + return false; + } + + return true; + }); + + int[] defaulting = defaultMask.getDefaulting(); + // append nullability masks to creation arguments + for (int i = 0; i < defaulting.length; i++) { + params[userParameterCount + i] = defaulting[i]; + } + + return params; + } + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java b/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java index 8851a978a..fff593070 100644 --- a/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java +++ b/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java @@ -15,11 +15,6 @@ */ package org.springframework.data.mapping.model; -import java.util.ArrayList; -import java.util.List; -import java.util.function.IntConsumer; -import java.util.function.Predicate; - import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.KParameter.Kind; @@ -27,6 +22,11 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntConsumer; +import java.util.function.Predicate; + /** * Value object representing defaulting masks used for Kotlin methods applying parameter defaulting. * diff --git a/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java new file mode 100644 index 000000000..00eea5d64 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.mapping.model; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; +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; + +/** + * {@link EntityInstantiator} that uses the {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an + * instance of the entity via reflection. + * + * @author Oliver Gierke + * @author Mark Paluch + */ +enum ReflectionEntityInstantiator implements EntityInstantiator { + + INSTANCE; + + private static final Object[] EMPTY_ARGS = new Object[0]; + + @SuppressWarnings("unchecked") + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + PreferredConstructor constructor = entity.getPersistenceConstructor(); + + if (constructor == null) { + + try { + Class clazz = entity.getType(); + if (clazz.isArray()) { + Class ctype = clazz; + int dims = 0; + while (ctype.isArray()) { + ctype = ctype.getComponentType(); + dims++; + } + return (T) Array.newInstance(clazz, dims); + } else { + return BeanUtils.instantiateClass(entity.getType()); + } + } catch (BeanInstantiationException e) { + throw new MappingInstantiationException(entity, Collections.emptyList(), e); + } + } + int parameterCount = constructor.getConstructor().getParameterCount(); + + Object[] params = parameterCount == 0 ? EMPTY_ARGS : new Object[parameterCount]; + int i = 0; + for (Parameter parameter : constructor.getParameters()) { + params[i++] = provider.getParameterValue(parameter); + } + + try { + return BeanUtils.instantiateClass(constructor.getConstructor(), params); + } catch (BeanInstantiationException e) { + throw new MappingInstantiationException(entity, new ArrayList<>(Arrays.asList(params)), e); + } + } +} diff --git a/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java b/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java new file mode 100644 index 000000000..7bc810bd2 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/InstantiationAwarePersistentPropertyAccessorUnitTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 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 + * + * https://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.mapping; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Value; + +import org.junit.Test; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.context.SampleMappingContext; +import org.springframework.data.mapping.context.SamplePersistentProperty; +import org.springframework.data.mapping.model.InstantiationAwarePropertyAccessor; + +/** + * @author Oliver Drotbohm + */ +public class InstantiationAwarePersistentPropertyAccessorUnitTests { + + @Test + public void testname() { + + EntityInstantiators instantiators = new EntityInstantiators(); + SampleMappingContext context = new SampleMappingContext(); + + PersistentEntity entity = context.getRequiredPersistentEntity(Sample.class); + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(new Sample("Dave", "Matthews", 42)); + + PersistentPropertyAccessor wrapper = new InstantiationAwarePropertyAccessor<>(accessor, + instantiators); + + wrapper.setProperty(entity.getRequiredPersistentProperty("firstname"), "Oliver August"); + + assertThat(wrapper.getBean()).isEqualTo(new Sample("Oliver August", "Matthews", 42)); + } + + @Value + static class Sample { + + String firstname, lastname; + int age; + } +} diff --git a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java index 668965752..523fda604 100755 --- a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java @@ -35,7 +35,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType.Type; @@ -196,8 +195,14 @@ public class BasicPersistentEntityUnitTests> { PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); assertThat(accessor).isNotInstanceOf(BeanWrapper.class); - assertThat(accessor.getClass().getName()).contains("_Accessor_"); - assertThat(accessor.getBean()).isEqualTo(value); + + assertThat(accessor).isInstanceOfSatisfying(InstantiationAwarePropertyAccessor.class, it -> { + + PersistentPropertyAccessor delegate = (PersistentPropertyAccessor) ReflectionTestUtils.getField(it, "delegate"); + + assertThat(delegate.getClass().getName()).contains("_Accessor_"); + assertThat(delegate.getBean()).isEqualTo(value); + }); } @Test // DATACMNS-596 diff --git a/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java similarity index 95% rename from src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java rename to src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java index d744d474e..873e5b1cb 100755 --- a/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.convert; +package org.springframework.data.mapping.model; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.data.util.ClassTypeInformation.from; @@ -29,16 +30,12 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.classloadersupport.HidingClassLoader; -import org.springframework.data.convert.ClassGeneratingEntityInstantiator.ObjectInstantiator; -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.data.mapping.model.ClassGeneratingEntityInstantiator.ObjectInstantiator; +import org.springframework.data.mapping.model.ClassGeneratingEntityInstantiatorUnitTests.Outer.Inner; import org.springframework.util.ReflectionUtils; /** @@ -363,11 +360,10 @@ public class ClassGeneratingEntityInstantiatorUnitTests

entityType = classLoader - .loadClass("org.springframework.data.mapping.model.PersistentPropertyAccessorTests$ClassLoaderTest"); + Class entityType = classLoader.loadClass("org.springframework.data.mapping.Person"); prepareMocks(entityType); diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java index 60bae7910..aba7f4f1d 100755 --- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryDatatypeTests.java @@ -139,7 +139,10 @@ public class ClassGeneratingPropertyAccessorFactoryDatatypeTests { .getRequiredPersistentEntity(bean.getClass()); assertThat(ReflectionTestUtils.getField(persistentEntity, "propertyAccessorFactory")) - .isInstanceOf(ClassGeneratingPropertyAccessorFactory.class); + .isInstanceOfSatisfying(InstantiationAwarePropertyAccessorFactory.class, it -> { + assertThat(ReflectionTestUtils.getField(it, "delegate")) + .isInstanceOf(ClassGeneratingPropertyAccessorFactory.class); + }); } private PersistentPropertyAccessor getPersistentPropertyAccessor(Object bean) { diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java index 2b5938fcd..060471a09 100755 --- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingPropertyAccessorFactoryTests.java @@ -150,7 +150,7 @@ public class ClassGeneratingPropertyAccessorFactoryTests { assertThat(getProperty(new Dummy(), "dummy")) .satisfies(property -> assertThatExceptionOfType(UnsupportedOperationException.class)// - .isThrownBy(() -> getPersistentPropertyAccessor(bean).getProperty(property))); + .isThrownBy(() -> getPersistentPropertyAccessor(bean).getProperty(property))); } @Test // DATACMNS-809 @@ -158,7 +158,7 @@ public class ClassGeneratingPropertyAccessorFactoryTests { assertThat(getProperty(new Dummy(), "dummy")) .satisfies(property -> assertThatExceptionOfType(UnsupportedOperationException.class)// - .isThrownBy(() -> getPersistentPropertyAccessor(bean).setProperty(property, Optional.empty()))); + .isThrownBy(() -> getPersistentPropertyAccessor(bean).setProperty(property, Optional.empty()))); } @Test // DATACMNS-809 @@ -168,7 +168,10 @@ public class ClassGeneratingPropertyAccessorFactoryTests { .getRequiredPersistentEntity(bean.getClass()); assertThat(ReflectionTestUtils.getField(persistentEntity, "propertyAccessorFactory")) - .isInstanceOf(ClassGeneratingPropertyAccessorFactory.class); + .isInstanceOfSatisfying(InstantiationAwarePropertyAccessorFactory.class, it -> { + assertThat(ReflectionTestUtils.getField(it, "delegate")) + .isInstanceOf(ClassGeneratingPropertyAccessorFactory.class); + }); } private PersistentPropertyAccessor getPersistentPropertyAccessor(Object bean) { diff --git a/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java similarity index 88% rename from src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java rename to src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java index 5647e9a17..ad040998d 100755 --- a/src/test/java/org/springframework/data/convert/EntityInstantiatorsUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.convert; +package org.springframework.data.mapping.model; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -55,8 +55,7 @@ public class EntityInstantiatorsUnitTests { doReturn(String.class).when(entity).getType(); - Map, EntityInstantiator> customInstantiators = Collections - .singletonMap(String.class, customInstantiator); + Map, EntityInstantiator> customInstantiators = Collections.singletonMap(String.class, customInstantiator); EntityInstantiators instantiators = new EntityInstantiators(customInstantiators); assertThat(instantiators.getInstantiatorFor(entity)).isEqualTo(customInstantiator); @@ -67,8 +66,8 @@ public class EntityInstantiatorsUnitTests { doReturn(Object.class).when(entity).getType(); - Map, EntityInstantiator> customInstantiators = Collections - .singletonMap(String.class, ReflectionEntityInstantiator.INSTANCE); + Map, EntityInstantiator> customInstantiators = Collections.singletonMap(String.class, + ReflectionEntityInstantiator.INSTANCE); EntityInstantiators instantiators = new EntityInstantiators(customInstantiator, customInstantiators); instantiators.getInstantiatorFor(entity); @@ -76,7 +75,6 @@ public class EntityInstantiatorsUnitTests { assertThat(instantiators.getInstantiatorFor(entity)).isEqualTo(customInstantiator); doReturn(String.class).when(entity).getType(); - assertThat(instantiators.getInstantiatorFor(entity)) - .isEqualTo((EntityInstantiator) ReflectionEntityInstantiator.INSTANCE); + assertThat(instantiators.getInstantiatorFor(entity)).isEqualTo(ReflectionEntityInstantiator.INSTANCE); } } diff --git a/src/test/java/org/springframework/data/convert/ParameterizedKotlinInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java similarity index 87% rename from src/test/java/org/springframework/data/convert/ParameterizedKotlinInstantiatorUnitTests.java rename to src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java index 8d1d00cad..22fbdcecb 100644 --- a/src/test/java/org/springframework/data/convert/ParameterizedKotlinInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.convert; +package org.springframework.data.mapping.model; import static org.assertj.core.api.Assertions.*; @@ -30,14 +30,13 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.context.SampleMappingContext; import org.springframework.data.mapping.context.SamplePersistentProperty; -import org.springframework.data.mapping.model.BasicPersistentEntity; -import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.With32Args; import org.springframework.data.mapping.model.With33Args; import org.springframework.test.util.ReflectionTestUtils; /** - * Unit test to verify correct object instantiation using Kotlin defaulting via {@link KotlinClassGeneratingEntityInstantiator}. + * Unit test to verify correct object instantiation using Kotlin defaulting via + * {@link KotlinClassGeneratingEntityInstantiator}. * * @author Mark Paluch */ @@ -51,7 +50,9 @@ public class ParameterizedKotlinInstantiatorUnitTests { private final String propertyUnderTestName; private final EntityInstantiator entityInstantiator; - public ParameterizedKotlinInstantiatorUnitTests(PersistentEntity entity, int propertyCount, int propertyUnderTestIndex, String propertyUnderTestName, EntityInstantiator entityInstantiator, String label) { + public ParameterizedKotlinInstantiatorUnitTests(PersistentEntity entity, + int propertyCount, int propertyUnderTestIndex, String propertyUnderTestName, + EntityInstantiator entityInstantiator, String label) { this.entity = entity; this.propertyCount = propertyCount; this.propertyUnderTestIndex = propertyUnderTestIndex; @@ -76,13 +77,16 @@ public class ParameterizedKotlinInstantiatorUnitTests { return fixtures; } - private static List createFixture(SampleMappingContext context, Class entityType, int propertyCount, EntityInstantiator entityInstantiator) { + private static List createFixture(SampleMappingContext context, Class entityType, int propertyCount, + EntityInstantiator entityInstantiator) { BasicPersistentEntity persistentEntity = context.getPersistentEntity(entityType); return IntStream.range(0, propertyCount).mapToObj(i -> { - return new Object[]{persistentEntity, propertyCount, i, Integer.toString(i), entityInstantiator, String.format("Property %d for %s using %s", i, entityType.getSimpleName(), entityInstantiator.getClass().getSimpleName())}; + return new Object[] { persistentEntity, propertyCount, i, Integer.toString(i), entityInstantiator, + String.format("Property %d for %s using %s", i, entityType.getSimpleName(), + entityInstantiator.getClass().getSimpleName()) }; }).collect(Collectors.toList()); } diff --git a/src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java similarity index 90% rename from src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java rename to src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java index b1a9be4f2..c1560d298 100755 --- a/src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.convert; +package org.springframework.data.mapping.model; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import static org.springframework.data.convert.ReflectionEntityInstantiator.*; +import static org.springframework.data.mapping.model.ReflectionEntityInstantiator.*; import static org.springframework.data.util.ClassTypeInformation.from; import java.lang.reflect.Constructor; @@ -28,15 +29,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.convert.ReflectionEntityInstantiatorUnitTests.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.data.mapping.model.ReflectionEntityInstantiatorUnitTests.Outer.Inner; import org.springframework.util.ReflectionUtils; /**