diff --git a/src/main/asciidoc/object-mapping.adoc b/src/main/asciidoc/object-mapping.adoc
index 2e01cdfd4..5f8592996 100644
--- a/src/main/asciidoc/object-mapping.adoc
+++ b/src/main/asciidoc/object-mapping.adoc
@@ -17,12 +17,13 @@ This means we need two fundamental steps:
Spring Data automatically tries to detect a persistent entity's constructor to be used to materialize objects of that type.
The resolution algorithm works as follows:
-1. If there is a single constructor, it is used.
-2. If there are multiple constructors and exactly one is annotated with `@PersistenceConstructor`, it is used.
-3. If there's a no-argument constructor, it is used.
+1. If there is a single static factory method annotated with `@FactoryMethod` then it is used.
+2. If there is a single constructor, it is used.
+3. If there are multiple constructors and exactly one is annotated with `@PersistenceConstructor`, it is used.
+4. If there's a no-argument constructor, it is used.
Other constructors will be ignored.
-The value resolution assumes constructor argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
+The value resolution assumes constructor/factory method argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
This also requires either parameter names information available in the class file or an `@ConstructorProperties` annotation being present on the constructor.
The value resolution can be customized by using Spring Framework's `@Value` value annotation using a store-specific SpEL expression.
@@ -206,6 +207,7 @@ Even if the intent is that the calculation should be preferred, it's important t
<6> The class exposes a factory method and a constructor for object creation.
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through `@PersistenceConstructor`.
Instead, defaulting of properties is handled within the factory method.
+If you want Spring Data to use the factory method for object instantiation, annotate it with `@FactoryMethod`.
[[mapping.general-recommendations]]
== General recommendations
diff --git a/src/main/java/org/springframework/data/annotation/EntityCreatorAnnotation.java b/src/main/java/org/springframework/data/annotation/EntityCreatorAnnotation.java
new file mode 100644
index 000000000..d1132955c
--- /dev/null
+++ b/src/main/java/org/springframework/data/annotation/EntityCreatorAnnotation.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011-2021 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation to declare a constructor or factory method annotation as factory/preferred constructor annotation.
+ *
+ * @author Mark Paluch
+ * @since 3.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.ANNOTATION_TYPE })
+public @interface EntityCreatorAnnotation {
+}
diff --git a/src/main/java/org/springframework/data/annotation/FactoryMethod.java b/src/main/java/org/springframework/data/annotation/FactoryMethod.java
new file mode 100644
index 000000000..9bccfe565
--- /dev/null
+++ b/src/main/java/org/springframework/data/annotation/FactoryMethod.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2011-2021 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to declare a {@code static} method as factory method for class instantiation.
+ *
+ * @author Mark Paluch
+ * @since 3.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+@EntityCreatorAnnotation
+public @interface FactoryMethod {
+}
diff --git a/src/main/java/org/springframework/data/annotation/PersistenceConstructor.java b/src/main/java/org/springframework/data/annotation/PersistenceConstructor.java
index ae2cf26ba..a44afb790 100644
--- a/src/main/java/org/springframework/data/annotation/PersistenceConstructor.java
+++ b/src/main/java/org/springframework/data/annotation/PersistenceConstructor.java
@@ -21,9 +21,13 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
+ * Annotation to declare a constructor for instantiation.
+ *
* @author Jon Brisbin
+ * @author Mark Paluch
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.CONSTRUCTOR)
+@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
+@EntityCreatorAnnotation
public @interface PersistenceConstructor {
}
diff --git a/src/main/java/org/springframework/data/mapping/EntityCreatorMetadata.java b/src/main/java/org/springframework/data/mapping/EntityCreatorMetadata.java
new file mode 100644
index 000000000..bdb3a41b0
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/EntityCreatorMetadata.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 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 java.util.List;
+
+/**
+ * Metadata describing a mechanism to create an entity instance.
+ *
+ * @author Mark Paluch
+ * @since 3.0
+ */
+public interface EntityCreatorMetadata
> {
+
+ /**
+ * Check whether the given {@link PersistentProperty} is being used as creator parameter.
+ *
+ * @param property
+ * @return
+ */
+ boolean isCreatorParameter(PersistentProperty> property);
+
+ /**
+ * Returns whether the given {@link Parameter} is one referring to parent value (such as an enclosing class or a
+ * receiver parameter).
+ *
+ * @param parameter
+ * @return
+ */
+ default boolean isParentParameter(Parameter, P> parameter) {
+ return false;
+ }
+
+ /**
+ * @return the number of parameters.
+ */
+ default int getParameterCount() {
+ return getParameters().size();
+ }
+
+ /**
+ * @return the parameters used by this creator.
+ */
+ List> getParameters();
+
+ /**
+ * @return whether the creator accepts {@link Parameter}s.
+ */
+ default boolean hasParameters() {
+ return !getParameters().isEmpty();
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/EntityCreatorMetadataSupport.java b/src/main/java/org/springframework/data/mapping/EntityCreatorMetadataSupport.java
new file mode 100644
index 000000000..c4fb4697f
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/EntityCreatorMetadataSupport.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2022 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 java.lang.reflect.Executable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.util.Assert;
+
+/**
+ * Value object to encapsulate the entity creation mechanism through a {@link Executable} to be used when mapping
+ * persistent data to objects.
+ *
+ * @author Mark Paluch
+ * @since 3.0
+ */
+class EntityCreatorMetadataSupport> implements EntityCreatorMetadata {
+
+ private final Executable executable;
+ private final List> parameters;
+ private final Map, Boolean> isPropertyParameterCache = new ConcurrentHashMap<>();
+
+ /**
+ * Creates a new {@link EntityCreatorMetadataSupport} from the given {@link Executable} and {@link Parameter}s.
+ *
+ * @param executable must not be {@literal null}.
+ * @param parameters must not be {@literal null}.
+ */
+ @SafeVarargs
+ public EntityCreatorMetadataSupport(Executable executable, Parameter... parameters) {
+
+ Assert.notNull(executable, "Executable must not be null!");
+ Assert.notNull(parameters, "Parameters must not be null!");
+
+ this.executable = executable;
+ this.parameters = Arrays.asList(parameters);
+ }
+
+ /**
+ * Returns the underlying {@link Executable} that can be invoked reflectively.
+ *
+ * @return
+ */
+ Executable getExecutable() {
+ return executable;
+ }
+
+ /**
+ * Returns the {@link Parameter}s of the executable.
+ *
+ * @return
+ */
+ public List> getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Returns whether the given {@link PersistentProperty} is referenced in a creator argument of the
+ * {@link PersistentEntity} backing this {@link EntityCreatorMetadataSupport}.
+ *
+ * Results of this call are cached and reused on the next invocation. Calling this method for a
+ * {@link PersistentProperty} that was not yet added to its owning {@link PersistentEntity} will capture that state
+ * and return the same result after adding {@link PersistentProperty} to its entity.
+ *
+ * @param property must not be {@literal null}.
+ * @return {@literal true} if the {@link PersistentProperty} is used in the creator.
+ */
+ @Override
+ public boolean isCreatorParameter(PersistentProperty> property) {
+
+ Assert.notNull(property, "Property must not be null!");
+
+ Boolean cached = isPropertyParameterCache.get(property);
+
+ if (cached != null) {
+ return cached;
+ }
+
+ boolean result = doGetIsCreatorParameter(property);
+
+ isPropertyParameterCache.put(property, result);
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return executable.toString();
+ }
+
+ private boolean doGetIsCreatorParameter(PersistentProperty> property) {
+
+ for (Parameter, P> parameter : parameters) {
+ if (parameter.maps(property)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/FactoryMethod.java b/src/main/java/org/springframework/data/mapping/FactoryMethod.java
new file mode 100644
index 000000000..f636f7bd3
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/FactoryMethod.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2011-2021 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 java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Value object to encapsulate the factory method to be used when mapping persistent data to objects.
+ *
+ * @author Mark Paluch
+ * @since 3.0
+ */
+public final class FactoryMethod> extends EntityCreatorMetadataSupport {
+
+ /**
+ * Creates a new {@link FactoryMethod} from the given {@link Constructor} and {@link Parameter}s.
+ *
+ * @param factoryMethod must not be {@literal null}.
+ * @param parameters must not be {@literal null}.
+ */
+ @SafeVarargs
+ public FactoryMethod(Method factoryMethod, Parameter... parameters) {
+
+ super(factoryMethod, parameters);
+ ReflectionUtils.makeAccessible(factoryMethod);
+ }
+
+ /**
+ * Returns the underlying {@link Constructor}.
+ *
+ * @return
+ */
+ public Method getFactoryMethod() {
+ return (Method) getExecutable();
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/mapping/Parameter.java b/src/main/java/org/springframework/data/mapping/Parameter.java
new file mode 100644
index 000000000..ebcd605de
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/Parameter.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2021 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 java.lang.annotation.Annotation;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.data.util.Lazy;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Value object to represent constructor parameters.
+ *
+ * @param the type of the parameter
+ * @author Oliver Gierke
+ */
+public class Parameter> {
+
+ private final @Nullable String name;
+ private final TypeInformation type;
+ private final MergedAnnotations annotations;
+ private final String key;
+ private final @Nullable PersistentEntity entity;
+
+ private final Lazy enclosingClassCache;
+ private final Lazy hasSpelExpression;
+
+ /**
+ * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of
+ * {@link Annotation}s. Will inspect the annotations for an {@link Value} annotation to lookup a key or an SpEL
+ * expression to be evaluated.
+ *
+ * @param name the name of the parameter, can be {@literal null}
+ * @param type must not be {@literal null}
+ * @param annotations must not be {@literal null} but can be empty
+ * @param entity must not be {@literal null}.
+ */
+ public Parameter(@Nullable String name, TypeInformation type, Annotation[] annotations,
+ @Nullable PersistentEntity entity) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(annotations, "Annotations must not be null!");
+
+ this.name = name;
+ this.type = type;
+ this.annotations = MergedAnnotations.from(annotations);
+ this.key = getValue(this.annotations);
+ this.entity = entity;
+
+ this.enclosingClassCache = Lazy.of(() -> {
+
+ if (entity == null) {
+ throw new IllegalStateException();
+ }
+
+ Class> owningType = entity.getType();
+
+ return owningType.isMemberClass() && type.getType().equals(owningType.getEnclosingClass());
+ });
+
+ this.hasSpelExpression = Lazy.of(() -> StringUtils.hasText(getSpelExpression()));
+ }
+
+ @Nullable
+ private static String getValue(MergedAnnotations annotations) {
+
+ return annotations.get(Value.class) //
+ .getValue("value", String.class) //
+ .filter(StringUtils::hasText) //
+ .orElse(null);
+ }
+
+ /**
+ * Returns the name of the parameter.
+ *
+ * @return
+ */
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the {@link TypeInformation} of the parameter.
+ *
+ * @return
+ */
+ public TypeInformation getType() {
+ return type;
+ }
+
+ /**
+ * Merged annotations that this parameter is annotated with.
+ *
+ * @return
+ * @since 2.5
+ */
+ public MergedAnnotations getAnnotations() {
+ return annotations;
+ }
+
+ /**
+ * Returns the raw resolved type of the parameter.
+ *
+ * @return
+ */
+ public Class getRawType() {
+ return type.getType();
+ }
+
+ /**
+ * Returns the key to be used when looking up a source data structure to populate the actual parameter value.
+ *
+ * @return
+ */
+ public String getSpelExpression() {
+ return key;
+ }
+
+ /**
+ * Returns whether the constructor parameter is equipped with a SpEL expression.
+ *
+ * @return
+ */
+ public boolean hasSpelExpression() {
+ return this.hasSpelExpression.get();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof Parameter, ?>)) {
+ return false;
+ }
+
+ Parameter, ?> parameter = (Parameter, ?>) o;
+
+ if (!ObjectUtils.nullSafeEquals(name, parameter.name)) {
+ return false;
+ }
+
+ if (!ObjectUtils.nullSafeEquals(type, parameter.type)) {
+ return false;
+ }
+
+ if (!ObjectUtils.nullSafeEquals(key, parameter.key)) {
+ return false;
+ }
+
+ return ObjectUtils.nullSafeEquals(entity, parameter.entity);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ int result = ObjectUtils.nullSafeHashCode(name);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(type);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(key);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(entity);
+ return result;
+ }
+
+ /**
+ * Returns whether the {@link Parameter} maps the given {@link PersistentProperty}.
+ *
+ * @param property
+ * @return
+ */
+ boolean maps(PersistentProperty> property) {
+
+ PersistentEntity entity = this.entity;
+ String name = this.name;
+
+ PersistentProperty> referencedProperty = entity == null //
+ ? null //
+ : name == null ? null : entity.getPersistentProperty(name);
+
+ return property.equals(referencedProperty);
+ }
+
+ boolean isEnclosingClassParameter() {
+ return enclosingClassCache.get();
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/PersistentEntity.java b/src/main/java/org/springframework/data/mapping/PersistentEntity.java
index 833ee40b7..11d9de611 100644
--- a/src/main/java/org/springframework/data/mapping/PersistentEntity.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentEntity.java
@@ -49,10 +49,24 @@ public interface PersistentEntity> extends It
* indicates that the instantiation of the object of that persistent entity is done through either a customer
* {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom conversion
* mechanisms entirely.
+ * @deprecated since 3.0, use {@link #getEntityCreator()}.
*/
@Nullable
+ @Deprecated
PreferredConstructor getPersistenceConstructor();
+ /**
+ * Returns the {@link EntityCreatorMetadata} to be used to instantiate objects of this {@link PersistentEntity}.
+ *
+ * @return {@literal null} in case no suitable creation mechanism for automatic construction can be found. This
+ * usually indicates that the instantiation of the object of that persistent entity is done through either a
+ * customer {@link org.springframework.data.mapping.model.EntityInstantiator} or handled by custom conversion
+ * mechanisms entirely.
+ * @since 3.0
+ */
+ @Nullable
+ EntityCreatorMetadata getEntityCreator();
+
/**
* Returns whether the given {@link PersistentProperty} is referred to by a constructor argument of the
* {@link PersistentEntity}.
@@ -60,8 +74,22 @@ public interface PersistentEntity> extends It
* @param property can be {@literal null}.
* @return true if the given {@link PersistentProperty} is referred to by a constructor argument or {@literal false}
* if not or {@literal null}.
+ * @deprecated since 3.0, use {@link #isCreatorArgument(PersistentProperty)} instead.
*/
- boolean isConstructorArgument(PersistentProperty> property);
+ @Deprecated
+ default boolean isConstructorArgument(PersistentProperty> property) {
+ return isCreatorArgument(property);
+ }
+
+ /**
+ * Returns whether the given {@link PersistentProperty} is referred to by a creator argument of the
+ * {@link PersistentEntity}.
+ *
+ * @param property can be {@literal null}.
+ * @return true if the given {@link PersistentProperty} is referred to by a creator argument or {@literal false} if
+ * not or {@literal null}.
+ */
+ boolean isCreatorArgument(PersistentProperty> property);
/**
* Returns whether the given {@link PersistentProperty} is the id property of the entity.
diff --git a/src/main/java/org/springframework/data/mapping/PreferredConstructor.java b/src/main/java/org/springframework/data/mapping/PreferredConstructor.java
index 8ac155a56..4ef733003 100644
--- a/src/main/java/org/springframework/data/mapping/PreferredConstructor.java
+++ b/src/main/java/org/springframework/data/mapping/PreferredConstructor.java
@@ -15,25 +15,14 @@
*/
package org.springframework.data.mapping;
-import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.annotation.PersistenceConstructor;
-import org.springframework.data.convert.WritingConverter;
-import org.springframework.data.util.Lazy;
-import org.springframework.data.util.TypeInformation;
-import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
-import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
-import org.springframework.util.StringUtils;
/**
* Value object to encapsulate the constructor to be used when mapping persistent data to objects.
@@ -46,11 +35,9 @@ import org.springframework.util.StringUtils;
* @author Myeonghyeon Lee
* @author Xeno Amess
*/
-public class PreferredConstructor> {
+public final class PreferredConstructor> extends EntityCreatorMetadataSupport {
- private final Constructor constructor;
private final List> parameters;
- private final Map, Boolean> isPropertyParameterCache = new ConcurrentHashMap<>();
/**
* Creates a new {@link PreferredConstructor} from the given {@link Constructor} and {@link Parameter}s.
@@ -61,11 +48,9 @@ public class PreferredConstructor> {
@SafeVarargs
public PreferredConstructor(Constructor constructor, Parameter... parameters) {
- Assert.notNull(constructor, "Constructor must not be null!");
- Assert.notNull(parameters, "Parameters must not be null!");
+ super(constructor, parameters);
ReflectionUtils.makeAccessible(constructor);
- this.constructor = constructor;
this.parameters = Arrays.asList(parameters);
}
@@ -75,27 +60,9 @@ public class PreferredConstructor> {
* @return
*/
public Constructor getConstructor() {
- return constructor;
+ return (Constructor) getExecutable();
}
- /**
- * Returns the {@link Parameter}s of the constructor.
- *
- * @return
- */
- public List> getParameters() {
- return parameters;
- }
-
- /**
- * Returns whether the constructor has {@link Parameter}s.
- *
- * @see #isNoArgConstructor()
- * @return
- */
- public boolean hasParameters() {
- return !parameters.isEmpty();
- }
/**
* Returns whether the constructor does not have any arguments.
@@ -104,7 +71,7 @@ public class PreferredConstructor> {
* @return
*/
public boolean isNoArgConstructor() {
- return parameters.isEmpty();
+ return !hasParameters();
}
/**
@@ -113,41 +80,22 @@ public class PreferredConstructor> {
* @return
*/
public boolean isExplicitlyAnnotated() {
- return AnnotationUtils.findAnnotation(constructor, PersistenceConstructor.class) != null;
+ return MergedAnnotations.from(getExecutable()).isPresent(PersistenceConstructor.class);
}
/**
- * Returns whether the given {@link PersistentProperty} is referenced in a constructor argument of the
- * {@link PersistentEntity} backing this {@link PreferredConstructor}.
- *
- * Results of this call are cached and reused on the next invocation. Calling this method for a
- * {@link PersistentProperty} that was not yet added to its owning {@link PersistentEntity} will capture that state
- * and return the same result after adding {@link PersistentProperty} to its entity.
- *
- * @param property must not be {@literal null}.
- * @return {@literal true} if the {@link PersistentProperty} is used in the constructor.
+ * @param property
+ * @return
+ * @deprecated since 3.0, use {@link #isCreatorParameter(PersistentProperty)} instead.
*/
+ @Deprecated
public boolean isConstructorParameter(PersistentProperty> property) {
+ return isCreatorParameter(property);
+ }
+ @Override
- Assert.notNull(property, "Property must not be null!");
-
- Boolean cached = isPropertyParameterCache.get(property);
-
- if (cached != null) {
- return cached;
- }
-
- boolean result = false;
- for (Parameter, P> parameter : parameters) {
- if (parameter.maps(property)) {
- result = true;
- break;
- }
- }
-
- isPropertyParameterCache.put(property, result);
-
- return result;
+ public boolean isParentParameter(Parameter, P> parameter) {
+ return isEnclosingClassParameter(parameter);
}
/**
@@ -169,186 +117,4 @@ public class PreferredConstructor> {
return parameters.get(0).equals(parameter);
}
- /**
- * Value object to represent constructor parameters.
- *
- * @param the type of the parameter
- * @author Oliver Gierke
- */
- public static class Parameter> {
-
- private final @Nullable String name;
- private final TypeInformation type;
- private final MergedAnnotations annotations;
- private final String key;
- private final @Nullable PersistentEntity entity;
-
- private final Lazy enclosingClassCache;
- private final Lazy hasSpelExpression;
-
- /**
- * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of
- * {@link Annotation}s. Will inspect the annotations for an {@link Value} annotation to lookup a key or an SpEL
- * expression to be evaluated.
- *
- * @param name the name of the parameter, can be {@literal null}
- * @param type must not be {@literal null}
- * @param annotations must not be {@literal null} but can be empty
- * @param entity must not be {@literal null}.
- */
- public Parameter(@Nullable String name, TypeInformation type, Annotation[] annotations,
- @Nullable PersistentEntity entity) {
-
- Assert.notNull(type, "Type must not be null!");
- Assert.notNull(annotations, "Annotations must not be null!");
-
- this.name = name;
- this.type = type;
- this.annotations = MergedAnnotations.from(annotations);
- this.key = getValue(this.annotations);
- this.entity = entity;
-
- this.enclosingClassCache = Lazy.of(() -> {
-
- if (entity == null) {
- throw new IllegalStateException();
- }
-
- Class owningType = entity.getType();
- return owningType.isMemberClass() && type.getType().equals(owningType.getEnclosingClass());
- });
-
- this.hasSpelExpression = Lazy.of(() -> StringUtils.hasText(getSpelExpression()));
- }
-
- @Nullable
- private static String getValue(MergedAnnotations annotations) {
-
- return annotations.get(Value.class) //
- .getValue("value", String.class) //
- .filter(StringUtils::hasText) //
- .orElse(null);
- }
-
- /**
- * Returns the name of the parameter.
- *
- * @return
- */
- @Nullable
- public String getName() {
- return name;
- }
-
- /**
- * Returns the {@link TypeInformation} of the parameter.
- *
- * @return
- */
- public TypeInformation getType() {
- return type;
- }
-
- /**
- * Merged annotations that this parameter is annotated with.
- *
- * @return
- * @since 2.5
- */
- public MergedAnnotations getAnnotations() {
- return annotations;
- }
-
- /**
- * Returns the raw resolved type of the parameter.
- *
- * @return
- */
- public Class getRawType() {
- return type.getType();
- }
-
- /**
- * Returns the key to be used when looking up a source data structure to populate the actual parameter value.
- *
- * @return
- */
- public String getSpelExpression() {
- return key;
- }
-
- /**
- * Returns whether the constructor parameter is equipped with a SpEL expression.
- *
- * @return
- */
- public boolean hasSpelExpression() {
- return this.hasSpelExpression.get();
- }
-
- /*
- * (non-Javadoc)
- * @see java.lang.Object#equals(java.lang.Object)
- */
- @Override
- public boolean equals(Object o) {
-
- if (this == o) {
- return true;
- }
-
- if (!(o instanceof Parameter)) {
- return false;
- }
-
- Parameter, ?> parameter = (Parameter, ?>) o;
-
- if (!ObjectUtils.nullSafeEquals(name, parameter.name)) {
- return false;
- }
-
- if (!ObjectUtils.nullSafeEquals(type, parameter.type)) {
- return false;
- }
-
- if (!ObjectUtils.nullSafeEquals(key, parameter.key)) {
- return false;
- }
-
- return ObjectUtils.nullSafeEquals(entity, parameter.entity);
- }
-
- /*
- * (non-Javadoc)
- * @see java.lang.Object#hashCode()
- */
- @Override
- public int hashCode() {
- int result = ObjectUtils.nullSafeHashCode(name);
- result = 31 * result + ObjectUtils.nullSafeHashCode(type);
- result = 31 * result + ObjectUtils.nullSafeHashCode(key);
- result = 31 * result + ObjectUtils.nullSafeHashCode(entity);
- return result;
- }
-
- /**
- * Returns whether the {@link Parameter} maps the given {@link PersistentProperty}.
- *
- * @param property
- * @return
- */
- boolean maps(PersistentProperty> property) {
-
- PersistentEntity entity = this.entity;
- String name = this.name;
-
- P referencedProperty = entity == null ? null : name == null ? null : entity.getPersistentProperty(name);
-
- return property.equals(referencedProperty);
- }
-
- private boolean isEnclosingClassParameter() {
- return enclosingClassCache.get();
- }
- }
}
diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java
index cd47b6a77..da7df78e6 100644
--- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java
+++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java
@@ -63,7 +63,7 @@ public class BasicPersistentEntity> implement
private static final String TYPE_MISMATCH = "Target bean of type %s is not of type of the persistent entity (%s)!";
- private final @Nullable PreferredConstructor constructor;
+ private final @Nullable EntityCreatorMetadata creator;
private final TypeInformation information;
private final List properties;
private final List
persistentPropertiesCache;
@@ -109,7 +109,7 @@ public class BasicPersistentEntity> implement
this.properties = new ArrayList<>();
this.persistentPropertiesCache = new ArrayList<>();
this.comparator = comparator;
- this.constructor = PreferredConstructorDiscoverer.discover(this);
+ this.creator = EntityCreatorMetadataDiscoverer.discover(this);
this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator));
this.propertyCache = new HashMap<>(16, 1f);
@@ -124,7 +124,7 @@ public class BasicPersistentEntity> implement
this.isImmutable = Lazy.of(() -> isAnnotationPresent(Immutable.class));
this.requiresPropertyPopulation = Lazy.of(() -> !isImmutable() && properties.stream() //
- .anyMatch(it -> !(isConstructorArgument(it) || it.isTransient())));
+ .anyMatch(it -> !(isCreatorArgument(it) || it.isTransient())));
}
/*
@@ -132,16 +132,23 @@ public class BasicPersistentEntity> implement
* @see org.springframework.data.mapping.PersistentEntity#getPersistenceConstructor()
*/
@Nullable
+ @Override
public PreferredConstructor getPersistenceConstructor() {
- return constructor;
+ return creator instanceof PreferredConstructor ? (PreferredConstructor) creator : null;
+ }
+
+ @Override
+ public EntityCreatorMetadata getEntityCreator() {
+ return creator;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.PersistentEntity#isConstructorArgument(org.springframework.data.mapping.PersistentProperty)
*/
- public boolean isConstructorArgument(PersistentProperty> property) {
- return constructor != null && constructor.isConstructorParameter(property);
+ @Override
+ public boolean isCreatorArgument(PersistentProperty> property) {
+ return creator != null && creator.isCreatorParameter(property);
}
/*
diff --git a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java
index 11b04c496..1e4d399ae 100644
--- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java
+++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java
@@ -18,6 +18,7 @@ package org.springframework.data.mapping.model;
import static org.springframework.asm.Opcodes.*;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
@@ -26,7 +27,6 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-
import org.springframework.asm.ClassWriter;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
@@ -34,19 +34,22 @@ import org.springframework.asm.Type;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.core.NativeDetector;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.FactoryMethod;
+import org.springframework.data.mapping.Parameter;
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.Assert;
import org.springframework.util.ClassUtils;
/**
* An {@link EntityInstantiator} that can generate byte code to speed-up dynamic object instantiation. Uses the
* {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an instance of the entity by dynamically
* generating factory methods with appropriate constructor invocations via ASM. If we cannot generate byte code for a
- * type, we gracefully fall-back to the {@link ReflectionEntityInstantiator}.
+ * type, we gracefully fallback to the {@link ReflectionEntityInstantiator}.
*
* @author Thomas Darimont
* @author Oliver Gierke
@@ -65,11 +68,21 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
private volatile Map, EntityInstantiator> entityInstantiators = new HashMap<>(32);
+ private final boolean fallbackToReflectionOnError;
+
/**
* Creates a new {@link ClassGeneratingEntityInstantiator}.
*/
public ClassGeneratingEntityInstantiator() {
+ this(true);
+ }
+
+ /**
+ * Creates a new {@link ClassGeneratingEntityInstantiator}.
+ */
+ ClassGeneratingEntityInstantiator(boolean fallbackToReflectionOnError) {
this.generator = new ObjectInstantiatorClassGenerator();
+ this.fallbackToReflectionOnError = fallbackToReflectionOnError;
}
/*
@@ -127,18 +140,22 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
return MappingInstantiationExceptionEntityInstantiator.create(entity.getType());
}
- try {
- return doCreateEntityInstantiator(entity);
- } catch (Throwable ex) {
+ if (fallbackToReflectionOnError) {
+ try {
+ return doCreateEntityInstantiator(entity);
+ } catch (Throwable ex) {
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug(
- String.format("Cannot create entity instantiator for %s. Falling back to ReflectionEntityInstantiator.",
- entity.getName()),
- ex);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ String.format("Cannot create entity instantiator for %s. Falling back to ReflectionEntityInstantiator.",
+ entity.getName()),
+ ex);
+ }
+ return ReflectionEntityInstantiator.INSTANCE;
}
- return ReflectionEntityInstantiator.INSTANCE;
}
+
+ return doCreateEntityInstantiator(entity);
}
/**
@@ -146,7 +163,8 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
* @return
*/
protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity, ?> entity) {
- return new EntityInstantiatorAdapter(createObjectInstantiator(entity, entity.getPersistenceConstructor()));
+ return new EntityInstantiatorAdapter(
+ createObjectInstantiator(entity, entity.getEntityCreator()));
}
/**
@@ -174,11 +192,30 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
return true;
}
- PreferredConstructor, ?> persistenceConstructor = entity.getPersistenceConstructor();
- if (persistenceConstructor == null || Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) {
+ EntityCreatorMetadata> entityCreator = entity.getEntityCreator();
+
+ if (entityCreator == null) {
return true;
}
+ if (entityCreator instanceof PreferredConstructor) {
+
+ PreferredConstructor, ?> persistenceConstructor = (PreferredConstructor, ?>) entityCreator;
+
+ if (Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) {
+ return true;
+ }
+ }
+
+ if (entityCreator instanceof FactoryMethod) {
+
+ FactoryMethod, ?> factoryMethod = (FactoryMethod, ?>) entityCreator;
+
+ if (Modifier.isPrivate(factoryMethod.getFactoryMethod().getModifiers())) {
+ return true;
+ }
+ }
+
if (!ClassUtils.isPresent(ObjectInstantiator.class.getName(), type.getClassLoader())) {
return true;
}
@@ -199,7 +236,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
/**
* 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 EntityCreatorMetadata}. There will always be exactly one {@link ObjectInstantiator} instance per
* {@link PersistentEntity}.
*
* @param entity
@@ -207,7 +244,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
* @return
*/
ObjectInstantiator createObjectInstantiator(PersistentEntity, ?> entity,
- @Nullable PreferredConstructor, ?> constructor) {
+ @Nullable EntityCreatorMetadata> constructor) {
try {
return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).newInstance();
@@ -245,7 +282,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
public , P extends PersistentProperty> T createInstance(E entity,
ParameterValueProvider
provider) {
- Object[] params = extractInvocationArguments(entity.getPersistenceConstructor(), provider);
+ Object[] params = extractInvocationArguments(entity.getEntityCreator(), provider);
try {
return (T) instantiator.newInstance(params);
@@ -263,13 +300,13 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
* @return
*/
static
, T> Object[] extractInvocationArguments(
- @Nullable PreferredConstructor extends T, P> constructor, ParameterValueProvider
provider) {
+ @Nullable EntityCreatorMetadata
constructor, ParameterValueProvider
provider) {
if (constructor == null || !constructor.hasParameters()) {
return allocateArguments(0);
}
- Object[] params = allocateArguments(constructor.getConstructor().getParameterCount());
+ Object[] params = allocateArguments(constructor.getParameterCount());
int index = 0;
for (Parameter, P> parameter : constructor.getParameters()) {
@@ -316,7 +353,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
public , P extends PersistentProperty> T createInstance(E entity,
ParameterValueProvider
provider) {
- Object[] params = extractInvocationArguments(entity.getPersistenceConstructor(), provider);
+ Object[] params = extractInvocationArguments(entity.getEntityCreator(), provider);
throw new MappingInstantiationException(entity, Arrays.asList(params),
new BeanInstantiationException(typeToCreate, "Class is abstract"));
@@ -366,7 +403,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
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 JAVA_LANG_OBJECT = Type.getInternalName(Object.class);
private static final String CREATE_METHOD_NAME = "newInstance";
private static final String[] IMPLEMENTED_INTERFACES = new String[] {
@@ -380,7 +417,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
* @return
*/
public Class> generateCustomInstantiatorClass(PersistentEntity, ?> entity,
- @Nullable PreferredConstructor, ?> constructor) {
+ @Nullable EntityCreatorMetadata> constructor) {
String className = generateClassName(entity);
Class> type = entity.getType();
@@ -417,11 +454,11 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
*
* @param internalClassName
* @param entity
- * @param constructor
+ * @param entityCreator
* @return
*/
public byte[] generateBytecode(String internalClassName, PersistentEntity, ?> entity,
- @Nullable PreferredConstructor, ?> constructor) {
+ @Nullable EntityCreatorMetadata> entityCreator) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
@@ -430,7 +467,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
visitDefaultConstructor(cw);
- visitCreateMethod(cw, entity, constructor);
+ visitCreateMethod(cw, entity, entityCreator);
cw.visitEnd();
@@ -453,49 +490,79 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
*
* @param cw
* @param entity
- * @param constructor
+ * @param entityCreator
*/
private void visitCreateMethod(ClassWriter cw, PersistentEntity, ?> entity,
- @Nullable PreferredConstructor, ?> constructor) {
+ @Nullable EntityCreatorMetadata> entityCreator) {
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);
+ "([" + BytecodeUtil.referenceName(Object.class) + ")" + BytecodeUtil.referenceName(Object.class),
+ null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, entityTypeResourcePath);
mv.visitInsn(DUP);
- if (constructor != null) {
+ if (entityCreator instanceof PreferredConstructor) {
+ visitConstructorCreation((PreferredConstructor, ?>) entityCreator, mv, entityTypeResourcePath);
+ }
- Constructor> ctor = constructor.getConstructor();
- Class>[] parameterTypes = ctor.getParameterTypes();
- List extends Parameter> parameters = constructor.getParameters();
+ if (entityCreator instanceof FactoryMethod) {
+ visitFactoryMethodCreation((FactoryMethod, ?>) entityCreator, mv, entityTypeResourcePath);
+ }
- for (int i = 0; i < parameterTypes.length; i++) {
+ mv.visitInsn(ARETURN);
+ mv.visitMaxs(0, 0); // (0, 0) = computed via ClassWriter.COMPUTE_MAXS
+ mv.visitEnd();
+ }
- mv.visitVarInsn(ALOAD, 1);
+ private static void visitConstructorCreation(PreferredConstructor, ?> constructor, MethodVisitor mv,
+ String entityTypeResourcePath) {
- visitArrayIndex(mv, i);
+ Constructor> ctor = constructor.getConstructor();
+ Class>[] parameterTypes = ctor.getParameterTypes();
+ List extends Parameter> parameters = constructor.getParameters();
- mv.visitInsn(AALOAD);
+ visitParameterTypes(mv, parameterTypes, parameters);
- if (parameterTypes[i].isPrimitive()) {
+ mv.visitMethodInsn(INVOKESPECIAL, entityTypeResourcePath, INIT, Type.getConstructorDescriptor(ctor), false);
+ }
- mv.visitInsn(DUP);
- String parameterName = parameters.size() > i ? parameters.get(i).getName() : null;
+ private static void visitFactoryMethodCreation(FactoryMethod, ?> factoryMethod, MethodVisitor mv,
+ String entityTypeResourcePath) {
- 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]));
- }
+ Method method = factoryMethod.getFactoryMethod();
+ Class>[] parameterTypes = method.getParameterTypes();
+ List extends Parameter> parameters = factoryMethod.getParameters();
+
+ visitParameterTypes(mv, parameterTypes, parameters);
+
+ mv.visitMethodInsn(INVOKESTATIC, entityTypeResourcePath, method.getName(), Type.getMethodDescriptor(method),
+ false);
+ }
+
+ private static void visitParameterTypes(MethodVisitor mv, Class>[] parameterTypes,
+ List extends Parameter> parameters) {
+
+ 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();
}
}
@@ -525,8 +592,8 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
// 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);
+ mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Assert.class), "notNull", String.format("(%s%s)V",
+ BytecodeUtil.referenceName(JAVA_LANG_OBJECT), BytecodeUtil.referenceName(String.class)), false);
}
/**
@@ -542,52 +609,52 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
switch (ch) {
case 'Z':
- if (!stackDescriptor.equals("Ljava/lang/Boolean")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Boolean.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Boolean.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", "()Z", false);
break;
case 'B':
- if (!stackDescriptor.equals("Ljava/lang/Byte")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Byte.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Byte.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", "()B", false);
break;
case 'C':
- if (!stackDescriptor.equals("Ljava/lang/Character")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Character.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Character.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Character.class), "charValue", "()C", false);
break;
case 'D':
- if (!stackDescriptor.equals("Ljava/lang/Double")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Double.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Double.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", "()D", false);
break;
case 'F':
- if (!stackDescriptor.equals("Ljava/lang/Float")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Float.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Float.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", "()F", false);
break;
case 'I':
- if (!stackDescriptor.equals("Ljava/lang/Integer")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Integer.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Integer.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", "()I", false);
break;
case 'J':
- if (!stackDescriptor.equals("Ljava/lang/Long")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Long.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Long.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", "()J", false);
break;
case 'S':
- if (!stackDescriptor.equals("Ljava/lang/Short")) {
- mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
+ if (!stackDescriptor.equals(BytecodeUtil.referenceName(Short.class))) {
+ mv.visitTypeInsn(CHECKCAST, Type.getInternalName(Short.class));
}
- mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Short.class), "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/DefaultSpELExpressionEvaluator.java b/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java
index 4bc62922f..2e68450e3 100644
--- a/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java
+++ b/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java
@@ -16,7 +16,7 @@
package org.springframework.data.mapping.model;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
+import org.springframework.data.mapping.Parameter;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
diff --git a/src/main/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscoverer.java b/src/main/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscoverer.java
new file mode 100644
index 000000000..537e5f6be
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscoverer.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2021 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.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.data.annotation.EntityCreatorAnnotation;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.FactoryMethod;
+import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.Parameter;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+
+/**
+ * Discoverer for factory methods and persistence constructors.
+ *
+ * @author Mark Paluch
+ * @since 3.0
+ */
+class EntityCreatorMetadataDiscoverer {
+
+ private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+ /**
+ * Discover an entity creator
+ *
+ * @param entity
+ * @param
+ * @param
+ * @return
+ */
+ @Nullable
+ public static > EntityCreatorMetadata discover(PersistentEntity entity) {
+
+ Constructor>[] declaredConstructors = entity.getType().getDeclaredConstructors();
+ Method[] declaredMethods = entity.getType().getDeclaredMethods();
+
+ boolean hasAnnotatedFactoryMethod = findAnnotation(EntityCreatorAnnotation.class, declaredMethods);
+ boolean hasAnnotatedConstructor = findAnnotation(EntityCreatorAnnotation.class, declaredConstructors);
+
+ if (hasAnnotatedConstructor && hasAnnotatedFactoryMethod) {
+ throw new MappingException(
+ String.format(
+ "Invalid usage of @Factory and @PersistenceConstructor on %s. Only one annotation type permitted to indicate how entity instances should be created.",
+ entity.getType().getName()));
+ }
+
+ if (hasAnnotatedFactoryMethod) {
+
+ List candidates = discoverFactoryMethods(entity, declaredMethods);
+
+ if (candidates.size() == 1) {
+ return getFactoryMethod(entity, candidates.get(0));
+ }
+ }
+
+ return PreferredConstructorDiscoverer.discover(entity);
+ }
+
+ private static > List discoverFactoryMethods(PersistentEntity entity,
+ Method[] declaredMethods) {
+
+ List candidates = new ArrayList<>();
+
+ for (Method method : declaredMethods) {
+
+ validateMethod(method);
+
+ if (!isFactoryMethod(method, entity.getType())) {
+ continue;
+ }
+
+ if (findAnnotation(EntityCreatorAnnotation.class, method)) {
+ candidates.add(method);
+ }
+ }
+
+ return candidates;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private static > FactoryMethod getFactoryMethod(
+ PersistentEntity entity, Method method) {
+
+ Parameter[] parameters = new Parameter[method.getParameterCount()];
+ Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ List> types = entity.getTypeInformation().getParameterTypes(method);
+
+ String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
+
+ for (int i = 0; i < parameters.length; i++) {
+
+ String name = parameterNames == null || parameterNames.length <= i ? null : parameterNames[i];
+ TypeInformation> type = types.get(i);
+ Annotation[] annotations = parameterAnnotations[i];
+
+ parameters[i] = new Parameter(name, type, annotations, entity);
+ }
+
+ return new FactoryMethod<>(method, parameters);
+ }
+
+ private static void validateMethod(Method method) {
+
+ if (MergedAnnotations.from(method).isPresent(EntityCreatorAnnotation.class)) {
+
+ if (!Modifier.isStatic(method.getModifiers())) {
+ throw new MappingException(
+ String.format("@Factory can only be used on static methods. Offending method: %s", method));
+ }
+ }
+ }
+
+ private static boolean isFactoryMethod(Method method, Class type) {
+
+ // private methods not supported
+ if (Modifier.isPrivate(method.getModifiers())) {
+ return false;
+ }
+
+ // synthetic methods not supported
+ if (method.isSynthetic()) {
+ return false;
+ }
+
+ return Modifier.isStatic(method.getModifiers()) && method.getReturnType().isAssignableFrom(type);
+ }
+
+ private static boolean findAnnotation(Class extends Annotation> annotationType, AnnotatedElement... elements) {
+
+ for (AnnotatedElement element : elements) {
+ if (MergedAnnotations.from(element).isPresent(annotationType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java
index 014e9597f..3a9830fdc 100644
--- a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java
+++ b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java
@@ -19,11 +19,11 @@ import java.util.function.Function;
import org.springframework.core.KotlinDetector;
import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.Parameter;
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.lang.Nullable;
import org.springframework.util.Assert;
@@ -107,22 +107,22 @@ public class InstantiationAwarePropertyAccessor implements PersistentProperty
return;
}
- PreferredConstructor, ?> constructor = owner.getPersistenceConstructor();
+ EntityCreatorMetadata> creator = owner.getEntityCreator();
- if (constructor == null) {
+ if (creator == null) {
throw new IllegalStateException(String.format(NO_SETTER_OR_CONSTRUCTOR, property.getName(), owner.getType()));
}
- if (!constructor.isConstructorParameter(property)) {
+ if (!creator.isCreatorParameter(property)) {
throw new IllegalStateException(
- String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), constructor.getConstructor()));
+ String.format(NO_CONSTRUCTOR_PARAMETER, property.getName(), creator));
}
- constructor.getParameters().forEach(it -> {
+ creator.getParameters().forEach(it -> {
if (it.getName() == null) {
throw new IllegalStateException(
- String.format("Cannot detect parameter names of copy constructor of %s!", owner.getType()));
+ String.format("Cannot detect parameter names of copy creator of %s!", owner.getType()));
}
});
diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java
index a89e71143..fa3273054 100644
--- a/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java
+++ b/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java
@@ -24,10 +24,11 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.Parameter;
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.KotlinReflectionUtils;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.lang.Nullable;
@@ -49,9 +50,10 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
@Override
protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity, ?> entity) {
- PreferredConstructor, ?> constructor = entity.getPersistenceConstructor();
+ EntityCreatorMetadata> creator = entity.getEntityCreator();
- if (KotlinReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) {
+ if (KotlinReflectionUtils.isSupportedKotlinClass(entity.getType())
+ && creator instanceof PreferredConstructor) {
PreferredConstructor, ?> defaultConstructor = new DefaultingKotlinConstructorResolver(entity)
.getDefaultConstructor();
@@ -60,7 +62,7 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor);
- return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor);
+ return new DefaultingKotlinClassInstantiatorAdapter(instantiator, (PreferredConstructor, ?>) creator);
}
}
@@ -82,9 +84,12 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
DefaultingKotlinConstructorResolver(PersistentEntity, ?> entity) {
Constructor> hit = resolveDefaultConstructor(entity);
- PreferredConstructor, ?> persistenceConstructor = entity.getPersistenceConstructor();
+ EntityCreatorMetadata> creator = entity.getEntityCreator();
+
+ if (hit != null && creator instanceof PreferredConstructor) {
+
+ PreferredConstructor, ?> persistenceConstructor = (PreferredConstructor, ?>) creator;
- if (hit != null && persistenceConstructor != null) {
this.defaultConstructor = new PreferredConstructor<>(hit,
persistenceConstructor.getParameters().toArray(new Parameter[0]));
} else {
@@ -95,12 +100,14 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
@Nullable
private static Constructor> resolveDefaultConstructor(PersistentEntity, ?> entity) {
- PreferredConstructor, ?> persistenceConstructor = entity.getPersistenceConstructor();
+ EntityCreatorMetadata> creator = entity.getEntityCreator();
- if (persistenceConstructor == null) {
+ if (!(creator instanceof PreferredConstructor)) {
return null;
}
+ PreferredConstructor, ?> persistenceConstructor = (PreferredConstructor, ?>) entity.getEntityCreator();
+
Constructor> hit = null;
Constructor> constructor = persistenceConstructor.getConstructor();
@@ -199,7 +206,7 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
public , P extends PersistentProperty> T createInstance(E entity,
ParameterValueProvider
provider) {
- Object[] params = extractInvocationArguments(entity.getPersistenceConstructor(), provider);
+ Object[] params = extractInvocationArguments(entity.getEntityCreator(), provider);
try {
return (T) instantiator.newInstance(params);
@@ -209,17 +216,17 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
}
private
, T> Object[] extractInvocationArguments(
- @Nullable PreferredConstructor extends T, P> preferredConstructor, ParameterValueProvider
provider) {
+ @Nullable EntityCreatorMetadata
entityCreator, ParameterValueProvider
provider) {
- if (preferredConstructor == null) {
- throw new IllegalArgumentException("PreferredConstructor must not be null!");
+ if (entityCreator == null) {
+ throw new IllegalArgumentException("EntityCreator must not be null!");
}
Object[] params = allocateArguments(synthetic.getParameterCount()
+ KotlinDefaultMask.getMaskCount(synthetic.getParameterCount()) + /* DefaultConstructorMarker */1);
int userParameterCount = kParameters.size();
- List> parameters = preferredConstructor.getParameters();
+ List> parameters = entityCreator.getParameters();
// Prepare user-space arguments
for (int i = 0; i < userParameterCount; i++) {
diff --git a/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java b/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java
index 6ac1494d6..4791c2988 100644
--- a/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java
+++ b/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java
@@ -19,10 +19,13 @@ import kotlin.reflect.KFunction;
import kotlin.reflect.jvm.ReflectJvmMapping;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.FactoryMethod;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.util.KotlinReflectionUtils;
@@ -43,7 +46,7 @@ public class MappingInstantiationException extends RuntimeException {
private static final String TEXT_TEMPLATE = "Failed to instantiate %s using constructor %s with arguments %s";
private final Class> entityType;
- private final Constructor> constructor;
+ private final EntityCreatorMetadata> entityCreator;
private final List constructorArguments;
/**
@@ -74,8 +77,7 @@ public class MappingInstantiationException extends RuntimeException {
super(buildExceptionMessage(entity, arguments, message), cause);
this.entityType = entity.map(PersistentEntity::getType).orElse(null);
- this.constructor = entity.map(PersistentEntity::getPersistenceConstructor).map(PreferredConstructor::getConstructor)
- .orElse(null);
+ this.entityCreator = entity.map(PersistentEntity::getEntityCreator).orElse(null);
this.constructorArguments = arguments;
}
@@ -84,7 +86,7 @@ public class MappingInstantiationException extends RuntimeException {
return entity.map(it -> {
- Optional extends PreferredConstructor, ?>> constructor = Optional.ofNullable(it.getPersistenceConstructor());
+ Optional extends EntityCreatorMetadata>> constructor = Optional.ofNullable(it.getEntityCreator());
List toStringArgs = new ArrayList<>(arguments.size());
for (Object o : arguments) {
@@ -98,6 +100,19 @@ public class MappingInstantiationException extends RuntimeException {
}).orElse(defaultMessage);
}
+ private static String toString(EntityCreatorMetadata> creator) {
+
+ if (creator instanceof PreferredConstructor) {
+ return toString((PreferredConstructor, ?>) creator);
+ }
+
+ if (creator instanceof FactoryMethod) {
+ return toString((FactoryMethod, ?>) creator);
+ }
+
+ return creator.toString();
+ }
+
private static String toString(PreferredConstructor, ?> preferredConstructor) {
Constructor> constructor = preferredConstructor.getConstructor();
@@ -114,6 +129,22 @@ public class MappingInstantiationException extends RuntimeException {
return constructor.toString();
}
+ private static String toString(FactoryMethod, ?> factoryMethod) {
+
+ Method method = factoryMethod.getFactoryMethod();
+
+ if (KotlinReflectionUtils.isSupportedKotlinClass(method.getDeclaringClass())) {
+
+ KFunction> kotlinFunction = ReflectJvmMapping.getKotlinFunction(method);
+
+ if (kotlinFunction != null) {
+ return kotlinFunction.toString();
+ }
+ }
+
+ return method.toString();
+ }
+
/**
* Returns the type of the entity that was attempted to instantiate.
*
@@ -127,9 +158,22 @@ public class MappingInstantiationException extends RuntimeException {
* The constructor used during the instantiation attempt.
*
* @return the constructor
+ * @deprecated since 3.0, use {@link #getEntityCreator()} instead.
*/
+ @Deprecated
public Optional> getConstructor() {
- return Optional.ofNullable(constructor);
+ return getEntityCreator().filter(PreferredConstructor.class::isInstance).map(PreferredConstructor.class::cast)
+ .map(PreferredConstructor::getConstructor);
+ }
+
+ /**
+ * The entity creator used during the instantiation attempt.
+ *
+ * @return the entity creator
+ * @since 3.0
+ */
+ public Optional> getEntityCreator() {
+ return Optional.ofNullable(entityCreator);
}
/**
diff --git a/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java
index a852891c5..6ceb00c4b 100644
--- a/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java
+++ b/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java
@@ -15,8 +15,8 @@
*/
package org.springframework.data.mapping.model;
+import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentProperty;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.lang.Nullable;
/**
diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
index 8e155ae6d..0f2d2ece4 100644
--- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
+++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java
@@ -15,11 +15,11 @@
*/
package org.springframework.data.mapping.model;
+import org.springframework.data.mapping.EntityCreatorMetadata;
import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.Parameter;
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.lang.Nullable;
/**
@@ -52,9 +52,9 @@ public class PersistentEntityParameterValueProvider T getParameterValue(Parameter parameter) {
- PreferredConstructor, P> constructor = entity.getPersistenceConstructor();
+ EntityCreatorMetadata creator = entity.getEntityCreator();
- if (constructor != null && constructor.isEnclosingClassParameter(parameter)) {
+ if (creator != null && creator.isParentParameter(parameter)) {
return (T) parent;
}
diff --git a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java
index 09d4fa243..97b95cc8f 100644
--- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java
+++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java
@@ -26,17 +26,14 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import javax.inject.Qualifier;
-
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
-import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.EntityCreatorAnnotation;
+import org.springframework.data.mapping.Parameter;
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.ClassTypeInformation;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.TypeInformation;
@@ -120,7 +117,7 @@ public interface PreferredConstructorDiscoverer !it.isSynthetic()) // Synthetic constructors should not be considered
- .filter(it -> AnnotationUtils.findAnnotation(it, PersistenceConstructor.class) != null) // Explicitly defined constructor trumps
+ .filter(it -> AnnotationUtils.findAnnotation(it, EntityCreatorAnnotation.class) != null) // Explicitly
+ // defined
+ // constructor
+ // trumps
// all
.map(it -> buildPreferredConstructor(it, type, entity)) //
.findFirst() //
diff --git a/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java
index e4335134a..e447f58aa 100644
--- a/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java
+++ b/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java
@@ -22,10 +22,13 @@ import java.util.Collections;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.FactoryMethod;
+import org.springframework.data.mapping.Parameter;
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.util.ReflectionUtils;
/**
* {@link EntityInstantiator} that uses the {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an
@@ -44,39 +47,73 @@ enum ReflectionEntityInstantiator implements EntityInstantiator {
public , P extends PersistentProperty> T createInstance(E entity,
ParameterValueProvider
provider) {
- PreferredConstructor extends T, P> constructor = entity.getPersistenceConstructor();
+ EntityCreatorMetadata
creator = entity.getEntityCreator();
- 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);
- }
+ if (creator == null) {
+ return instantiateClass(entity);
}
- int parameterCount = constructor.getConstructor().getParameterCount();
+
+ int parameterCount = creator.getParameterCount();
Object[] params = parameterCount == 0 ? EMPTY_ARGS : new Object[parameterCount];
int i = 0;
- for (Parameter, P> parameter : constructor.getParameters()) {
+
+ for (Parameter, P> parameter : creator.getParameters()) {
params[i++] = provider.getParameterValue(parameter);
}
+ if (creator instanceof FactoryMethod) {
+
+ FactoryMethod, ?> method = (FactoryMethod, ?>) creator;
+
+ try {
+
+ T t = (T) ReflectionUtils.invokeMethod(method.getFactoryMethod(), null, params);
+
+ if (t == null) {
+ throw new IllegalStateException(String.format("Method %s returned null!", method.getFactoryMethod()));
+ }
+
+ return t;
+
+ } catch (Exception e) {
+ throw new MappingInstantiationException(entity, new ArrayList<>(Arrays.asList(params)), e);
+ }
+ }
+
try {
- return BeanUtils.instantiateClass(constructor.getConstructor(), params);
+ return BeanUtils.instantiateClass(((PreferredConstructor) creator).getConstructor(), params);
} catch (BeanInstantiationException e) {
throw new MappingInstantiationException(entity, new ArrayList<>(Arrays.asList(params)), e);
}
}
+
+ @SuppressWarnings("unchecked")
+ private , P extends PersistentProperty> T instantiateClass(
+ E entity) {
+
+ 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);
+ }
+ }
}
diff --git a/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java
index 390d74951..9f3dea938 100644
--- a/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java
+++ b/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java
@@ -16,8 +16,8 @@
package org.springframework.data.mapping.model;
import org.springframework.core.convert.ConversionService;
+import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentProperty;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.lang.Nullable;
/**
diff --git a/src/main/java/org/springframework/data/repository/query/ReturnedType.java b/src/main/java/org/springframework/data/repository/query/ReturnedType.java
index af93e97a7..d4a690c62 100644
--- a/src/main/java/org/springframework/data/repository/query/ReturnedType.java
+++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java
@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import org.springframework.data.projection.ProjectionFactory;
@@ -302,7 +303,7 @@ public abstract class ReturnedType {
List properties = new ArrayList<>(constructor.getConstructor().getParameterCount());
- for (PreferredConstructor.Parameter parameter : constructor.getParameters()) {
+ for (Parameter parameter : constructor.getParameters()) {
properties.add(parameter.getName());
}
diff --git a/src/test/java/org/springframework/data/mapping/ParameterUnitTests.java b/src/test/java/org/springframework/data/mapping/ParameterUnitTests.java
index 7ae23d23b..bad7cff5c 100755
--- a/src/test/java/org/springframework/data/mapping/ParameterUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/ParameterUnitTests.java
@@ -24,7 +24,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
diff --git a/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java
index 35136de9c..12d8fb3a6 100755
--- a/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java
@@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.PersistenceConstructor;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.PreferredConstructorDiscovererUnitTests.Outer.Inner;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
@@ -100,7 +99,7 @@ class PreferredConstructorDiscovererUnitTests> {
assertThat(PreferredConstructorDiscoverer.discover(entity)).satisfies(constructor -> {
Parameter, P> parameter = constructor.getParameters().iterator().next();
- assertThat(constructor.isEnclosingClassParameter(parameter)).isTrue();
+ assertThat(constructor.isParentParameter(parameter)).isTrue();
});
}
diff --git a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java
index de5e139a9..dc77e8ee4 100755
--- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java
@@ -32,12 +32,12 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
-
+import org.springframework.data.annotation.FactoryMethod;
import org.springframework.data.classloadersupport.HidingClassLoader;
+import org.springframework.data.mapping.Parameter;
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.ClassGeneratingEntityInstantiator.ObjectInstantiator;
import org.springframework.data.mapping.model.ClassGeneratingEntityInstantiatorUnitTests.Outer.Inner;
import org.springframework.data.util.ClassTypeInformation;
@@ -56,7 +56,7 @@ import org.springframework.util.ReflectionUtils;
@MockitoSettings(strictness = Strictness.LENIENT)
class ClassGeneratingEntityInstantiatorUnitTests
> {
- ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator();
+ ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator(false);
@Mock PersistentEntity, P> entity;
@Mock ParameterValueProvider
provider;
@@ -83,7 +83,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(Foo.class);
doReturn(Foo.class).when(entity).getType();
- doReturn(constructor).when(entity).getPersistenceConstructor();
+ doReturn(constructor).when(entity).getEntityCreator();
assertThat(instance.createInstance(entity, provider)).isInstanceOf(Foo.class);
@@ -104,8 +104,8 @@ class ClassGeneratingEntityInstantiatorUnitTests
@Test // DATACMNS-134, DATACMNS-578
void createsInnerClassInstanceCorrectly() {
- BasicPersistentEntity entity = new BasicPersistentEntity<>(from(Inner.class));
- assertThat(entity.getPersistenceConstructor()).satisfies(constructor -> {
+ BasicPersistentEntity entity = new BasicPersistentEntity(from(Inner.class));
+ assertThat(entity.getEntityCreator()).satisfies(constructor -> {
Parameter parameter = constructor.getParameters().iterator().next();
@@ -189,12 +189,43 @@ class ClassGeneratingEntityInstantiatorUnitTests
assertThat(reference.sample.name).isEqualTo("FOO");
}
+ @Test // DATACMNS-1175
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ void createsInstancesWithFactoryMethodCorrectly() {
+
+ PersistentEntity entity = new BasicPersistentEntity<>(from(WithFactoryMethod.class));
+
+ doReturn(2L, "FOO").when(provider).getParameterValue(any(Parameter.class));
+
+ ParameterValueProvider provider = new ParameterValueProvider
() {
+
+ @Override
+ public T getParameterValue(Parameter parameter) {
+
+ if (parameter.getName().equals("id")) {
+ return (T) Long.valueOf(1);
+ }
+
+ if (parameter.getName().equals("name")) {
+ return (T) "Walter";
+ }
+
+ throw new UnsupportedOperationException(parameter.getName());
+ }
+ };
+
+ WithFactoryMethod result = this.instance.createInstance(entity, provider);
+
+ assertThat(result.id).isEqualTo(1L);
+ assertThat(result.name).isEqualTo("Hello Walter");
+ }
+
@Test // DATACMNS-578, DATACMNS-1126
void instantiateObjCtorDefault() {
doReturn(ObjCtorDefault.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtorDefault.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
IntStream.range(0, 2)
.forEach(i -> assertThat(this.instance.createInstance(entity, provider)).isInstanceOf(ObjCtorDefault.class));
@@ -205,7 +236,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(ObjCtorNoArgs.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtorNoArgs.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -224,7 +255,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(ObjCtor1ParamString.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtor1ParamString.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
doReturn("FOO").when(provider).getParameterValue(any());
IntStream.range(0, 2).forEach(i -> {
@@ -242,7 +273,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(ObjCtor2ParamStringString.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjCtor2ParamStringString.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -262,7 +293,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(ObjectCtor1ParamInt.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor1ParamInt.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -280,7 +311,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(ObjectCtor1ParamInt.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor1ParamInt.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
assertThatThrownBy(() -> this.instance.createInstance(entity, provider)) //
.hasCauseInstanceOf(IllegalArgumentException.class);
@@ -292,7 +323,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(ObjectCtor7ParamsString5IntsString.class).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor7ParamsString5IntsString.class))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
IntStream.range(0, 2).forEach(i -> {
@@ -412,7 +443,7 @@ class ClassGeneratingEntityInstantiatorUnitTests
doReturn(type).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(type))//
- .when(entity).getPersistenceConstructor();
+ .when(entity).getEntityCreator();
}
static class Foo {
@@ -429,6 +460,23 @@ class ClassGeneratingEntityInstantiatorUnitTests
}
}
+ static class WithFactoryMethod {
+
+ final Long id;
+ final String name;
+
+ private WithFactoryMethod(Long id, String name) {
+
+ this.id = id;
+ this.name = name;
+ }
+
+ @FactoryMethod
+ public static WithFactoryMethod create(Long id, String name) {
+ return new WithFactoryMethod(id, "Hello " + name);
+ }
+ }
+
static class Sample {
final Long id;
diff --git a/src/test/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscovererUnitTests.java
new file mode 100644
index 000000000..c11539b2c
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscovererUnitTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2021 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.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.annotation.FactoryMethod;
+import org.springframework.data.mapping.EntityCreatorMetadata;
+import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PreferredConstructor;
+import org.springframework.data.util.ClassTypeInformation;
+
+/**
+ * Unit tests for {@link EntityCreatorMetadataDiscoverer}.
+ *
+ * @author Mark Paluch
+ */
+class EntityCreatorMetadataDiscovererUnitTests {
+
+ @Test
+ void shouldDiscoverAnnotatedFactoryMethod() {
+
+ PersistentEntity entity = new BasicPersistentEntity<>(
+ ClassTypeInformation.from(FactoryMethodsPerson.class));
+ EntityCreatorMetadata> creator = EntityCreatorMetadataDiscoverer.discover(entity);
+
+ assertThat(creator).isInstanceOf(org.springframework.data.mapping.FactoryMethod.class);
+ assertThat(((org.springframework.data.mapping.FactoryMethod, ?>) creator).getFactoryMethod().getParameterCount())
+ .isEqualTo(2);
+ }
+
+ @Test
+ void shouldDiscoverAnnotatedConstructor() {
+
+ PersistentEntity entity = new BasicPersistentEntity<>(
+ ClassTypeInformation.from(ConstructorPerson.class));
+ EntityCreatorMetadata> creator = EntityCreatorMetadataDiscoverer.discover(entity);
+
+ assertThat(creator).isInstanceOf(PreferredConstructor.class);
+ }
+
+ @Test
+ void shouldDiscoverDefaultConstructor() {
+
+ PersistentEntity entity = new BasicPersistentEntity<>(ClassTypeInformation.from(Person.class));
+ EntityCreatorMetadata> creator = EntityCreatorMetadataDiscoverer.discover(entity);
+
+ assertThat(creator).isInstanceOf(PreferredConstructor.class);
+ }
+
+ @Test
+ void shouldRejectNonStaticFactoryMethod() {
+ assertThatExceptionOfType(MappingException.class)
+ .isThrownBy(() -> new BasicPersistentEntity<>(ClassTypeInformation.from(NonStaticFactoryMethod.class)));
+ }
+
+ static class Person {
+
+ private final String firstname, lastname;
+
+ private Person(String firstname, String lastname) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ }
+
+ }
+
+ static class NonStaticFactoryMethod {
+
+ @FactoryMethod
+ public ConstructorPerson of(String firstname, String lastname) {
+ return new ConstructorPerson(firstname, lastname);
+ }
+
+ }
+
+ static class FactoryMethodsPerson {
+
+ private final String firstname, lastname;
+
+ private FactoryMethodsPerson(String firstname, String lastname) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ }
+
+ public static FactoryMethodsPerson of(String firstname) {
+ return new FactoryMethodsPerson(firstname, "unknown");
+ }
+
+ @FactoryMethod
+ public static FactoryMethodsPerson of(String firstname, String lastname) {
+ return new FactoryMethodsPerson(firstname, lastname);
+ }
+ }
+
+ static class ConstructorPerson {
+
+ private final String firstname, lastname;
+
+ private ConstructorPerson(String firstname, String lastname) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ }
+
+ public static ConstructorPerson of(String firstname, String lastname) {
+ return new ConstructorPerson(firstname, lastname);
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java
index f615ac26f..a7048b97b 100755
--- a/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/EntityInstantiatorsUnitTests.java
@@ -44,13 +44,6 @@ class EntityInstantiatorsUnitTests {
assertThatIllegalArgumentException().isThrownBy(() -> new EntityInstantiators((EntityInstantiator) null));
}
- @Test
- void usesReflectionEntityInstantiatorAsDefaultFallback() {
-
- EntityInstantiators instantiators = new EntityInstantiators();
- assertThat(instantiators.getInstantiatorFor(entity)).isInstanceOf(ClassGeneratingEntityInstantiator.class);
- }
-
@Test
void returnsCustomInstantiatorForTypeIfRegistered() {
diff --git a/src/test/java/org/springframework/data/mapping/model/FactoryMethodUnitTests.java b/src/test/java/org/springframework/data/mapping/model/FactoryMethodUnitTests.java
new file mode 100644
index 000000000..f1b75652f
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/model/FactoryMethodUnitTests.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 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.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.annotation.FactoryMethod;
+import org.springframework.data.mapping.Parameter;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.util.ClassTypeInformation;
+
+/**
+ * Unit tests for {@link org.springframework.data.mapping.FactoryMethod}.
+ *
+ * @author Mark Paluch
+ */
+class FactoryMethodUnitTests {
+
+ private static EntityInstantiators instantiators = new EntityInstantiators();
+
+ @Test
+ void shouldCreateInstanceThroughFactoryMethod() {
+
+ PersistentEntity entity = new BasicPersistentEntity<>(
+ ClassTypeInformation.from(FactoryPerson.class));
+
+ FactoryPerson result = instantiators.getInstantiatorFor(entity)
+ .createInstance(entity, new ParameterValueProvider() {
+
+ @Override
+ public Object getParameterValue(Parameter parameter) {
+
+ if (parameter.getName().equals("firstname")) {
+ return "Walter";
+ }
+
+ if (parameter.getName().equals("lastname")) {
+ return "White";
+ }
+ return null;
+ }
+ });
+
+ assertThat(result.firstname).isEqualTo("Walter");
+ assertThat(result.lastname).isEqualTo("Mr. White");
+ }
+
+ static class FactoryPerson {
+
+ private final String firstname, lastname;
+
+ private FactoryPerson(String firstname, String lastname) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ }
+
+ @FactoryMethod
+ public static FactoryPerson of(String firstname, String lastname) {
+ return new FactoryPerson(firstname, "Mr. " + lastname);
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java
index e092b66fc..604e1629f 100644
--- a/src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/ParameterizedKotlinInstantiatorUnitTests.java
@@ -27,8 +27,8 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
+import org.springframework.data.mapping.Parameter;
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.test.util.ReflectionTestUtils;
diff --git a/src/test/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProviderUnitTests.java b/src/test/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProviderUnitTests.java
index 6423f45fa..4a474f26c 100755
--- a/src/test/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProviderUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProviderUnitTests.java
@@ -24,11 +24,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-
import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProviderUnitTests.Outer.Inner;
import org.springframework.data.util.ClassTypeInformation;
@@ -57,7 +56,7 @@ class PersistentEntityParameterValueProviderUnitTests {
+ assertThat(entity.getEntityCreator()).satisfies(constructor -> {
Iterator> iterator = constructor.getParameters().iterator();
ParameterValueProvider provider = new PersistentEntityParameterValueProvider<>(entity, propertyValueProvider,
@@ -76,7 +75,7 @@ class PersistentEntityParameterValueProviderUnitTests
provider = new PersistentEntityParameterValueProvider<>(entity, propertyValueProvider,
Optional.of(property));
- assertThat(entity.getPersistenceConstructor())
+ assertThat(entity.getEntityCreator())
.satisfies(constructor -> assertThatExceptionOfType(MappingException.class)//
.isThrownBy(() -> provider.getParameterValue(constructor.getParameters().iterator().next()))//
.withMessageContaining("bar")//
diff --git a/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java
index 5b40f5b7f..b5ba63c48 100755
--- a/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java
@@ -30,10 +30,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.mapping.Parameter;
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.ReflectionEntityInstantiatorUnitTests.Outer.Inner;
import org.springframework.util.ReflectionUtils;
@@ -69,7 +69,7 @@ class ReflectionEntityInstantiatorUnitTests
> {
PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(Foo.class);
- doReturn(constructor).when(entity).getPersistenceConstructor();
+ doReturn(constructor).when(entity).getEntityCreator();
Object instance = INSTANCE.createInstance(entity, provider);
@@ -91,8 +91,8 @@ class ReflectionEntityInstantiatorUnitTests> {
@Test // DATACMNS-134
void createsInnerClassInstanceCorrectly() {
- BasicPersistentEntity entity = new BasicPersistentEntity<>(from(Inner.class));
- assertThat(entity.getPersistenceConstructor()).satisfies(it -> {
+ BasicPersistentEntity entity = new BasicPersistentEntity(from(Inner.class));
+ assertThat(entity.getEntityCreator()).satisfies(it -> {
Parameter parameter = it.getParameters().iterator().next();
diff --git a/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java b/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java
index 872621cbe..5e7623f18 100755
--- a/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java
@@ -27,7 +27,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.core.convert.ConversionService;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
+import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.model.AbstractPersistentPropertyUnitTests.SamplePersistentProperty;
/**
diff --git a/src/test/kotlin/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiatorUnitTests.kt b/src/test/kotlin/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiatorUnitTests.kt
index fc997307e..b06348ce1 100644
--- a/src/test/kotlin/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiatorUnitTests.kt
+++ b/src/test/kotlin/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiatorUnitTests.kt
@@ -45,7 +45,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
val constructor = PreferredConstructorDiscoverer.discover(Contact::class.java)
every { provider.getParameterValue(any()) }.returnsMany("Walter", "White")
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
every { entity.type } returns constructor.constructor.declaringClass
every { entity.typeInformation } returns mockk()
@@ -67,7 +67,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
null, null, null, null, null, null, null, null, null, null, /* 20-29 */
null, "Walter", null, "Junior", null)
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
every { entity.type } returns constructor.constructor.declaringClass
every { entity.typeInformation } returns mockk()
@@ -88,7 +88,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
val constructor = PreferredConstructorDiscoverer.discover(WithBoolean::class.java)
every { provider.getParameterValue(any()) } returns null
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
every { entity.type } returns constructor.constructor.declaringClass
every { entity.typeInformation } returns mockk()
@@ -112,7 +112,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
every { provider.getParameterValue(any()) } returns null
every { provider.getParameterValue(any()) } returns null
every { provider.getParameterValue(any()) } returns null
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
every { entity.type } returns constructor.constructor.declaringClass
every { entity.typeInformation } returns mockk()
@@ -135,7 +135,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
val constructor = PreferredConstructorDiscoverer.discover(CustomUser::class.java)
every { provider.getParameterValue(any()) } returns "Walter"
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
every { entity.type } returns constructor.constructor.declaringClass
every { entity.typeInformation } returns mockk()
diff --git a/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt b/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt
index 824c32ae0..8a1ac61dc 100644
--- a/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt
+++ b/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt
@@ -42,7 +42,7 @@ class ReflectionEntityInstantiatorDataClassUnitTests {
val constructor = PreferredConstructorDiscoverer.discover(Contact::class.java)
every { provider.getParameterValue(any()) }.returnsMany("Walter", "White")
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
val instance: Contact = ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)
@@ -57,7 +57,7 @@ class ReflectionEntityInstantiatorDataClassUnitTests {
val constructor = PreferredConstructorDiscoverer.discover(ContactWithDefaulting::class.java)
every { provider.getParameterValue(any()) }.returnsMany("Walter", null)
- every { entity.persistenceConstructor } returns constructor
+ every { entity.entityCreator } returns constructor
val instance: ContactWithDefaulting = ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)