Skip to content

Commit 80ca2b4

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for Kotlin value classes.
This commit introduces support for Kotlin Value Classes which are designed for a more expressive domain model to make underlying concepts explicit. Spring Data can now read and write types that define properties using Value Classes. The support covers reflection based instantiation of Kotlin inline class, nullability and defaulting permutations as well as value classes with generics. Closes: #1947 Original Pull Request: #2866
1 parent 15bb8aa commit 80ca2b4

24 files changed

+2133
-237
lines changed

pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<scala>2.11.7</scala>
3434
<xmlbeam>1.4.24</xmlbeam>
3535
<java-module-name>spring.data.commons</java-module-name>
36+
<kotlin.api.target>1.8</kotlin.api.target>
3637

3738
</properties>
3839

@@ -126,6 +127,12 @@
126127
<scope>test</scope>
127128
</dependency>
128129

130+
<dependency>
131+
<groupId>org.jetbrains.kotlin</groupId>
132+
<artifactId>kotlin-maven-plugin</artifactId>
133+
<version>${kotlin}</version>
134+
</dependency>
135+
129136
<!-- RxJava -->
130137

131138
<dependency>

src/main/asciidoc/object-mapping.adoc

+24
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,27 @@ You can exclude properties by annotating these with `@Transient`.
436436
2. How to represent properties in your data store?
437437
Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
438438
3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be set.
439+
440+
[[mapping.kotlin.value.classes]]
441+
=== Kotlin Value Classes
442+
443+
Kotlin Value Classes are designed for a more expressive domain model to make underlying concepts explicit.
444+
Spring Data can read and write types that define properties using Value Classes.
445+
446+
Consider the following domain model:
447+
448+
====
449+
[source,kotlin]
450+
----
451+
@JvmInline
452+
value class EmailAddress(val theAddress: String) <1>
453+
454+
data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) <2>
455+
----
456+
457+
<1> A simple value class with a non-nullable value type.
458+
<2> Data class defining a property using the `EmailAddress` value class.
459+
====
460+
461+
NOTE: Non-nullable properties using non-primitive value types are flattened in the compiled class to the value type.
462+
Nullable primitive value types or nullable value-in-value types are represented with their wrapper type and that affects how value types are represented in the database.

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ static <T> Object setProperty(PersistentProperty<?> property, T bean, @Nullable
168168
KCallable<?> copy = copyMethodCache.computeIfAbsent(type, it -> getCopyMethod(it, property));
169169

170170
if (copy == null) {
171-
throw new UnsupportedOperationException(String.format(
172-
"Kotlin class %s has no .copy(…) method for property %s", type.getName(), property.getName()));
171+
throw new UnsupportedOperationException(String.format("Kotlin class %s has no .copy(…) method for property %s",
172+
type.getName(), property.getName()));
173173
}
174174

175175
return copy.callBy(getCallArgs(copy, property, bean, value));
@@ -179,7 +179,6 @@ private static <T> Map<KParameter, Object> getCallArgs(KCallable<?> callable, Pe
179179
T bean, @Nullable Object value) {
180180

181181
Map<KParameter, Object> args = new LinkedHashMap<>(2, 1);
182-
183182
List<KParameter> parameters = callable.getParameters();
184183

185184
for (KParameter parameter : parameters) {
@@ -190,7 +189,8 @@ private static <T> Map<KParameter, Object> getCallArgs(KCallable<?> callable, Pe
190189

191190
if (parameter.getKind() == Kind.VALUE && parameter.getName() != null
192191
&& parameter.getName().equals(property.getName())) {
193-
args.put(parameter, value);
192+
193+
args.put(parameter, KotlinValueUtils.getCopyValueHierarchy(parameter).wrap(value));
194194
}
195195
}
196196
return args;

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

+86-2
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
import static org.springframework.asm.Opcodes.*;
1919
import static org.springframework.data.mapping.model.BytecodeUtil.*;
2020

21+
import kotlin.reflect.KParameter;
22+
import kotlin.reflect.KParameter.Kind;
23+
2124
import java.lang.reflect.Constructor;
2225
import java.lang.reflect.Field;
2326
import java.lang.reflect.Member;
2427
import java.lang.reflect.Method;
2528
import java.lang.reflect.Modifier;
29+
import java.lang.reflect.Parameter;
2630
import java.security.ProtectionDomain;
2731
import java.util.ArrayList;
2832
import java.util.Collections;
@@ -48,12 +52,16 @@
4852
import org.springframework.data.mapping.PersistentPropertyAccessor;
4953
import org.springframework.data.mapping.SimpleAssociationHandler;
5054
import org.springframework.data.mapping.SimplePropertyHandler;
55+
import org.springframework.data.mapping.model.KotlinCopyMethod.KotlinCopyByProperty;
56+
import org.springframework.data.mapping.model.KotlinValueUtils.ValueBoxing;
5157
import org.springframework.data.util.Optionals;
5258
import org.springframework.data.util.TypeInformation;
5359
import org.springframework.lang.Nullable;
5460
import org.springframework.util.Assert;
5561
import org.springframework.util.ClassUtils;
62+
import org.springframework.util.ConcurrentLruCache;
5663
import org.springframework.util.ReflectionUtils;
64+
import org.springframework.util.StringUtils;
5765

5866
/**
5967
* A factory that can generate byte code to speed-up dynamic property access. Uses the {@link PersistentEntity}'s
@@ -76,6 +84,9 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
7684
private volatile Map<TypeInformation<?>, Class<PersistentPropertyAccessor<?>>> propertyAccessorClasses = new HashMap<>(
7785
32);
7886

87+
private final ConcurrentLruCache<PersistentProperty<?>, Function<Object, Object>> wrapperCache = new ConcurrentLruCache<>(
88+
256, KotlinValueBoxingAdapter::getWrapper);
89+
7990
@Override
8091
public <T> PersistentPropertyAccessor<T> getPropertyAccessor(PersistentEntity<?, ?> entity, T bean) {
8192

@@ -96,7 +107,14 @@ public <T> PersistentPropertyAccessor<T> getPropertyAccessor(PersistentEntity<?,
96107
args[0] = bean;
97108

98109
try {
99-
return (PersistentPropertyAccessor<T>) constructor.newInstance(args);
110+
111+
PersistentPropertyAccessor<T> accessor = (PersistentPropertyAccessor<T>) constructor.newInstance(args);
112+
113+
if (KotlinDetector.isKotlinType(entity.getType())) {
114+
return new KotlinValueBoxingAdapter<>(entity, accessor, wrapperCache);
115+
}
116+
117+
return accessor;
100118
} catch (Exception e) {
101119
throw new IllegalArgumentException(String.format("Cannot create persistent property accessor for %s", entity), e);
102120
} finally {
@@ -1431,7 +1449,7 @@ static boolean supportsMutation(PersistentProperty<?> property) {
14311449
* Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy}
14321450
* method with parameter defaulting.
14331451
*
1434-
* @param type must not be {@literal null}.
1452+
* @param property must not be {@literal null}.
14351453
* @return
14361454
*/
14371455
private static boolean hasKotlinCopyMethod(PersistentProperty<?> property) {
@@ -1444,4 +1462,70 @@ private static boolean hasKotlinCopyMethod(PersistentProperty<?> property) {
14441462

14451463
return false;
14461464
}
1465+
1466+
/**
1467+
* Adapter to encapsulate Kotlin's value class boxing when properties are nullable.
1468+
*
1469+
* @param entity the entity that could use value class properties.
1470+
* @param delegate the property accessor to delegate to.
1471+
* @param wrapperCache cache for wrapping functions.
1472+
* @param <T>
1473+
* @since 3.2
1474+
*/
1475+
record KotlinValueBoxingAdapter<T> (PersistentEntity<?, ?> entity, PersistentPropertyAccessor<T> delegate,
1476+
ConcurrentLruCache<PersistentProperty<?>, Function<Object, Object>> wrapperCache)
1477+
implements
1478+
PersistentPropertyAccessor<T> {
1479+
1480+
@Override
1481+
public void setProperty(PersistentProperty<?> property, @Nullable Object value) {
1482+
delegate.setProperty(property, wrapperCache.get(property).apply(value));
1483+
}
1484+
1485+
/**
1486+
* Create a wrapper function if the {@link PersistentProperty} uses value classes.
1487+
*
1488+
* @param property the persistent property to inspect.
1489+
* @return a wrapper function to wrap a value class component into the hierarchy of value classes or
1490+
* {@link Function#identity()} if wrapping is not necessary.
1491+
* @see KotlinValueUtils#getCopyValueHierarchy(KParameter)
1492+
*/
1493+
static Function<Object, Object> getWrapper(PersistentProperty<?> property) {
1494+
1495+
Optional<KotlinCopyMethod> kotlinCopyMethod = KotlinCopyMethod.findCopyMethod(property.getOwner().getType())
1496+
.filter(it -> it.supportsProperty(property));
1497+
1498+
if (kotlinCopyMethod.isPresent()
1499+
&& kotlinCopyMethod.filter(it -> it.forProperty(property).isPresent()).isPresent()) {
1500+
KotlinCopyMethod copyMethod = kotlinCopyMethod.get();
1501+
1502+
Optional<KParameter> kParameter = kotlinCopyMethod.stream()
1503+
.flatMap(it -> it.getCopyFunction().getParameters().stream()) //
1504+
.filter(kf -> kf.getKind() == Kind.VALUE) //
1505+
.filter(kf -> StringUtils.hasText(kf.getName())) //
1506+
.filter(kf -> kf.getName().equals(property.getName())) //
1507+
.findFirst();
1508+
1509+
ValueBoxing vh = kParameter.map(KotlinValueUtils::getCopyValueHierarchy).orElse(null);
1510+
KotlinCopyByProperty kotlinCopyByProperty = copyMethod.forProperty(property).get();
1511+
Method copy = copyMethod.getSyntheticCopyMethod();
1512+
1513+
Parameter parameter = copy.getParameters()[kotlinCopyByProperty.getParameterPosition()];
1514+
1515+
return o -> ClassUtils.isAssignableValue(parameter.getType(), o) || vh == null ? o : vh.wrap(o);
1516+
}
1517+
1518+
return Function.identity();
1519+
}
1520+
1521+
@Override
1522+
public Object getProperty(PersistentProperty<?> property) {
1523+
return delegate.getProperty(property);
1524+
}
1525+
1526+
@Override
1527+
public T getBean() {
1528+
return delegate.getBean();
1529+
}
1530+
}
14471531
}

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

+36-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
*/
1616
package org.springframework.data.mapping.model;
1717

18+
import kotlin.jvm.JvmClassMappingKt;
19+
import kotlin.reflect.KCallable;
20+
import kotlin.reflect.KClass;
21+
import kotlin.reflect.KProperty;
22+
1823
import java.lang.annotation.Annotation;
1924
import java.lang.reflect.AnnotatedElement;
2025
import java.lang.reflect.Constructor;
@@ -24,6 +29,7 @@
2429
import java.util.List;
2530

2631
import org.springframework.core.DefaultParameterNameDiscoverer;
32+
import org.springframework.core.KotlinDetector;
2733
import org.springframework.core.ParameterNameDiscoverer;
2834
import org.springframework.core.annotation.MergedAnnotations;
2935
import org.springframework.data.annotation.PersistenceCreator;
@@ -55,7 +61,8 @@ class InstanceCreatorMetadataDiscoverer {
5561
* @return
5662
*/
5763
@Nullable
58-
public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> discover(PersistentEntity<T, P> entity) {
64+
public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> discover(
65+
PersistentEntity<T, P> entity) {
5966

6067
Constructor<?>[] declaredConstructors = entity.getType().getDeclaredConstructors();
6168
Method[] declaredMethods = entity.getType().getDeclaredMethods();
@@ -78,6 +85,34 @@ public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> di
7885
}
7986
}
8087

88+
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(entity.getType())) {
89+
90+
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(entity.getType());
91+
// We use box-impl as factory for classes.
92+
if (kClass.isValue()) {
93+
94+
String propertyName = "";
95+
for (KCallable<?> member : kClass.getMembers()) {
96+
if (member instanceof KProperty<?>) {
97+
propertyName = member.getName();
98+
break;
99+
}
100+
}
101+
102+
for (Method declaredMethod : entity.getType().getDeclaredMethods()) {
103+
if (declaredMethod.getName().equals("box-impl") && declaredMethod.isSynthetic()
104+
&& declaredMethod.getParameterCount() == 1) {
105+
106+
Annotation[][] parameterAnnotations = declaredMethod.getParameterAnnotations();
107+
List<TypeInformation<?>> types = entity.getTypeInformation().getParameterTypes(declaredMethod);
108+
109+
return new FactoryMethod<>(declaredMethod,
110+
new Parameter<>(propertyName, (TypeInformation) types.get(0), parameterAnnotations[0], entity));
111+
}
112+
}
113+
}
114+
}
115+
81116
return PreferredConstructorDiscoverer.discover(entity);
82117
}
83118

0 commit comments

Comments
 (0)