I'm trying to store a Kotlin's (v1.3.61) inline class to a MongoDB using Spring Data MongoDB (2.2.3-RELEASE), with no luck so far. This is the set up:
inline class UserId(@NotBlank val id: String)
and
@Document(collection = "data")
class Data(
@Field("uid")
val userId: UserId
)
Spring throws the following exception when creating its beans:
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1 at org.springframework.data.mapping.model.PreferredConstructorDiscoverer$Discoverers.buildPreferredConstructor(PreferredConstructorDiscoverer.java:221) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.model.PreferredConstructorDiscoverer$Discoverers.access$200(PreferredConstructorDiscoverer.java:89) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.model.PreferredConstructorDiscoverer$Discoverers$2.lambda$discover$0(PreferredConstructorDiscoverer.java:161) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) ~[na:1.8.0_171] at java.util.Spliterators$ArraySpliterator.tryAdvance(Spliterators.java:958) ~[na:1.8.0_171] at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126) ~[na:1.8.0_171] at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498) ~[na:1.8.0_171] at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485) ~[na:1.8.0_171] at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_171] at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152) ~[na:1.8.0_171] at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_171] at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464) ~[na:1.8.0_171] at org.springframework.data.mapping.model.PreferredConstructorDiscoverer$Discoverers$2.discover(PreferredConstructorDiscoverer.java:164) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.model.PreferredConstructorDiscoverer.discover(PreferredConstructorDiscoverer.java:77) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.model.BasicPersistentEntity.(BasicPersistentEntity.java:105) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity.(BasicMongoPersistentEntity.java:74) ~[spring-data-mongodb-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mongodb.core.mapping.MongoMappingContext.createPersistentEntity(MongoMappingContext.java:91) ~[spring-data-mongodb-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mongodb.core.mapping.MongoMappingContext.createPersistentEntity(MongoMappingContext.java:39) ~[spring-data-mongodb-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:357) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:323) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_171] at org.springframework.data.mapping.context.AbstractMappingContext.initialize(AbstractMappingContext.java:452) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.data.mapping.context.AbstractMappingContext.afterPropertiesSet(AbstractMappingContext.java:444) ~[spring-data-commons-2.2.3.RELEASE.jar:2.2.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1855) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1792) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE] ... 151 common frames omitted
The stacktrace is quite obscure, but the exception occurs in the PreferredConstructorDiscoverer
and the userId
is part of the constructor, so the issue might be located there.
Next thing was checking the byte code of the Data
class:
// ================net/test/Data.class
================= // class version 50.0 (50) // access flags 0x31
public final class net/test/Data {
@Lorg/springframework/data/mongodb/core/mapping/Document;(collection="data")
// access flags 0x12 private final Ljava/lang/String; userId @Lorg/springframework/data/mongodb/core/mapping/Field;(value="uid") @Lorg/jetbrains/annotations/NotNull;() // invisible
// access flags 0x11 public final getUserId()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0
LINENUMBER 10 L0
ALOAD 0
GETFIELD net/test/Data.userId : Ljava/lang/String;
ARETURN L1
LOCALVARIABLE this Lnet/test/Data; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x2 private <init>(Ljava/lang/String;)V L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
ALOAD 0
ALOAD 1
PUTFIELD net/test/Data.userId : Ljava/lang/String;
RETURN L1
LOCALVARIABLE this Lnet/test/Data; L0 L1 0
LOCALVARIABLE userId Ljava/lang/String; L0 L1 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1001 public synthetic <init>(Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
// annotable parameter count: 2 (visible)
// annotable parameter count: 2 (invisible) L0
LINENUMBER 7 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL net/test/Data.<init> (Ljava/lang/String;)V
RETURN L1
LOCALVARIABLE this Lnet/test/Data; L0 L1 0
LOCALVARIABLE userId Ljava/lang/String; L0 L1 1
LOCALVARIABLE $constructor_marker Lkotlin/jvm/internal/DefaultConstructorMarker; L0 L1 2
MAXSTACK = 2
MAXLOCALS = 3
@Lkotlin/Metadata;(mv={1, 1, 16}, bv={1, 0, 3}, k=1, d1={"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\u0008\u0005\u0008\u0007\u0018\u00002\u00020\u0001B\u0010\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u00f8\u0001\u0000\u00a2\u0006\u0002\u0010\u0004R\u001b\u0010\u0002\u001a\u00020\u00038\u0006X\u0087\u0004\u00f8\u0001\u0000\u00a2\u0006\n\n\u0002\u0010\u0007\u001a\u0004\u0008\u0005\u0010\u0006\u0082\u0002\u0004\n\u0002\u0008\u0019\u00a8\u0006\u0008"}, d2={"Lnet/test/Data;", "", "userId", "Lnet/test/UserId;", "(Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V", "getUserId", "()Ljava/lang/String;", "Ljava/lang/String;", "core"}) // compiled from: Data.kt }
// ================META-INF/core.kotlin_module =================
and this is the relevant line 221 of the PreferredConstructorDiscoverer
from the Spring source:
String name = parameterNames == null ? null : parameterNames[i];
I'm a total noob in understand byte code, but one or the other might see the issue right away.
Something else I've tried was implementing two custom converters of type org.springframework.core.convert.converter.Converter<UserId,String>
and vice versa. However, the bean instantiation takes place before any conversion can occur, so this seems to be irrelevant.
I'm aware that inline classes are still experimental in Kotlin 1.3, but maybe the actual issue lies somewhere else. Does anybody have an idea?
By the way, when changing the type from UserId
to String
everything works fine.
I'm using a workaround for this, by introducing a private field.
But with this approach you should switch from extending CRUD-Interfaces to implement the basic operations by yourself (otherwise the ID-class makes no sense):
If its worth the overhead? Pro:
Cons:
If DATACMNS-1517 is fixed sometime in the future, adaption should be relatively easy (mostly the implementation of repositories is removed in favour to extended interfaces).