spring-projects / spring-data-commons

Spring Data Commons. Interfaces and code shared between the various datastore specific implementations.
https://spring.io/projects/spring-data
Apache License 2.0
783 stars 676 forks source link

Generated Kotlin PropertyAccessor uses wrong copy method [DATACMNS-1391] #1827

Closed spring-projects-issues closed 6 years ago

spring-projects-issues commented 6 years ago

Marian Jureczko opened DATACMNS-1391 and commented

Given (in kotlin):

@Document(collection = "catalogue.CatalogueTags")
data class CatalogueTags internal constructor(
        @Id val id: ObjectId = ObjectId.get(),
        @Version val version: Int? = null,
        val shop: Shop,
...
       val activeInCatalogue: DateTimeRange = DateTimeRange()
) : CatalogueData<CatalogueTags>() {
    override fun copy(newActiveInCatalogue: DateTimeRange) = copy(activeInCatalogue = newActiveInCatalogue)
}

@Repository
interface CatalogueTagsRepository : RxJava2CrudRepository<CatalogueTags, ObjectId>  

When:

class MyTest : CatalogueBaseComponentTest() {

    @Autowired
    lateinit var repo: CatalogueTagsRepository

    @Test
    fun save() {
        val some = some<CatalogueTags>()
        val saved = repo.save(some).blockingGet()
    }
} 

Then:

java.lang.ClassCastException: java.lang.Integer cannot be cast to com.ocado.gembus.hub.commons.vos.DateTimeRange

at com.ocado.gembus.hub.catalogue.test.CatalogueTags_Accessor_jcjqjz.setProperty(Unknown Source)
at org.springframework.data.mapping.model.ConvertingPropertyAccessor.setProperty(ConvertingPropertyAccessor.java:61)
at org.springframework.data.mongodb.core.EntityOperations$AdaptibleMappedEntity.incrementVersion(EntityOperations.java:650)
at org.springframework.data.mongodb.core.ReactiveMongoTemplate.lambda$doSaveVersioned$33(ReactiveMongoTemplate.java:1407)
at org.springframework.data.mongodb.core.ReactiveMongoTemplate.lambda$createMono$8(ReactiveMongoTemplate.java:599)
at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:141)
at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:53)
at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.java:44)
at reactor.core.publisher.Mono.subscribe(Mono.java:3080)
at io.reactivex.internal.operators.flowable.FlowableFromPublisher.subscribeActual(FlowableFromPublisher.java:29)
at io.reactivex.Flowable.subscribe(Flowable.java:12995)
at io.reactivex.Flowable.subscribe(Flowable.java:12941)
at io.reactivex.internal.operators.observable.ObservableFromPublisher.subscribeActual(ObservableFromPublisher.java:31)
at io.reactivex.Observable.subscribe(Observable.java:10903)
at io.reactivex.internal.operators.observable.ObservableSingleMaybe.subscribeActual(ObservableSingleMaybe.java:30)
at io.reactivex.Maybe.subscribe(Maybe.java:3730)
at io.reactivex.internal.operators.maybe.MaybeToSingle.subscribeActual(MaybeToSingle.java:46)
at io.reactivex.Single.subscribe(Single.java:2700)
at io.reactivex.Single.blockingGet(Single.java:2153)
at com.ocado.gembus.hub.catalogue.test.MyTest.save(CatalogueTags.kt:54)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)

Affects: 2.1 RC2 (Lovelace)

Attachments:

Referenced from: pull request https://github.com/spring-projects/spring-data-commons/pull/312

1 votes, 4 watchers

spring-projects-issues commented 6 years ago

Oliver Drotbohm commented

Looks like we need to be more defensive about the copy(…) method we select and make sure it actually takes all parameters we need to hand it over, right?

spring-projects-issues commented 6 years ago

Marian Jureczko commented

I am not sure what exactly was going on there. When decompiling CatalogueTags_Accessor_jcjqjz.setProperty(), there was nothing wrong, i.e. the correct copy method was called. But, with a black box approach, when there is an additional copy method with different arguments (DateTimeRange in my case), the additional one is called in place of the data class default one (accepting all class fields) when trying to set version of a new entity to 0. At least that is the only explanation I could come up with to justify the unsuccessful Int to DateTimeRange casting

spring-projects-issues commented 6 years ago

Mark Paluch commented

The culprit here is an optimization that we made for single-property classes (value types). We attempt to find a public copy method that takes a single argument and returns the entity type so it does not require setup of defaulting masks.

We should guard the optimization with a check on the parameter type and make sure the entity consists of a single property to use the public copy optimization

spring-projects-issues commented 6 years ago

Mark Paluch commented

I moved this ticket to Spring Data Commons as the fix will happen here

spring-projects-issues commented 6 years ago

Mark Paluch commented

I pushed a change to a bugfix branch and we have a snapshot build available

It would be cool if you could give the snapshots a try (use version 2.1.0.DATACMNS-1391-SNAPSHOT) and see if this behaves as expected. 

spring-projects-issues commented 6 years ago

Marcin Sokołowski commented

Another problem occurred, I've prepared some minimal working example here: https://github.com/sokolek/DATACMNS-1391

Method

org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory#findPublicCopyMethod:1466

