Introduce support to create domain objects via factory methods.

Issue #2476.
This commit is contained in:
Mark Paluch
2021-10-06 15:13:36 +02:00
committed by Oliver Drotbohm
parent 0ee9c55f05
commit c4a324e3cf
35 changed files with 1274 additions and 424 deletions

View File

@@ -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

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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<P extends PersistentProperty<P>> {
/**
* Check whether the given {@link PersistentProperty} is being used as creator parameter.
*
* @param property
* @return
*/
boolean isCreatorParameter(PersistentProperty<?> property);
/**
* Returns whether the given {@link Parameter} is one referring to parent value (such as an enclosing class or a
* receiver parameter).
*
* @param parameter
* @return
*/
default boolean isParentParameter(Parameter<?, P> parameter) {
return false;
}
/**
* @return the number of parameters.
*/
default int getParameterCount() {
return getParameters().size();
}
/**
* @return the parameters used by this creator.
*/
List<Parameter<Object, P>> getParameters();
/**
* @return whether the creator accepts {@link Parameter}s.
*/
default boolean hasParameters() {
return !getParameters().isEmpty();
}
}

View File

@@ -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<T, P extends PersistentProperty<P>> implements EntityCreatorMetadata<P> {
private final Executable executable;
private final List<Parameter<Object, P>> parameters;
private final Map<PersistentProperty<?>, 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<Object, P>... 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<Parameter<Object, P>> getParameters() {
return parameters;
}
/**
* Returns whether the given {@link PersistentProperty} is referenced in a creator argument of the
* {@link PersistentEntity} backing this {@link EntityCreatorMetadataSupport}.
* <p>
* 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<?, P> parameter : parameters) {
if (parameter.maps(property)) {
return true;
}
}
return false;
}
}

View File

@@ -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<T, P extends PersistentProperty<P>> extends EntityCreatorMetadataSupport<T, P> {
/**
* 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<Object, P>... parameters) {
super(factoryMethod, parameters);
ReflectionUtils.makeAccessible(factoryMethod);
}
/**
* Returns the underlying {@link Constructor}.
*
* @return
*/
public Method getFactoryMethod() {
return (Method) getExecutable();
}
}

View File

@@ -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 <T> the type of the parameter
* @author Oliver Gierke
*/
public class Parameter<T, P extends PersistentProperty<P>> {
private final @Nullable String name;
private final TypeInformation<T> type;
private final MergedAnnotations annotations;
private final String key;
private final @Nullable PersistentEntity<T, P> entity;
private final Lazy<Boolean> enclosingClassCache;
private final Lazy<Boolean> 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<T> type, Annotation[] annotations,
@Nullable PersistentEntity<T, P> 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<T> 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<T> 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();
}
}

View File

@@ -49,10 +49,24 @@ public interface PersistentEntity<T, P extends PersistentProperty<P>> 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<T, P> 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<P> 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<T, P extends PersistentProperty<P>> 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.

View File

@@ -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<T, P extends PersistentProperty<P>> {
public final class PreferredConstructor<T, P extends PersistentProperty<P>> extends EntityCreatorMetadataSupport<T, P> {
private final Constructor<T> constructor;
private final List<Parameter<Object, P>> parameters;
private final Map<PersistentProperty<?>, 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<T, P extends PersistentProperty<P>> {
@SafeVarargs
public PreferredConstructor(Constructor<T> constructor, Parameter<Object, P>... 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<T, P extends PersistentProperty<P>> {
* @return
*/
public Constructor<T> getConstructor() {
return constructor;
return (Constructor<T>) getExecutable();
}
/**
* Returns the {@link Parameter}s of the constructor.
*
* @return
*/
public List<Parameter<Object, P>> 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<T, P extends PersistentProperty<P>> {
* @return
*/
public boolean isNoArgConstructor() {
return parameters.isEmpty();
return !hasParameters();
}
/**
@@ -113,41 +80,22 @@ public class PreferredConstructor<T, P extends PersistentProperty<P>> {
* @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}.
* <p>
* 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<?, P> parameter : parameters) {
if (parameter.maps(property)) {
result = true;
break;
}
}
isPropertyParameterCache.put(property, result);
return result;
@Override
public boolean isParentParameter(Parameter<?, P> parameter) {
return isEnclosingClassParameter(parameter);
}
/**
@@ -169,176 +117,4 @@ public class PreferredConstructor<T, P extends PersistentProperty<P>> {
return parameters.get(0).equals(parameter);
}
/**
* Value object to represent constructor parameters.
*
* @param <T> the type of the parameter
* @author Oliver Gierke
*/
public static class Parameter<T, P extends PersistentProperty<P>> {
private final @Nullable String name;
private final TypeInformation<T> type;
private final MergedAnnotations annotations;
private final String key;
private final @Nullable PersistentEntity<T, P> entity;
private final Lazy<Boolean> enclosingClassCache;
private final Lazy<Boolean> 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<T> type, Annotation[] annotations,
@Nullable PersistentEntity<T, P> 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<T> 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<T> 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();
}
}
}

View File

@@ -63,7 +63,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> 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<T, P> constructor;
private final @Nullable EntityCreatorMetadata<P> creator;
private final TypeInformation<T> information;
private final List<P> properties;
private final List<P> persistentPropertiesCache;
@@ -109,7 +109,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> 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<T, P extends PersistentProperty<P>> 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<T, P> getPersistenceConstructor() {
return constructor;
return creator instanceof PreferredConstructor ? (PreferredConstructor<T, P>) creator : null;
}
public boolean isConstructorArgument(PersistentProperty<?> property) {
return constructor != null && constructor.isConstructorParameter(property);
@Override
public EntityCreatorMetadata<P> getEntityCreator() {
return creator;
}
@Override
public boolean isCreatorArgument(PersistentProperty<?> property) {
return creator != null && creator.isCreatorParameter(property);
}
public boolean isIdProperty(PersistentProperty<?> property) {

View File

@@ -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<TypeInformation<?>, 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 <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
ParameterValueProvider<P> 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 <P extends PersistentProperty<P>, T> Object[] extractInvocationArguments(
@Nullable PreferredConstructor<? extends T, P> constructor, ParameterValueProvider<P> provider) {
@Nullable EntityCreatorMetadata<P> constructor, ParameterValueProvider<P> 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<?, P> parameter : constructor.getParameters()) {
@@ -303,7 +336,7 @@ class ClassGeneratingEntityInstantiator implements EntityInstantiator {
public <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
ParameterValueProvider<P> 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 = "<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<? extends Parameter<Object, ?>> 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<? extends Parameter<Object, ?>> 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<? extends Parameter<Object, ?>> parameters = factoryMethod.getParameters();
visitParameterTypes(mv, parameterTypes, parameters);
mv.visitMethodInsn(INVOKESTATIC, entityTypeResourcePath, method.getName(), Type.getMethodDescriptor(method),
false);
}
private static void visitParameterTypes(MethodVisitor mv, Class<?>[] parameterTypes,
List<? extends Parameter<Object, ?>> 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 + "'");

View File

@@ -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;

View File

@@ -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 <T>
* @param <P>
* @return
*/
@Nullable
public static <T, P extends PersistentProperty<P>> EntityCreatorMetadata<P> discover(PersistentEntity<T, P> 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 <T, P extends PersistentProperty<P>> List<Method> discoverFactoryMethods(PersistentEntity<T, P> entity,
Method[] declaredMethods) {
List<Method> 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 <T, P extends PersistentProperty<P>> FactoryMethod<Object, P> getFactoryMethod(
PersistentEntity<T, P> entity, Method method) {
Parameter<Object, P>[] 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 <T> boolean isFactoryMethod(Method method, Class<T> type) {
// private methods not supported
if (Modifier.isPrivate(method.getModifiers())) {
return false;
}
// synthetic methods not supported
if (method.isSynthetic()) {
return false;
}
return Modifier.isStatic(method.getModifiers()) && method.getReturnType().isAssignableFrom(type);
}
private static boolean findAnnotation(Class<? extends Annotation> annotationType, AnnotatedElement... elements) {
for (var element : elements) {
if (MergedAnnotations.from(element).isPresent(annotationType)) {
return true;
}
}
return false;
}
}

View File

@@ -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<T> 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()));
}
});

View File

@@ -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 <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
ParameterValueProvider<P> 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 <P extends PersistentProperty<P>, T> Object[] extractInvocationArguments(
@Nullable PreferredConstructor<? extends T, P> preferredConstructor, ParameterValueProvider<P> provider) {
@Nullable EntityCreatorMetadata<P> entityCreator, ParameterValueProvider<P> 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++) {

View File

@@ -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<Object> 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<? extends PreferredConstructor<?, ?>> constructor = Optional.ofNullable(it.getPersistenceConstructor());
Optional<? extends EntityCreatorMetadata<?>> constructor = Optional.ofNullable(it.getEntityCreator());
List<String> 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<Constructor<?>> 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<EntityCreatorMetadata<?>> getEntityCreator() {
return Optional.ofNullable(entityCreator);
}
/**

View File

@@ -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;
/**

View File

@@ -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<P extends PersistentProperty
@SuppressWarnings("unchecked")
public <T> T getParameterValue(Parameter<T, P> 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();

View File

@@ -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<T, P extends PersistentProperty<
continue;
}
if (AnnotationUtils.findAnnotation(candidate, PersistenceConstructor.class) != null) {
if (AnnotationUtils.findAnnotation(candidate, EntityCreatorAnnotation.class) != null) {
return buildPreferredConstructor(candidate, type, entity);
}
@@ -148,7 +145,10 @@ public interface PreferredConstructorDiscoverer<T, P extends PersistentProperty<
return Arrays.stream(rawOwningType.getDeclaredConstructors()) //
.filter(it -> !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() //

View File

@@ -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 <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
ParameterValueProvider<P> 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<?, P> parameter : constructor.getParameters()) {
for (Parameter<?, P> 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<T, ?>) creator).getConstructor(), params);
} catch (BeanInstantiationException e) {
throw new MappingInstantiationException(entity, new ArrayList<>(Arrays.asList(params)), e);
}
}
private <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> 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);
}
}
}

View File

@@ -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;
/**

View File

@@ -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<String> properties = new ArrayList<>(constructor.getConstructor().getParameterCount());
for (PreferredConstructor.Parameter<Object, ?> parameter : constructor.getParameters()) {
for (Parameter<Object, ?> parameter : constructor.getParameters()) {
properties.add(parameter.getName());
}

View File

@@ -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;

View File

@@ -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<P extends PersistentProperty<P>> {
assertThat(PreferredConstructorDiscoverer.discover(entity)).satisfies(constructor -> {
Parameter<?, P> parameter = constructor.getParameters().iterator().next();
assertThat(constructor.isEnclosingClassParameter(parameter)).isTrue();
assertThat(constructor.isParentParameter(parameter)).isTrue();
});
}

View File

@@ -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<P extends PersistentProperty<P>> {
ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator();
ClassGeneratingEntityInstantiator instance = new ClassGeneratingEntityInstantiator(false);
@Mock PersistentEntity<?, P> entity;
@Mock ParameterValueProvider<P> provider;
@@ -83,7 +84,7 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
PreferredConstructor<Foo, P> 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<P extends PersistentProperty<P>
void createsInnerClassInstanceCorrectly() {
var entity = new BasicPersistentEntity<Inner, P>(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<P extends PersistentProperty<P>
assertThat(reference.sample.name).isEqualTo("FOO");
}
@Test // DATACMNS-1175
@SuppressWarnings({ "unchecked", "rawtypes" })
void createsInstancesWithFactoryMethodCorrectly() {
PersistentEntity<WithFactoryMethod, P> entity = new BasicPersistentEntity<>(from(WithFactoryMethod.class));
doReturn(2L, "FOO").when(provider).getParameterValue(any(Parameter.class));
var provider = new ParameterValueProvider<P>() {
@Override
public <T> T getParameterValue(Parameter<T, P> 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<P extends PersistentProperty<P>
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<P extends PersistentProperty<P>
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<P extends PersistentProperty<P>
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<P extends PersistentProperty<P>
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<P extends PersistentProperty<P>
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<P extends PersistentProperty<P>
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<P extends PersistentProperty<P>
doReturn(type).when(entity).getType();
doReturn(PreferredConstructorDiscoverer.discover(type))//
.when(entity).getPersistenceConstructor();
.when(entity).getEntityCreator();
}
static class Foo {
@@ -429,6 +461,23 @@ class ClassGeneratingEntityInstantiatorUnitTests<P extends PersistentProperty<P>
}
}
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;

View File

@@ -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);
}
}
}

View File

@@ -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() {

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -55,7 +55,7 @@ class PersistentEntityParameterValueProviderUnitTests<P extends PersistentProper
}
};
assertThat(entity.getPersistenceConstructor()).satisfies(constructor -> {
assertThat(entity.getEntityCreator()).satisfies(constructor -> {
var iterator = constructor.getParameters().iterator();
ParameterValueProvider<P> provider = new PersistentEntityParameterValueProvider<>(entity, propertyValueProvider,
@@ -74,7 +74,7 @@ class PersistentEntityParameterValueProviderUnitTests<P extends PersistentProper
ParameterValueProvider<P> 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")//

View File

@@ -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<P extends PersistentProperty<P>> {
PreferredConstructor<Foo, P> 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<P extends PersistentProperty<P>> {
void createsInnerClassInstanceCorrectly() {
var entity = new BasicPersistentEntity<Inner, P>(from(Inner.class));
assertThat(entity.getPersistenceConstructor()).satisfies(it -> {
assertThat(entity.getEntityCreator()).satisfies(it -> {
var parameter = it.getParameters().iterator().next();

View File

@@ -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;
/**

View File

@@ -45,7 +45,7 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
)
every { provider.getParameterValue<String>(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<Boolean>(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<Double>(any()) } returns null
every { provider.getParameterValue<Char>(any()) } returns null
every { provider.getParameterValue<Boolean>(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<String>(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()

View File

@@ -43,7 +43,7 @@ class ReflectionEntityInstantiatorDataClassUnitTests {
)
every { provider.getParameterValue<String>(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<String>(any()) }.returnsMany("Walter", null)
every { entity.persistenceConstructor } returns constructor
every { entity.entityCreator } returns constructor
val instance: ContactWithDefaulting =
ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)