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

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