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 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..f4105f058 --- /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!"); + + var cached = isPropertyParameterCache.get(property); + + if (cached != null) { + return cached; + } + + var result = doGetIsCreatorParameter(property); + + isPropertyParameterCache.put(property, result); + + return result; + } + + @Override + public String toString() { + return executable.toString(); + } + + private boolean doGetIsCreatorParameter(PersistentProperty property) { + + for (Parameter 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..035402d7d --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/Parameter.java @@ -0,0 +1,208 @@ +/* + * 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(); + } + + var 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 parameter)) { + return false; + } + + 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() { + var 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) { + + var entity = this.entity; + var name = this.name; + + var 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 b19b7ff0e..c5b641922 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 476ffc2a9..7b70501d7 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); + } - Assert.notNull(property, "Property must not be null!"); - - var cached = isPropertyParameterCache.get(property); - - if (cached != null) { - return cached; - } - - var result = false; - for (Parameter parameter : parameters) { - if (parameter.maps(property)) { - result = true; - break; - } - } - - isPropertyParameterCache.put(property, result); - - return result; + @Override + public boolean isParentParameter(Parameter parameter) { + return isEnclosingClassParameter(parameter); } /** @@ -169,176 +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(); - } - - var 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(); - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - - if (!(o instanceof Parameter parameter)) { - return false; - } - - 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); - } - - @Override - public int hashCode() { - var 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) { - - var entity = this.entity; - var name = this.name; - - var 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 ec4a556bc..972204fb7 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,16 +124,23 @@ 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()))); } @Nullable + @Override public PreferredConstructor getPersistenceConstructor() { - return constructor; + return creator instanceof PreferredConstructor ? (PreferredConstructor) creator : null; } - public boolean isConstructorArgument(PersistentProperty property) { - return constructor != null && constructor.isConstructorParameter(property); + @Override + public EntityCreatorMetadata

getEntityCreator() { + return creator; + } + + @Override + public boolean isCreatorArgument(PersistentProperty property) { + return creator != null && creator.isCreatorParameter(property); } public boolean isIdProperty(PersistentProperty 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 95a466e5e..9c46ade01 100644 --- a/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiator.java @@ -33,19 +33,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 @@ -64,11 +67,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; } @Override @@ -122,18 +135,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); } /** @@ -141,7 +158,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())); } /** @@ -169,11 +187,26 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator { return true; } - var persistenceConstructor = entity.getPersistenceConstructor(); - if (persistenceConstructor == null || Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) { + var entityCreator = entity.getEntityCreator(); + + if (entityCreator == null) { return true; } + if (entityCreator instanceof PreferredConstructor persistenceConstructor) { + + if (Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) { + return true; + } + } + + if (entityCreator instanceof FactoryMethod factoryMethod) { + + if (Modifier.isPrivate(factoryMethod.getFactoryMethod().getModifiers())) { + return true; + } + } + if (!ClassUtils.isPresent(ObjectInstantiator.class.getName(), type.getClassLoader())) { return true; } @@ -194,7 +227,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 @@ -202,7 +235,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(); @@ -236,7 +269,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator { public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { - var params = extractInvocationArguments(entity.getPersistenceConstructor(), provider); + var params = extractInvocationArguments(entity.getEntityCreator(), provider); try { return (T) instantiator.newInstance(params); @@ -254,13 +287,13 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator { * @return */ static

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

