Skip to content

KotlinBeanInfoFactory fails on bean where setter is primitive type and getter is boxed #2993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
drewhk opened this issue Nov 29, 2023 · 10 comments
Assignees
Labels
type: bug A general bug

Comments

@drewhk
Copy link

drewhk commented Nov 29, 2023

I have a bean that worked before (spring-boot 3.1.3 and Kotlin 1.7.21), but does not work with 3.2.0 (and Kotlin 1.9.21).

The following code reproduces this issue (edited down to only show relevant parts):

typealias Timestamp = Long

interface Interval<T : Comparable<T>> : Comparable<Interval<T>> {
    val end: T
    // ...
}

abstract class MyEntity: Interval<Timestamp> {
  //...
  override var end: Timestamp = -1L
      protected set
  //...
}

The end property above compiles down to the following pair of getter and setter (taken from javap output):

  public java.lang.Long getEnd();
    descriptor: ()Ljava/lang/Long;

  protected void setEnd(long);
    descriptor: (J)V

So this ends up in KotlinBeanInfoFactory as a call to pds.add(new PropertyDescriptor(property.getName(), getter, setter));. At this point the getter is public java.lang.Long MyEntity.getEnd() and the setter is protected void MyEntity.setEnd(long). This call finally ends up in java.beans.PropertyDescriptor.findPropertyType@L678, where the test if (propertyType != null && !params[0].isAssignableFrom(propertyType)) fails with a IntrospectionException("type mismatch between read and write methods"). The reason is that isAssignableFrom fails on comparing long and java.lang.Long.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Nov 29, 2023
@drewhk
Copy link
Author

drewhk commented Nov 29, 2023

Nota bene, this is not a change introduced by the Kotlin upgrade. I only upgraded because one spring-boot dependency required it. The same signatures are generated by Kotlin 1.7.21 (I checked with javap), in fact, when I replaced the class file in question with one generated by the 1.7 Kotlin compiler I got the exact same issue. So something have changed in the bean info logic here...

@drewhk
Copy link
Author

drewhk commented Nov 29, 2023

Sorry, I forgot the original stacktrace, apologies 😊 :

Caused by: org.springframework.beans.FatalBeanException: Failed to obtain BeanInfo for class [REDACTED]
	at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:301) ~[spring-beans-6.1.1.jar:6.1.1]
	at org.springframework.beans.CachedIntrospectionResults.forClass(CachedIntrospectionResults.java:157) ~[spring-beans-6.1.1.jar:6.1.1]
	at org.springframework.beans.BeanUtils.getPropertyDescriptors(BeanUtils.java:490) ~[spring-beans-6.1.1.jar:6.1.1]
	at org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:414) ~[spring-data-commons-3.2.0.jar:3.2.0]
	at org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:379) ~[spring-data-commons-3.2.0.jar:3.2.0]
	... 265 common frames omitted
Caused by: java.beans.IntrospectionException: type mismatch between read and write methods
	at java.desktop/java.beans.PropertyDescriptor.findPropertyType(PropertyDescriptor.java:699) ~[na:na]
	at java.desktop/java.beans.PropertyDescriptor.setWriteMethod(PropertyDescriptor.java:356) ~[na:na]
	at java.desktop/java.beans.PropertyDescriptor.<init>(PropertyDescriptor.java:142) ~[na:na]
	at org.springframework.data.util.KotlinBeanInfoFactory.getBeanInfo(KotlinBeanInfoFactory.java:70) ~[spring-data-commons-3.2.0.jar:3.2.0]
	at org.springframework.beans.CachedIntrospectionResults.getBeanInfo(CachedIntrospectionResults.java:222) ~[spring-beans-6.1.1.jar:6.1.1]
	at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:248) ~[spring-beans-6.1.1.jar:6.1.1]
	... 269 common frames omitted

@mp911de
Copy link
Member

mp911de commented Nov 29, 2023

That is expected as Spring is based on the Java Beans spec. Getters and setters must use the same type.

We introduced the factory to reliably determine Kotlin properties as the various compilation and usage patterns in Kotlin would otherwise introduce a complexity that we wouldn't be able to handle.

@drewhk
Copy link
Author

drewhk commented Nov 29, 2023

Well...

First of all, this worked before, only Spring Boot 3.2.0 breaks it (this is a several years old project, this worked before, even with 3.1.3).
Second, in this case the user, who writes the code does NOT introduce any discrepancy in the types. A generic val that is instantiated with the type Timestamp is overridden in Kotlin to be a var now, but all types are Timestamp (see the code illustration I included in the description). We cannot in any way influence the type of the setter, it is generated by Kotlin - and it has been generated this way since 1.7.

@mp911de
Copy link
Member

mp911de commented Nov 29, 2023

The only alternatives we have is excluding these properties from property inspection or deciding which side of the property we want to add (the reading or the writing one).

Generally speaking, the issue is not Spring but Kotlin's way to do things that then do not comply with standards that predate Kotlin by 15 years.

Previously, the PropertyDescriptor from the interface has been utilized and it would make sense to go back to this state if getters and setters use different types.

@mp911de mp911de added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 29, 2023
@mp911de mp911de self-assigned this Nov 29, 2023
@mp911de mp911de added this to the 3.1.7 (2023.0.7) milestone Nov 29, 2023
@mp911de mp911de changed the title KotlinBeanInfoFactory fails on bean where setter is primitive type and getter is boxed. KotlinBeanInfoFactory fails on bean where setter is primitive type and getter is boxed Nov 29, 2023
@mp911de mp911de changed the title KotlinBeanInfoFactory fails on bean where setter is primitive type and getter is boxed KotlinBeanInfoFactory fails on bean where setter is primitive type and getter is boxed Nov 29, 2023
mp911de added a commit that referenced this issue Nov 29, 2023
We now skip adding asymmetric Kotlin properties if the getter returns a different type than the setter (e.g. due to value boxing).

Closes #2993
mp911de added a commit that referenced this issue Nov 29, 2023
We now skip adding asymmetric Kotlin properties if the getter returns a different type than the setter (e.g. due to value boxing).

Closes #2993
mp911de added a commit that referenced this issue Nov 29, 2023
We now skip adding asymmetric Kotlin properties if the getter returns a different type than the setter (e.g. due to value boxing).

Closes #2993
@mp911de
Copy link
Member

mp911de commented Nov 29, 2023

That's addressed now in 3.2.1-SNAPSHOT. Care to test your application against that build?

@drewhk
Copy link
Author

drewhk commented Nov 29, 2023

Thanks, I will try it tomorrow in the morning.

@drewhk
Copy link
Author

drewhk commented Nov 30, 2023

Hmm, my gradle build fails with Could not find org.springframework.boot:spring-boot-dependencies:3.2.1-SNAPSHOT.. I added the maven { url 'https://repo.spring.io/snapshot' } repo, but it does not seem to be enough.

@mp911de
Copy link
Member

mp911de commented Nov 30, 2023

You need to add an entry for org.springframework.data:spring-data-commons:3.2.1-SNAPSHOT instead of using the Boot dependencies as this is Spring Data.

@drewhk
Copy link
Author

drewhk commented Nov 30, 2023

Ok it does not fail anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants