DATACMNS-1126 - Add class-generating entity instantiation support for Kotlin.
We now discover Kotlin constructors and apply parameter defaulting to allow construction of immutable value objects. Constructor discovery uses primary constructors by default and considers PersistenceConstructor annotations.
KotlinClassGeneratingEntityInstantiator can instantiate Kotlin classes with default parameters by resolving the synthetic constructor. Null values translate to parameter defaults.
class Person(val firstname: String = "Walter") {
}
class Address(val street: String, val city: String) {
@PersistenceConstructor
constructor(street: String = "Unknown", city: String = "Unknown", country: String) : this(street, city)
}
Original pull request: #233.
This commit is contained in:
committed by
Oliver Gierke
parent
e4e677d6b1
commit
22b37800ec
@@ -21,10 +21,8 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.asm.ClassWriter;
|
||||
@@ -57,6 +55,19 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
|
||||
private static final int ARG_CACHE_SIZE = 100;
|
||||
|
||||
private static final ThreadLocal<Object[][]> OBJECT_POOL = ThreadLocal.withInitial(() -> {
|
||||
|
||||
Object[][] cached = new Object[ARG_CACHE_SIZE][];
|
||||
|
||||
for (int i = 0; i < ARG_CACHE_SIZE; i++) {
|
||||
cached[i] = new Object[i];
|
||||
}
|
||||
|
||||
return cached;
|
||||
});
|
||||
|
||||
private final ObjectInstantiatorClassGenerator generator;
|
||||
|
||||
private volatile Map<TypeInformation<?>, EntityInstantiator> entityInstantiators = new HashMap<>(32);
|
||||
@@ -120,12 +131,20 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
}
|
||||
|
||||
try {
|
||||
return new EntityInstantiatorAdapter(createObjectInstantiator(entity));
|
||||
return doCreateEntityInstantiator(entity);
|
||||
} catch (Throwable ex) {
|
||||
return ReflectionEntityInstantiator.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity<?, ?> entity) {
|
||||
return new EntityInstantiatorAdapter(createObjectInstantiator(entity, entity.getPersistenceConstructor()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param entity
|
||||
* @return
|
||||
@@ -151,17 +170,46 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity}. There will
|
||||
* always be exactly one {@link ObjectInstantiator} instance per {@link PersistentEntity}.
|
||||
* <p>
|
||||
* Allocates an object array for instance creation. This method uses the argument array cache if possible.
|
||||
*
|
||||
* @param argumentCount
|
||||
* @return
|
||||
* @since 2.0
|
||||
* @see #ARG_CACHE_SIZE
|
||||
*/
|
||||
static Object[] allocateArguments(int argumentCount) {
|
||||
return argumentCount < ARG_CACHE_SIZE ? OBJECT_POOL.get()[argumentCount] : new Object[argumentCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocates an object array used for instance creation. Parameters are cleared if the array was cached.
|
||||
*
|
||||
* @param argumentCount
|
||||
* @return
|
||||
* @since 2.0
|
||||
* @see #ARG_CACHE_SIZE
|
||||
*/
|
||||
static void deallocateArguments(Object[] params) {
|
||||
|
||||
if (params.length != 0 && params.length < ARG_CACHE_SIZE) {
|
||||
Arrays.fill(params, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and
|
||||
* {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per
|
||||
* {@link PersistentEntity}.
|
||||
*
|
||||
* @param entity
|
||||
* @param constructor
|
||||
* @return
|
||||
*/
|
||||
private ObjectInstantiator createObjectInstantiator(PersistentEntity<?, ?> entity) {
|
||||
ObjectInstantiator createObjectInstantiator(PersistentEntity<?, ?> entity,
|
||||
@Nullable PreferredConstructor<?, ?> constructor) {
|
||||
|
||||
try {
|
||||
return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity).newInstance();
|
||||
return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -172,11 +220,10 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
*
|
||||
* @author Thomas Darimont
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
private static class EntityInstantiatorAdapter implements EntityInstantiator {
|
||||
|
||||
private static final Object[] EMPTY_ARRAY = new Object[0];
|
||||
|
||||
private final ObjectInstantiator instantiator;
|
||||
|
||||
/**
|
||||
@@ -203,6 +250,8 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
return (T) instantiator.newInstance(params);
|
||||
} catch (Exception e) {
|
||||
throw new MappingInstantiationException(entity, Arrays.asList(params), e);
|
||||
} finally {
|
||||
deallocateArguments(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,17 +265,18 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
private <P extends PersistentProperty<P>, T> Object[] extractInvocationArguments(
|
||||
@Nullable PreferredConstructor<? extends T, P> constructor, ParameterValueProvider<P> provider) {
|
||||
|
||||
if (provider == null || constructor == null || !constructor.hasParameters()) {
|
||||
return EMPTY_ARRAY;
|
||||
if (constructor == null || !constructor.hasParameters()) {
|
||||
return allocateArguments(0);
|
||||
}
|
||||
|
||||
List<Object> params = new ArrayList<>(constructor.getConstructor().getParameterCount());
|
||||
Object[] params = allocateArguments(constructor.getConstructor().getParameterCount());
|
||||
|
||||
int index = 0;
|
||||
for (Parameter<?, P> parameter : constructor.getParameters()) {
|
||||
params.add(provider.getParameterValue(parameter));
|
||||
params[index++] = provider.getParameterValue(parameter);
|
||||
}
|
||||
|
||||
return params.toArray();
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +340,7 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
|
||||
private final ByteArrayClassLoader classLoader;
|
||||
|
||||
private ObjectInstantiatorClassGenerator() {
|
||||
ObjectInstantiatorClassGenerator() {
|
||||
|
||||
this.classLoader = AccessController.doPrivileged(
|
||||
(PrivilegedAction<ByteArrayClassLoader>) () -> new ByteArrayClassLoader(ClassUtils.getDefaultClassLoader()));
|
||||
@@ -300,12 +350,14 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
* Generate a new class for the given {@link PersistentEntity}.
|
||||
*
|
||||
* @param entity
|
||||
* @param constructor
|
||||
* @return
|
||||
*/
|
||||
public Class<?> generateCustomInstantiatorClass(PersistentEntity<?, ?> entity) {
|
||||
public Class<?> generateCustomInstantiatorClass(PersistentEntity<?, ?> entity,
|
||||
@Nullable PreferredConstructor<?, ?> constructor) {
|
||||
|
||||
String className = generateClassName(entity);
|
||||
byte[] bytecode = generateBytecode(className, entity);
|
||||
byte[] bytecode = generateBytecode(className, entity, constructor);
|
||||
|
||||
return classLoader.loadClass(className, bytecode);
|
||||
}
|
||||
@@ -323,9 +375,11 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
*
|
||||
* @param internalClassName
|
||||
* @param entity
|
||||
* @param constructor
|
||||
* @return
|
||||
*/
|
||||
public byte[] generateBytecode(String internalClassName, PersistentEntity<?, ?> entity) {
|
||||
public byte[] generateBytecode(String internalClassName, PersistentEntity<?, ?> entity,
|
||||
@Nullable PreferredConstructor<?, ?> constructor) {
|
||||
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
|
||||
@@ -334,7 +388,7 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
|
||||
visitDefaultConstructor(cw);
|
||||
|
||||
visitCreateMethod(cw, entity);
|
||||
visitCreateMethod(cw, entity, constructor);
|
||||
|
||||
cw.visitEnd();
|
||||
|
||||
@@ -357,8 +411,10 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
*
|
||||
* @param cw
|
||||
* @param entity
|
||||
* @param constructor
|
||||
*/
|
||||
private void visitCreateMethod(ClassWriter cw, PersistentEntity<?, ?> entity) {
|
||||
private void visitCreateMethod(ClassWriter cw, PersistentEntity<?, ?> entity,
|
||||
@Nullable PreferredConstructor<?, ?> constructor) {
|
||||
|
||||
String entityTypeResourcePath = Type.getInternalName(entity.getType());
|
||||
|
||||
@@ -368,8 +424,6 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator {
|
||||
mv.visitTypeInsn(NEW, entityTypeResourcePath);
|
||||
mv.visitInsn(DUP);
|
||||
|
||||
PreferredConstructor<?, ?> constructor = entity.getPersistenceConstructor();
|
||||
|
||||
if (constructor != null) {
|
||||
|
||||
Constructor<?> ctor = constructor.getConstructor();
|
||||
|
||||
@@ -24,10 +24,11 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* Simple value object allowing access to {@link EntityInstantiator} instances for a given type falling back to a
|
||||
* default one.
|
||||
*
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class EntityInstantiators {
|
||||
|
||||
@@ -43,7 +44,7 @@ public class EntityInstantiators {
|
||||
|
||||
/**
|
||||
* Creates a new {@link EntityInstantiators} using the given {@link EntityInstantiator} as fallback.
|
||||
*
|
||||
*
|
||||
* @param fallback must not be {@literal null}.
|
||||
*/
|
||||
public EntityInstantiators(EntityInstantiator fallback) {
|
||||
@@ -52,17 +53,17 @@ public class EntityInstantiators {
|
||||
|
||||
/**
|
||||
* Creates a new {@link EntityInstantiators} using the default fallback instantiator and the given custom ones.
|
||||
*
|
||||
*
|
||||
* @param customInstantiators must not be {@literal null}.
|
||||
*/
|
||||
public EntityInstantiators(Map<Class<?>, EntityInstantiator> customInstantiators) {
|
||||
this(new ClassGeneratingEntityInstantiator(), customInstantiators);
|
||||
this(new ClassGeneratingKotlinEntityInstantiator(), customInstantiators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link EntityInstantiator} using the given fallback {@link EntityInstantiator} and the given custom
|
||||
* ones.
|
||||
*
|
||||
*
|
||||
* @param defaultInstantiator must not be {@literal null}.
|
||||
* @param customInstantiators must not be {@literal null}.
|
||||
*/
|
||||
@@ -78,7 +79,7 @@ public class EntityInstantiators {
|
||||
|
||||
/**
|
||||
* Returns the {@link EntityInstantiator} to be used to create the given {@link PersistentEntity}.
|
||||
*
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.convert;
|
||||
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.mapping.PreferredConstructor.Parameter;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.util.ReflectionUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Kotlin-specific extension to {@link ClassGeneratingEntityInstantiator} that adapts Kotlin constructors with
|
||||
* defaulting.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ClassGeneratingKotlinEntityInstantiator extends ClassGeneratingEntityInstantiator {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity)
|
||||
*/
|
||||
@Override
|
||||
protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity<?, ?> entity) {
|
||||
|
||||
if (ReflectionUtils.isKotlinClass(entity.getType()) && entity.getPersistenceConstructor() != null) {
|
||||
|
||||
PreferredConstructor<?, ?> defaultConstructor = new DefaultingKotlinConstructorResolver(entity)
|
||||
.getDefaultConstructor();
|
||||
|
||||
if (defaultConstructor != null) {
|
||||
return new DefaultingKotlinClassInstantiatorAdapter(createObjectInstantiator(entity, defaultConstructor),
|
||||
entity.getPersistenceConstructor());
|
||||
}
|
||||
}
|
||||
|
||||
return super.doCreateEntityInstantiator(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a {@link PreferredConstructor} to a synthetic Kotlin constructor accepting the same user-space parameters
|
||||
* suffixed by Kotlin-specifics required for defaulting and the {@code kotlin.jvm.internal.DefaultConstructorMarker}.
|
||||
*
|
||||
* @since 2.0
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
static class DefaultingKotlinConstructorResolver {
|
||||
|
||||
@Nullable private final PreferredConstructor<?, ?> defaultConstructor;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
DefaultingKotlinConstructorResolver(PersistentEntity<?, ?> entity) {
|
||||
|
||||
Constructor<?> hit = resolveDefaultConstructor(entity);
|
||||
PreferredConstructor<?, ?> persistenceConstructor = entity.getPersistenceConstructor();
|
||||
|
||||
if (hit != null && persistenceConstructor != null) {
|
||||
this.defaultConstructor = new PreferredConstructor<>(hit,
|
||||
persistenceConstructor.getParameters().toArray(new Parameter[0]));
|
||||
} else {
|
||||
this.defaultConstructor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Constructor<?> resolveDefaultConstructor(PersistentEntity<?, ?> entity) {
|
||||
|
||||
if (entity.getPersistenceConstructor() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Constructor<?> hit = null;
|
||||
Constructor<?> constructor = entity.getPersistenceConstructor().getConstructor();
|
||||
|
||||
for (Constructor<?> candidate : entity.getType().getDeclaredConstructors()) {
|
||||
|
||||
// use only synthetic constructors
|
||||
if (!candidate.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// with a parameter count greater zero
|
||||
if (constructor.getParameterCount() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// candidates must contain at least two additional parameters (int, DefaultConstructorMarker)
|
||||
if (constructor.getParameterCount() + 2 > candidate.getParameterCount()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
java.lang.reflect.Parameter[] constructorParameters = constructor.getParameters();
|
||||
java.lang.reflect.Parameter[] candidateParameters = candidate.getParameters();
|
||||
|
||||
if (!candidateParameters[candidateParameters.length - 1].getType().getName()
|
||||
.equals("kotlin.jvm.internal.DefaultConstructorMarker")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parametersMatch(constructorParameters, candidateParameters)) {
|
||||
hit = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
private static boolean parametersMatch(java.lang.reflect.Parameter[] constructorParameters,
|
||||
java.lang.reflect.Parameter[] candidateParameters) {
|
||||
|
||||
return IntStream.range(0, constructorParameters.length)
|
||||
.allMatch(i -> constructorParameters[i].getType().equals(candidateParameters[i].getType()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
PreferredConstructor<?, ?> getDefaultConstructor() {
|
||||
return defaultConstructor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity instantiator for Kotlin constructors that apply parameter defaulting. Kotlin constructors that apply
|
||||
* argument defaulting are marked with {@link kotlin.jvm.internal.DefaultConstructorMarker} and accept additional
|
||||
* parameters besides the regular (user-space) parameters. Additional parameters are:
|
||||
* <ul>
|
||||
* <li>defaulting bitmask ({@code int}), a bit mask slot for each 32 parameters</li>
|
||||
* <li>{@code DefaultConstructorMarker} (usually null)</li>
|
||||
* </ul>
|
||||
* <strong>Defaulting bitmask</strong>
|
||||
* <p/>
|
||||
* The defaulting bitmask is a 32 bit integer representing which positional argument should be defaulted. Defaulted
|
||||
* arguments are passed as {@literal null} and require the appropriate positional bit set ( {@code 1 << 2} for the 2.
|
||||
* argument)). Since the bitmask represents only 32 bit states, it requires additional masks (slots) if more than 32
|
||||
* arguments are represented.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantiator {
|
||||
|
||||
private final ObjectInstantiator instantiator;
|
||||
private final List<KParameter> kParameters;
|
||||
private final Constructor<?> synthetic;
|
||||
|
||||
DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator, PreferredConstructor<?, ?> constructor) {
|
||||
|
||||
KFunction<?> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor.getConstructor());
|
||||
|
||||
if (kotlinConstructor == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"No corresponding Kotlin constructor found for " + constructor.getConstructor());
|
||||
}
|
||||
|
||||
this.instantiator = instantiator;
|
||||
this.kParameters = kotlinConstructor.getParameters();
|
||||
this.synthetic = constructor.getConstructor();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
|
||||
ParameterValueProvider<P> provider) {
|
||||
|
||||
PreferredConstructor<? extends T, P> preferredConstructor = entity.getPersistenceConstructor();
|
||||
Assert.notNull(preferredConstructor, "PreferredConstructor must not be null!");
|
||||
|
||||
int[] defaulting = new int[(synthetic.getParameterCount() / 32) + 1];
|
||||
|
||||
Object[] params = allocateArguments(
|
||||
synthetic.getParameterCount() + defaulting.length + /* DefaultConstructorMarker */1);
|
||||
int userParameterCount = kParameters.size();
|
||||
|
||||
List<Parameter<Object, P>> parameters = preferredConstructor.getParameters();
|
||||
|
||||
// Prepare user-space arguments
|
||||
for (int i = 0; i < userParameterCount; i++) {
|
||||
|
||||
int slot = i / 32;
|
||||
int offset = slot * 32;
|
||||
|
||||
Object param = provider.getParameterValue(parameters.get(i));
|
||||
|
||||
KParameter kParameter = kParameters.get(i);
|
||||
|
||||
// what about null and parameter is mandatory? What if parameter is non-null?
|
||||
if (kParameter.isOptional()) {
|
||||
|
||||
if (param == null) {
|
||||
defaulting[slot] = defaulting[slot] | (1 << (i - offset));
|
||||
}
|
||||
}
|
||||
|
||||
params[i] = param;
|
||||
}
|
||||
|
||||
// append nullability masks to creation arguments
|
||||
for (int i = 0; i < defaulting.length; i++) {
|
||||
params[userParameterCount + i] = defaulting[i];
|
||||
}
|
||||
|
||||
try {
|
||||
return (T) instantiator.newInstance(params);
|
||||
} finally {
|
||||
deallocateArguments(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user