Skip to content

Commit bd0d729

Browse files
committed
Polishing.
Refine KotlinInstantiationDelegate design, improve encapsulation to avoid handing in and out values from lookups. Replace stream usage with loops, remove unused code, avoid duplicate parameter lookups. See #3389 Original pull request: #3390
1 parent f80a432 commit bd0d729

File tree

3 files changed

+145
-91
lines changed

3 files changed

+145
-91
lines changed

src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.springframework.data.mapping.model;
1717

18-
import java.lang.reflect.Constructor;
1918
import java.util.Arrays;
2019

2120
import org.springframework.data.mapping.InstanceCreatorMetadata;
@@ -43,15 +42,11 @@ protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity<?, ?> e
4342
if (KotlinReflectionUtils.isSupportedKotlinClass(entity.getType())
4443
&& creator instanceof PreferredConstructor<?, ?> constructor) {
4544

46-
PreferredConstructor<?, ? extends PersistentProperty<?>> kotlinJvmConstructor = KotlinInstantiationDelegate
47-
.resolveKotlinJvmConstructor(constructor);
45+
KotlinInstantiationDelegate delegate = KotlinInstantiationDelegate.resolve(constructor);
4846

49-
if (kotlinJvmConstructor != null) {
50-
51-
ObjectInstantiator instantiator = createObjectInstantiator(entity, kotlinJvmConstructor);
52-
53-
return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor,
54-
kotlinJvmConstructor.getConstructor());
47+
if (delegate != null) {
48+
ObjectInstantiator instantiator = createObjectInstantiator(entity, delegate.getInstanceCreator());
49+
return new DefaultingKotlinClassInstantiatorAdapter(instantiator, delegate);
5550
}
5651
}
5752

@@ -82,10 +77,10 @@ static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantia
8277
private final KotlinInstantiationDelegate delegate;
8378

8479
DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator,
85-
PreferredConstructor<?, ?> defaultConstructor, Constructor<?> constructorToInvoke) {
80+
KotlinInstantiationDelegate delegate) {
8681

8782
this.instantiator = instantiator;
88-
this.delegate = new KotlinInstantiationDelegate(defaultConstructor, constructorToInvoke);
83+
this.delegate = delegate;
8984
}
9085

9186
@Override

src/main/java/org/springframework/data/mapping/model/KotlinInstantiationDelegate.java

Lines changed: 134 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.List;
2727
import java.util.Map;
2828
import java.util.function.Function;
29-
import java.util.stream.IntStream;
3029

3130
import org.jspecify.annotations.Nullable;
3231