provider) { + @Nullable EntityCreatorMetadata

constructor, ParameterValueProvider

provider) { if (constructor == null || !constructor.hasParameters()) { return allocateArguments(0); } - var params = allocateArguments(constructor.getConstructor().getParameterCount()); + var params = allocateArguments(constructor.getParameterCount()); var index = 0; for (Parameter parameter : constructor.getParameters()) { @@ -303,7 +336,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator { public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { - var params = extractInvocationArguments(entity.getPersistenceConstructor(), provider); + var params = extractInvocationArguments(entity.getEntityCreator(), provider); throw new MappingInstantiationException(entity, Arrays.asList(params), new BeanInstantiationException(typeToCreate, "Class is abstract")); @@ -353,7 +386,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[] { @@ -367,7 +400,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator { * @return */ public Class generateCustomInstantiatorClass(PersistentEntity entity, - @Nullable PreferredConstructor constructor) { + @Nullable EntityCreatorMetadata constructor) { var className = generateClassName(entity); var type = entity.getType(); @@ -404,11 +437,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) { var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); @@ -417,7 +450,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator { visitDefaultConstructor(cw); - visitCreateMethod(cw, entity, constructor); + visitCreateMethod(cw, entity, entityCreator); cw.visitEnd(); @@ -440,49 +473,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) { var entityTypeResourcePath = Type.getInternalName(entity.getType()); var 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 constructor) { + visitConstructorCreation(constructor, mv, entityTypeResourcePath); + } - var ctor = constructor.getConstructor(); - var parameterTypes = ctor.getParameterTypes(); - List> parameters = constructor.getParameters(); + if (entityCreator instanceof FactoryMethod factoryMethod) { + visitFactoryMethodCreation(factoryMethod, mv, entityTypeResourcePath); + } - for (var 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); + var ctor = constructor.getConstructor(); + var parameterTypes = ctor.getParameterTypes(); + List> 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); - var 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])); - } + var method = factoryMethod.getFactoryMethod(); + var parameterTypes = method.getParameterTypes(); + List> 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> parameters) { + + for (var i = 0; i < parameterTypes.length; i++) { + + mv.visitVarInsn(ALOAD, 1); + + visitArrayIndex(mv, i); + + mv.visitInsn(AALOAD); + + if (parameterTypes[i].isPrimitive()) { + + mv.visitInsn(DUP); + var 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(); } } @@ -512,8 +575,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); } /** @@ -529,52 +592,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 c04e87d86..f324435a6 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.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; 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..9a9713374 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscoverer.java @@ -0,0 +1,161 @@ +/* + * 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.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.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) { + + var declaredConstructors = entity.getType().getDeclaredConstructors(); + var declaredMethods = entity.getType().getDeclaredMethods(); + + var hasAnnotatedFactoryMethod = findAnnotation(EntityCreatorAnnotation.class, declaredMethods); + var hasAnnotatedConstructor = findAnnotation(EntityCreatorAnnotation.class, declaredConstructors); + + if (hasAnnotatedConstructor && hasAnnotatedFactoryMethod) { + throw new MappingException( + "Invalid usage of @Factory and @PersistenceConstructor on %s. Only one annotation type permitted to indicate how entity instances should be created." + .formatted(entity.getType().getName())); + } + + if (hasAnnotatedFactoryMethod) { + + var 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 (var 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()]; + var parameterAnnotations = method.getParameterAnnotations(); + var types = entity.getTypeInformation().getParameterTypes(method); + + var parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + + for (var i = 0; i < parameters.length; i++) { + + var name = parameterNames == null || parameterNames.length <= i ? null : parameterNames[i]; + var type = types.get(i); + var 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( + "@Factory can only be used on static methods. Offending method: %s".formatted(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 annotationType, AnnotatedElement... elements) { + + for (var 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 41831d28c..66a6685e1 100644 --- a/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java +++ b/src/main/java/org/springframework/data/mapping/model/InstantiationAwarePropertyAccessor.java @@ -19,9 +19,9 @@ import java.util.function.Function; import org.springframework.core.KotlinDetector; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -81,22 +81,22 @@ public class InstantiationAwarePropertyAccessor implements PersistentProperty return; } - var constructor = owner.getPersistenceConstructor(); + var 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 b517eee7e..ed65591fa 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; @@ -45,9 +46,10 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta @Override protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { - var constructor = entity.getPersistenceConstructor(); + var creator = entity.getEntityCreator(); - if (KotlinReflectionUtils.isSupportedKotlinClass(entity.getType()) && constructor != null) { + if (KotlinReflectionUtils.isSupportedKotlinClass(entity.getType()) + && creator instanceof PreferredConstructor constructor) { var defaultConstructor = new DefaultingKotlinConstructorResolver(entity) .getDefaultConstructor(); @@ -78,9 +80,9 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta DefaultingKotlinConstructorResolver(PersistentEntity entity) { var hit = resolveDefaultConstructor(entity); - var persistenceConstructor = entity.getPersistenceConstructor(); + var creator = entity.getEntityCreator(); - if (hit != null && persistenceConstructor != null) { + if (hit != null && creator instanceof PreferredConstructor persistenceConstructor) { this.defaultConstructor = new PreferredConstructor<>(hit, persistenceConstructor.getParameters().toArray(new Parameter[0])); } else { @@ -91,9 +93,7 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta @Nullable private static Constructor resolveDefaultConstructor(PersistentEntity entity) { - var persistenceConstructor = entity.getPersistenceConstructor(); - - if (persistenceConstructor == null) { + if (!(entity.getEntityCreator()instanceof PreferredConstructor persistenceConstructor)) { return null; } @@ -191,7 +191,7 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { - var params = extractInvocationArguments(entity.getPersistenceConstructor(), provider); + var params = extractInvocationArguments(entity.getEntityCreator(), provider); try { return (T) instantiator.newInstance(params); @@ -201,17 +201,17 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta } private

, T> Object[] extractInvocationArguments( - @Nullable PreferredConstructor 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!"); } var params = allocateArguments(synthetic.getParameterCount() + KotlinDefaultMask.getMaskCount(synthetic.getParameterCount()) + /* DefaultConstructorMarker */1); var userParameterCount = kParameters.size(); - var parameters = preferredConstructor.getParameters(); + var parameters = entityCreator.getParameters(); // Prepare user-space arguments for (var 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 7cbae3037..85d89f7af 100644 --- a/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java +++ b/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java @@ -22,6 +22,8 @@ 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; @@ -42,7 +44,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; /** @@ -73,8 +75,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; } @@ -83,7 +84,7 @@ public class MappingInstantiationException extends RuntimeException { return entity.map(it -> { - Optional> constructor = Optional.ofNullable(it.getPersistenceConstructor()); + Optional> constructor = Optional.ofNullable(it.getEntityCreator()); List toStringArgs = new ArrayList<>(arguments.size()); for (var o : arguments) { @@ -97,6 +98,19 @@ public class MappingInstantiationException extends RuntimeException { }).orElse(defaultMessage); } + private static String toString(EntityCreatorMetadata creator) { + + if (creator instanceof PreferredConstructor c) { + return toString(c); + } + + if (creator instanceof FactoryMethod m) { + return toString(m); + } + + return creator.toString(); + } + private static String toString(PreferredConstructor preferredConstructor) { var constructor = preferredConstructor.getConstructor(); @@ -113,6 +127,22 @@ public class MappingInstantiationException extends RuntimeException { return constructor.toString(); } + private static String toString(FactoryMethod factoryMethod) { + + var constructor = factoryMethod.getFactoryMethod(); + + if (KotlinReflectionUtils.isSupportedKotlinClass(constructor.getDeclaringClass())) { + + var kotlinFunction = ReflectJvmMapping.getKotlinFunction(constructor); + + if (kotlinFunction != null) { + return kotlinFunction.toString(); + } + } + + return constructor.toString(); + } + /** * Returns the type of the entity that was attempted to instantiate. * @@ -126,9 +156,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 42a482ade..05767a284 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -16,9 +16,9 @@ package org.springframework.data.mapping.model; 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.lang.Nullable; /** @@ -47,10 +47,10 @@ public class PersistentEntityParameterValueProvider