sometimes picks copy method from the data class , and sometimes overloaded copy method, because type.getDeclaredMethods()) returns them in random order. Result of findPublicCopyMethod method is then indirectly assigned in ClassGeneratingPropertyAccessorFactory.visitKotlinCopy:1102 to KotlinDefaultCopyMethod defaultMethod, but unfortunately this could be overloaded version of copy, which is not copy$default. !screenshot-dataclass.png|width=1525,height=1017!

In result following Exception is thrown:


java.lang.RuntimeException: java.lang.IllegalArgumentException: Cannot resolve parameter name id to a index in method copy!

    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory.createAccessorClass(ClassGeneratingPropertyAccessorFactory.java:206)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory.potentiallyCreateAndRegisterPersistentPropertyAccessorClass(ClassGeneratingPropertyAccessorFactory.java:190)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory.getPropertyAccessor(ClassGeneratingPropertyAccessorFactory.java:100)
    at org.springframework.data.mapping.model.BasicPersistentEntity.getPropertyAccessor(BasicPersistentEntity.java:455)
    at org.springframework.data.mapping.model.PersistentEntityIsNewStrategy.lambda$new$0(PersistentEntityIsNewStrategy.java:48)
    at org.springframework.data.mapping.model.PersistentEntityIsNewStrategy.isNew(PersistentEntityIsNewStrategy.java:95)
    at org.springframework.data.mapping.model.BasicPersistentEntity.isNew(BasicPersistentEntity.java:483)
    at org.springframework.data.repository.core.support.PersistentEntityInformation.isNew(PersistentEntityInformation.java:44)
    at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:80)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:359)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:200)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:644)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:608)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.lambda$invoke$3(RepositoryFactorySupport.java:595)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:595)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.data.repository.core.support.MethodInvocationValidator.invoke(MethodInvocationValidator.java:99)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy94.save(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:338)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:197)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy94.save(Unknown Source)
    at com.mytest.MyTest.send new dtc product(MyTest.kt:39)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.IllegalArgumentException: Cannot resolve parameter name id to a index in method copy!
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$KotlinCopyByProperty.findIndex(ClassGeneratingPropertyAccessorFactory.java:1562)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$KotlinCopyByProperty.<init>(ClassGeneratingPropertyAccessorFactory.java:1549)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$KotlinDefaultCopyMethod.forProperty(ClassGeneratingPropertyAccessorFactory.java:1531)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$PropertyAccessorClassGenerator.visitKotlinCopy(ClassGeneratingPropertyAccessorFactory.java:1103)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$PropertyAccessorClassGenerator.visitSetProperty0(ClassGeneratingPropertyAccessorFactory.java:1014)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$PropertyAccessorClassGenerator.visitSetPropertySwitch(ClassGeneratingPropertyAccessorFactory.java:986)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$PropertyAccessorClassGenerator.visitSetProperty(ClassGeneratingPropertyAccessorFactory.java:936)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$PropertyAccessorClassGenerator.generateBytecode(ClassGeneratingPropertyAccessorFactory.java:350)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory$PropertyAccessorClassGenerator.generateCustomAccessorClass(ClassGeneratingPropertyAccessorFactory.java:326)
    at org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory.createAccessorClass(ClassGeneratingPropertyAccessorFactory.java:204)
    ... 73 more
spring-projects-issues commented 6 years ago

Mark Paluch commented

Thanks a lot. I'll have a look

spring-projects-issues commented 6 years ago

Mark Paluch commented

I pushed another change to fix the issue. Care to give it a spin? (2.1.0.DATACMNS-1391-SNAPSHOT resolving to 2.1.0.DATACMNS-1391-20180914.084050-5)

spring-projects-issues commented 6 years ago

Marcin Sokołowski commented

Thanks for quick response. It works for the current setup, but it won't if the name of the arguments in overloaded copy method will be the same as the actual property names. See current master on https://github.com/sokolek/DATACMNS-1391/

The problem is caused by the logic in filtering at org.springframework.data.mapping.model.KotlinCopyMethod#findPublicCopyMethod:149-176. I think that the method should not rely how the parameters are named, but rather on their order, types and the total number of parameters. I guess that the intention there is just to find copy method generated by Kotlin (not the static copy$default one, and not the any overloaded copy from upper class), is my understanding correct?

Let me ask just one more question, why not just always use static copy$default method? I think you don't want to rely on the implementation details of copy methods by kotlin (as fas as I know, copy$default isn't documented anywhere), is that true?

spring-projects-issues commented 6 years ago

Mark Paluch commented

We have different approaches and cases where we need to distinguish between the public and the synthetic methods. Using reflection, we're using Kotlin's reflection API to invoke methods and to let Kotlin reflection apply defaulting. To do so, we need to lookup a KCallable. This is only possible by using the public generated copy method. Invoking ReflectJvmMapping.getKotlinFunction(…) with the copy$default method isn't able to resolve to a KCallable but returns null. Kotlin reflection is only able to resolve regular methods to KCallable and cannot use the synthetic ones.

So we need to find the generated copy method. There's next to no documentation on how to achieve that so we're still investigating. It looks as if the copy method has the same signature as the primary constructor and we should add another validation level.

spring-projects-issues commented 6 years ago

Mark Paluch commented

We aligned the copy method discovery to use the primary constructor signature. A new build is available

spring-projects-issues commented 6 years ago

Marcin Sokołowski commented

Now it works like a charm

spring-projects-issues commented 6 years ago

Mark Paluch commented

Thanks a lot for verifying the patch. Creating a pull request so we can include the change in our upcoming release