@@ -49,31 +48,27 @@
4948
*/
5049
class KotlinInstantiationDelegate {
5150

52-
private final KFunction<?> constructor;
51+
private final PreferredConstructor<?, ?> constructor;
52+
private final KFunction<?> constructorFunction;
5353
private final List<KParameter> kParameters;
5454
private final Map<KParameter, Integer> indexByKParameter;
55-
private final List<Function<@Nullable Object, @Nullable Object>> wrappers = new ArrayList<>();
56-
private final Constructor<?> constructorToInvoke;
55+
private final List<Function<@Nullable Object, @Nullable Object>> wrappers;
56+
private final boolean hasDefaultConstructorMarker;
5757

58-
public KotlinInstantiationDelegate(PreferredConstructor<?, ?> preferredConstructor,
59-
Constructor<?> constructorToInvoke) {
58+
private KotlinInstantiationDelegate(PreferredConstructor<?, ?> constructor, KFunction<?> constructorFunction) {
6059

61-
KFunction<?> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(preferredConstructor.getConstructor());
60+
this.constructor = constructor;
61+
this.hasDefaultConstructorMarker = hasDefaultConstructorMarker(getConstructor().getParameters());
6262

63-
if (kotlinConstructor == null) {
64-
throw new IllegalArgumentException(
65-
"No corresponding Kotlin constructor found for " + preferredConstructor.getConstructor());
66-
}
67-
68-
this.constructor = kotlinConstructor;
69-
this.kParameters = kotlinConstructor.getParameters();
70-
this.indexByKParameter = new IdentityHashMap<>();
63+
this.constructorFunction = constructorFunction;
64+
this.kParameters = constructorFunction.getParameters();
65+
this.indexByKParameter = new IdentityHashMap<>(kParameters.size());
7166

7267
for (int i = 0; i < kParameters.size(); i++) {
7368
indexByKParameter.put(kParameters.get(i), i);
7469
}
7570

76-
this.constructorToInvoke = constructorToInvoke;
71+
this.wrappers = new ArrayList<>(kParameters.size());
7772

7873
for (KParameter kParameter : kParameters) {
7974

@@ -82,17 +77,32 @@ public KotlinInstantiationDelegate(PreferredConstructor<?, ?> preferredConstruct
8277
}
8378
}
8479

85-
static boolean hasDefaultConstructorMarker(java.lang.reflect.Parameter[] parameters) {
80+
/**
81+
* @return the constructor to invoke. {@link PreferredConstructor#getParameters() Constructor parameters} describe the
82+
* detected (i.e. user-facing) constructor parameters and not {@link PreferredConstructor#getConstructor()}
83+
* parameters and therefore do not contain any synthetic parameters.
84+
* @since 4.0
85+
*/
86+
public InstanceCreatorMetadata<?> getInstanceCreator() {
87+
return constructor;
88+
}
8689

87-
return parameters.length > 0
88-
&& parameters[parameters.length - 1].getType().getName().equals("kotlin.jvm.internal.DefaultConstructorMarker");
90+
/**
91+
* @return the constructor to invoke. {@link PreferredConstructor#getParameters() Constructor parameters} describe the
92+
* detected (i.e. user-facing) constructor parameters and not {@link PreferredConstructor#getConstructor()}
93+
* parameters and therefore do not contain any synthetic parameters.
94+
* @since 4.0
95+
*/
96+
public Constructor<?> getConstructor() {
97+
return constructor.getConstructor();
8998
}
9099

91100
/**
92-
* @return number of constructor arguments.
101+
* @return number of actual constructor arguments.
102+
* @see #getConstructor()
93103
*/
94104
public int getRequiredParameterCount() {
95-
return constructorToInvoke.getParameterCount();
105+
return getConstructor().getParameterCount();
96106
}
97107

98108
/**
@@ -107,7 +117,6 @@ public <P extends PersistentProperty<P>> void extractInvocationArguments(@Nullab
107117
}
108118

109119
int userParameterCount = kParameters.size();
110-
111120
List<Parameter<Object, P>> parameters = entityCreator.getParameters();
112121

113122
// Prepare user-space arguments
@@ -117,54 +126,83 @@ public <P extends PersistentProperty<P>> void extractInvocationArguments(@Nullab
117126
params[i] = provider.getParameterValue(parameter);
118127
}
119128

120-
KotlinDefaultMask defaultMask = KotlinDefaultMask.forConstructor(constructor, it -> {
129+
// late rewrapping to indicate potential absence of parameters for defaulting
130+
for (int i = 0; i < userParameterCount; i++) {
131+
params[i] = wrappers.get(i).apply(params[i]);
132+
}
121133

122-
int index = indexByKParameter.get(it);
134+
if (hasDefaultConstructorMarker) {
123135

124-
Parameter<Object, P> parameter = parameters.get(index);
125-
Class<Object> type = parameter.getType().getType();
136+
KotlinDefaultMask defaultMask = KotlinDefaultMask.forConstructor(constructorFunction, it -> {
126137

127-
if (it.isOptional() && (params[index] == null)) {
128-
if (type.isPrimitive()) {
138+
int index = indexByKParameter.get(it);
129139

130-
// apply primitive defaulting to prevent NPE on primitive downcast
131-
params[index] = ReflectionUtils.getPrimitiveDefault(type);
140+
Parameter<Object, P> parameter = parameters.get(index);
141+
Class<Object> type = parameter.getType().getType();
142+
143+
if (it.isOptional() && (params[index] == null)) {
144+
if (type.isPrimitive()) {
145+
146+
// apply primitive defaulting to prevent NPE on primitive downcast
147+
params[index] = ReflectionUtils.getPrimitiveDefault(type);
148+
}
149+
return false;
132150
}
133-
return false;
134-
}
135151

136-
return true;
137-
});
152+
return true;
153+
});
138154

139-
// late rewrapping to indicate potential absence of parameters for defaulting
140-
for (int i = 0; i < userParameterCount; i++) {
141-
params[i] = wrappers.get(i).apply(params[i]);
155+
int[] defaulting = defaultMask.getDefaulting();
156+
// append nullability masks to creation arguments
157+
for (int i = 0; i < defaulting.length; i++) {
158+
params[userParameterCount + i] = defaulting[i];
159+
}
142160
}
161+
}
162+
163+
/**
164+
* Try to resolve {@code KotlinInstantiationDelegate} from a {@link PreferredConstructor}. Resolution attempts to find
165+
* a JVM constructor equivalent considering value class mangling, Kotlin defaulting and potentially synthetic
166+
* constructors generated by the Kotlin compile including the lookup of a {@link KFunction} from the given
167+
* {@link PreferredConstructor}.
168+
*
169+
* @return the {@code KotlinInstantiationDelegate} if resolution was successful; {@literal null} otherwise.
170+
* @since 4.0
171+
*/
172+
public static @Nullable KotlinInstantiationDelegate resolve(PreferredConstructor<?, ?> preferredConstructor) {
143173

144-
int[] defaulting = defaultMask.getDefaulting();
145-
// append nullability masks to creation arguments
146-
for (int i = 0; i < defaulting.length; i++) {
147-
params[userParameterCount + i] = defaulting[i];
174+
KFunction<?> constructorFunction = ReflectJvmMapping.getKotlinFunction(preferredConstructor.getConstructor());
175+
176+
if (constructorFunction == null) {
177+
return null;
148178
}
179+
180+
PreferredConstructor<?, ?> resolved = resolveKotlinJvmConstructor(preferredConstructor, constructorFunction);
181+
return resolved != null ? new KotlinInstantiationDelegate(resolved, constructorFunction) : null;
149182
}
150183

151184
/**
152185
* Resolves a {@link PreferredConstructor} to the constructor to be invoked. This can be a synthetic Kotlin
153186
* constructor accepting the same user-space parameters suffixed by Kotlin-specifics required for defaulting and the
154187
* {@code kotlin.jvm.internal.DefaultConstructorMarker} or an actual non-synthetic constructor (i.e. private
155188
* constructor).
189+
* <p>
190+
* Constructor resolution may return {@literal null} indicating that no matching constructor could be found.
191+
* <p>
192+
* The resulting constructor {@link PreferredConstructor#getParameters()} (and parameter count) reflect user-facing
193+
* parameters and do not contain any synthetic parameters.
156194
*
195+
* @return the resolved constructor or {@literal null} if the constructor could not be resolved.
157196
* @since 2.0
158-
* @author Mark Paluch
159197
*/
160198
@SuppressWarnings("unchecked")
161199
@Nullable
162-
public static PreferredConstructor<?, ?> resolveKotlinJvmConstructor(
163-
PreferredConstructor<?, ?> preferredConstructor) {
200+
private static PreferredConstructor<?, ?> resolveKotlinJvmConstructor(PreferredConstructor<?, ?> preferredConstructor,
201+
KFunction<?> constructorFunction) {
164202

165-
Constructor<?> hit = doResolveKotlinConstructor(preferredConstructor.getConstructor());
203+
Constructor<?> hit = findKotlinConstructor(preferredConstructor.getConstructor(), constructorFunction);
166204

167-
if (hit == preferredConstructor.getConstructor()) {
205+
if (preferredConstructor.getConstructor().equals(hit)) {
168206
return preferredConstructor;
169207
}
170208

@@ -176,17 +214,23 @@ public <P extends PersistentProperty<P>> void extractInvocationArguments(@Nullab
176214
}
177215

178216
@Nullable
179-
private static Constructor<?> doResolveKotlinConstructor(Constructor<?> detectedConstructor) {
217+
private static Constructor<?> findKotlinConstructor(Constructor<?> preferredConstructor,
218+
KFunction<?> constructorFunction) {
180219

181-
Class<?> entityType = detectedConstructor.getDeclaringClass();
220+
Class<?> entityType = preferredConstructor.getDeclaringClass();
182221
Constructor<?> hit = null;
183222
Constructor<?> privateFallback = null;
184-
KFunction<?> kotlinFunction = ReflectJvmMapping.getKotlinFunction(detectedConstructor);
223+
java.lang.reflect.Parameter[] detectedParameters = preferredConstructor.getParameters();
224+
boolean hasDefaultConstructorMarker = KotlinInstantiationDelegate.hasDefaultConstructorMarker(detectedParameters);
185225

186226
for (Constructor<?> candidate : entityType.getDeclaredConstructors()) {
187227

228+
java.lang.reflect.Parameter[] candidateParameters = preferredConstructor.equals(candidate)
229+
? detectedParameters
230+
: candidate.getParameters();
231+
188232
if (Modifier.isPrivate(candidate.getModifiers())) {
189-
if (detectedConstructor.equals(candidate)) {
233+
if (preferredConstructor.equals(candidate)) {
190234
privateFallback = candidate;
191235
}
192236
}
@@ -196,26 +240,22 @@ private static Constructor<?> doResolveKotlinConstructor(Constructor<?> detected
196240
continue;
197241
}
198242

199-
java.lang.reflect.Parameter[] detectedConstructorParameters = detectedConstructor.getParameters();
200-
java.lang.reflect.Parameter[] candidateParameters = candidate.getParameters();
201-
202-
if (!KotlinInstantiationDelegate.hasDefaultConstructorMarker(detectedConstructorParameters)) {
243+
if (!hasDefaultConstructorMarker) {
203244

204245
// candidates must contain at least two additional parameters (int, DefaultConstructorMarker).
205246
// Number of defaulting masks derives from the original constructor arg count
206-
int syntheticParameters = KotlinDefaultMask.getMaskCount(detectedConstructor.getParameterCount())
247+
int syntheticParameters = KotlinDefaultMask.getMaskCount(detectedParameters.length)
207248
+ /* DefaultConstructorMarker */ 1;
208249

209-
if ((detectedConstructor.getParameterCount() + syntheticParameters) != candidate.getParameterCount()) {
250+
if ((detectedParameters.length + syntheticParameters) != candidate.getParameterCount()) {
210251
continue;
211252
}
212-
} else if (kotlinFunction != null) {
253+
} else {
213254

214-
int optionalParameterCount = (int) kotlinFunction.getParameters().stream().filter(KParameter::isOptional)
215-
.count();
255+
int optionalParameterCount = getOptionalParameterCount(constructorFunction);
216256
int syntheticParameters = KotlinDefaultMask.getExactMaskCount(optionalParameterCount);
217257

218-
if ((detectedConstructor.getParameterCount() + syntheticParameters) != candidate.getParameterCount()) {
258+
if ((detectedParameters.length + syntheticParameters) != candidate.getParameterCount()) {
219259
continue;
220260
}
221261
}
@@ -224,9 +264,8 @@ private static Constructor<?> doResolveKotlinConstructor(Constructor<?> detected
224264
continue;
225265
}
226266

227-
int userParameterCount = kotlinFunction != null ? kotlinFunction.getParameters().size()
228-
: detectedConstructor.getParameterCount();
229-
if (parametersMatch(detectedConstructorParameters, candidateParameters, userParameterCount)) {
267+
int userParameterCount = constructorFunction.getParameters().size();
268+
if (parametersMatch(detectedParameters, candidateParameters, userParameterCount)) {
230269
hit = candidate;
231270
}
232271
}
@@ -238,24 +277,48 @@ private static Constructor<?> doResolveKotlinConstructor(Constructor<?> detected
238277
return hit;
239278
}
240279

280+
private static int getOptionalParameterCount(KFunction<?> function) {
281+
282+
int count = 0;
283+
284+
for (KParameter parameter : function.getParameters()) {
285+
if (parameter.isOptional()) {
286+
count++;
287+
}
288+
}
289+
290+
return count;
291+
}
292+
241293
private static boolean parametersMatch(java.lang.reflect.Parameter[] constructorParameters,
242294
java.lang.reflect.Parameter[] candidateParameters, int userParameterCount) {
243295

244-
return IntStream.range(0, userParameterCount)
245-
.allMatch(i -> parametersMatch(constructorParameters[i], candidateParameters[i]));
296+
for (int i = 0; i < userParameterCount; i++) {
297+
if (!parametersMatch(constructorParameters[i].getType(), candidateParameters[i].getType())) {
298+
return false;
299+
}
300+
}
301+
return true;
246302
}
247303

248-
static boolean parametersMatch(java.lang.reflect.Parameter constructorParameter,
249-
java.lang.reflect.Parameter candidateParameter) {
304+
private static boolean parametersMatch(Class<?> constructorParameter, Class<?> candidateParameter) {
250305

251-
if (constructorParameter.getType().equals(candidateParameter.getType())) {
306+
if (constructorParameter.equals(candidateParameter)) {
252307
return true;
253308
}
254309

255310
// candidate can be also a wrapper
256-
Class<?> componentOrWrapperType = KotlinValueUtils.getConstructorValueHierarchy(candidateParameter.getType())
257-
.getActualType();
311+
Class<?> componentOrWrapperType = KotlinValueUtils.getConstructorValueHierarchy(candidateParameter).getActualType();
312+
313+
return constructorParameter.equals(componentOrWrapperType);
314+
}
315+
316+
private static boolean hasDefaultConstructorMarker(java.lang.reflect.Parameter[] parameters) {
317+
318+
return parameters.length > 0 && isDefaultConstructorMarker(parameters[parameters.length - 1].getType());
319+
}
258320

259-
return constructorParameter.getType().equals(componentOrWrapperType);
321+
private static boolean isDefaultConstructorMarker(Class<?> cls) {
322+
return cls.getName().equals("kotlin.jvm.internal.DefaultConstructorMarker");
260323
}
261324
}

0 commit comments

Comments
 (0)