T getParameterValue(Parameter parameter) { - var constructor = entity.getPersistenceConstructor(); + var creator = entity.getEntityCreator(); - if (constructor != null && constructor.isEnclosingClassParameter(parameter)) { - return (T) parent; + if (creator.isParentParameter(parameter)) { + return (T) parent; } var name = parameter.getName(); 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 6b17d5b11..fe1ae2e48 100644 --- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java +++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java @@ -24,17 +24,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; @@ -114,7 +111,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 55ecd879a..581616bd4 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,12 @@ import java.util.Collections; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; +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 +46,58 @@ enum ReflectionEntityInstantiator implements EntityInstantiator { public , P extends PersistentProperty

> T createInstance(E entity, ParameterValueProvider

provider) { - var constructor = entity.getPersistenceConstructor(); + var creator = entity.getEntityCreator(); - if (constructor == null) { - - try { - Class clazz = entity.getType(); - if (clazz.isArray()) { - var ctype = clazz; - var 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); } - var parameterCount = constructor.getConstructor().getParameterCount(); + var parameterCount = creator.getParameterCount(); var params = parameterCount == 0 ? EMPTY_ARGS : new Object[parameterCount]; var i = 0; - for (Parameter parameter : constructor.getParameters()) { + for (Parameter parameter : creator.getParameters()) { params[i++] = provider.getParameterValue(parameter); } + if (creator instanceof FactoryMethod method) { + + try { + var t = (T) ReflectionUtils.invokeMethod(method.getFactoryMethod(), null, params); + + if (t == null) { + throw new IllegalStateException("Method %s returned null!".formatted(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); } } + + private , P extends PersistentProperty

> T instantiateClass( + E entity) { + + try { + Class clazz = entity.getType(); + if (clazz.isArray()) { + var ctype = clazz; + var 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 31c2aede6..6da9e6c58 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 36ce46c5e..7706cd6aa 100644 --- a/src/main/java/org/springframework/data/repository/query/ReturnedType.java +++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java @@ -23,6 +23,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; @@ -261,7 +262,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 9d2139f5f..5ae8ea7b0 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 817b8ace0..056b98e12 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 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 46f438a94..59eb7f229 100755 --- a/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/ClassGeneratingEntityInstantiatorUnitTests.java @@ -33,11 +33,12 @@ 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 +57,7 @@ import org.springframework.util.ReflectionUtils; @MockitoSettings(strictness = Strictness.LENIENT) class ClassGeneratingEntityInstantiatorUnitTests

> { - ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator(); + ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator(false); @Mock PersistentEntity entity; @Mock ParameterValueProvider

provider; @@ -83,7 +84,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); @@ -105,7 +106,7 @@ class ClassGeneratingEntityInstantiatorUnitTests

void createsInnerClassInstanceCorrectly() { var entity = new BasicPersistentEntity(from(Inner.class)); - assertThat(entity.getPersistenceConstructor()).satisfies(constructor -> { + assertThat(entity.getEntityCreator()).satisfies(constructor -> { var parameter = constructor.getParameters().iterator().next(); @@ -189,12 +190,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)); + + var 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()); + } + }; + + var 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 +237,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 +256,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 +274,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 +294,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 +312,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 +324,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 +444,7 @@ class ClassGeneratingEntityInstantiatorUnitTests

doReturn(type).when(entity).getType(); doReturn(PreferredConstructorDiscoverer.discover(type))// - .when(entity).getPersistenceConstructor(); + .when(entity).getEntityCreator(); } static class Foo { @@ -429,6 +461,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..8ffc8b659 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/EntityCreatorMetadataDiscovererUnitTests.java @@ -0,0 +1,121 @@ +/* + * 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.MappingException; +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() { + + var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(FactoryMethodsPerson.class)); + var 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() { + + var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(ConstructorPerson.class)); + var creator = EntityCreatorMetadataDiscoverer.discover(entity); + + assertThat(creator).isInstanceOf(PreferredConstructor.class); + } + + @Test + void shouldDiscoverDefaultConstructor() { + + var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(Person.class)); + var 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 7ff832b61..a8ada71fe 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() { - - var 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..e0ec5eec4 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/FactoryMethodUnitTests.java @@ -0,0 +1,75 @@ +/* + * 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.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() { + + var entity = new BasicPersistentEntity<>(ClassTypeInformation.from(FactoryPerson.class)); + + var 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 3378417b7..1c5e2bd4f 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 997d8282d..5fd30c297 100755 --- a/src/test/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProviderUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProviderUnitTests.java @@ -55,7 +55,7 @@ class PersistentEntityParameterValueProviderUnitTests

{ + assertThat(entity.getEntityCreator()).satisfies(constructor -> { var iterator = constructor.getParameters().iterator(); ParameterValueProvider

provider = new PersistentEntityParameterValueProvider<>(entity, propertyValueProvider, @@ -74,7 +74,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 788d4231a..51bb159cf 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(); var instance = INSTANCE.createInstance(entity, provider); @@ -92,7 +92,7 @@ class ReflectionEntityInstantiatorUnitTests

> { void createsInnerClassInstanceCorrectly() { var entity = new BasicPersistentEntity(from(Inner.class)); - assertThat(entity.getPersistenceConstructor()).satisfies(it -> { + assertThat(entity.getEntityCreator()).satisfies(it -> { var 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/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt index c4e3000ac..3b29fb7ff 100644 --- a/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt @@ -45,7 +45,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests { ) 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() @@ -73,7 +73,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests { 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() @@ -97,7 +97,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests { ) 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() @@ -130,7 +130,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() @@ -156,7 +156,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests { ) 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/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt index b33897852..217518e88 100644 --- a/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt @@ -43,7 +43,7 @@ class ReflectionEntityInstantiatorDataClassUnitTests { ) 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) @@ -63,7 +63,7 @@ class ReflectionEntityInstantiatorDataClassUnitTests { ) 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)