Specialize Kotlin property accessors in KotlinBeanInfoFactory.

We now attempt to detect property accessors for properties declared in Kotlin that do not have a Kotlin-style accessor but one that instead comes from an interface.

Also, we specialize accessor methods that are inherited from a Java superclass but override accessors in the Kotlin realm.

Closes #3140
Closes #3146
This commit is contained in:
Mark Paluch
2024-09-02 14:36:05 +02:00
parent 3e75581336
commit b99db0cf01
3 changed files with 111 additions and 10 deletions

View File

@@ -29,15 +29,19 @@ import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.BeanInfoFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.KotlinDetector;
import org.springframework.core.Ordered;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link BeanInfoFactory} specific to Kotlin types using Kotlin reflection to determine bean properties.
@@ -62,7 +66,7 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered {
KClass<?> kotlinClass = JvmClassMappingKt.getKotlinClass(beanClass);
Collection<KCallable<?>> members = kotlinClass.getMembers();
Set<PropertyDescriptor> pds = new LinkedHashSet<>(members.size());
Map<String, PropertyDescriptor> descriptors = new LinkedHashMap<>(members.size(), 1.f);
for (KCallable<?> member : members) {
@@ -71,6 +75,16 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered {
Method getter = ReflectJvmMapping.getJavaGetter(property);
Method setter = property instanceof KMutableProperty<?> kmp ? ReflectJvmMapping.getJavaSetter(kmp) : null;
if (getter == null) {
Type javaType = ReflectJvmMapping.getJavaType(property.getReturnType());
getter = ReflectionUtils.findMethod(beanClass,
javaType == Boolean.TYPE ? "is" : "get" + StringUtils.capitalize(property.getName()));
}
if (getter != null) {
getter = ClassUtils.getMostSpecificMethod(getter, beanClass);
}
if (getter != null && (Modifier.isStatic(getter.getModifiers()) || getter.getParameterCount() != 0)) {
continue;
}
@@ -82,7 +96,7 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered {
}
}
pds.add(new PropertyDescriptor(property.getName(), getter, setter));
descriptors.put(property.getName(), new PropertyDescriptor(property.getName(), getter, setter));
}
}
@@ -95,9 +109,17 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered {
if (javaClass != Object.class) {
PropertyDescriptor[] javaPropertyDescriptors = BeanUtils.getPropertyDescriptors(javaClass);
pds.addAll(Arrays.asList(javaPropertyDescriptors));
for (PropertyDescriptor descriptor : javaPropertyDescriptors) {
descriptor = new PropertyDescriptor(descriptor.getName(), specialize(beanClass, descriptor.getReadMethod()),
specialize(beanClass, descriptor.getWriteMethod()));
descriptors.put(descriptor.getName(), descriptor);
}
}
PropertyDescriptor[] propertyDescriptors = descriptors.values().toArray(new PropertyDescriptor[0]);
return new SimpleBeanInfo() {
@Override
public BeanDescriptor getBeanDescriptor() {
@@ -106,11 +128,21 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered {
@Override
public PropertyDescriptor[] getPropertyDescriptors() {
return pds.toArray(new PropertyDescriptor[0]);
return propertyDescriptors;
}
};
}
@Nullable
private static Method specialize(Class<?> beanClass, @Nullable Method method) {
if (method == null) {
return method;
}
return ClassUtils.getMostSpecificMethod(method, beanClass);
}
@Override
public int getOrder() {
return LOWEST_PRECEDENCE - 10; // leave some space for customizations.

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.util;
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.util
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.BeanUtils
import org.springframework.data.annotation.Id
import org.springframework.data.domain.Persistable
import org.springframework.data.repository.Repository
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport
import org.springframework.data.repository.core.support.RepositoryFactorySupport
@@ -95,9 +97,11 @@ class KotlinBeanInfoFactoryUnitTests {
@Test // GH-2994
internal fun includesPropertiesFromJavaSupertypes() {
val pds = BeanUtils.getPropertyDescriptors(MyRepositoryFactoryBeanImpl::class.java)
val pds =
BeanUtils.getPropertyDescriptors(MyRepositoryFactoryBeanImpl::class.java)
assertThat(pds).extracting("name").contains("myQueryLookupStrategyKey", "repositoryBaseClass")
assertThat(pds).extracting("name")
.contains("myQueryLookupStrategyKey", "repositoryBaseClass")
}
@Test // GH-2993
@@ -110,6 +114,22 @@ class KotlinBeanInfoFactoryUnitTests {
assertThat(pds[0].readMethod).isNotNull()
}
@Test // GH-3140
internal fun specializesBeanMethods() {
var pds = BeanUtils.getPropertyDescriptors(Entity::class.java)
assertThat(pds.find { it.name == "id" }!!.readMethod!!.declaringClass).isEqualTo(
Entity::class.java
)
pds = BeanUtils.getPropertyDescriptors(DogEntity::class.java)
assertThat(pds.find { it.name == "name" }!!.readMethod!!.declaringClass).isEqualTo(
DogEntity::class.java
)
}
data class SimpleDataClass(val id: String, var name: String)
@JvmInline
@@ -127,7 +147,8 @@ class KotlinBeanInfoFactoryUnitTests {
Foo, Bar
}
class MyRepositoryFactoryBeanImpl<R, E, I>(repository: Class<R>) : RepositoryFactoryBeanSupport<R, E, I>(repository)
class MyRepositoryFactoryBeanImpl<R, E, I>(repository: Class<R>) :
RepositoryFactoryBeanSupport<R, E, I>(repository)
where R : Repository<E, I>, E : Any, I : Any {
private var myQueryLookupStrategyKey: String
@@ -149,4 +170,23 @@ class KotlinBeanInfoFactoryUnitTests {
override var end: Long = -1L
protected set
}
class Entity(
private val id: Long? = null,
val name: String
) : Persistable<Long> {
override fun getId(): Long? = id
override fun isNew(): Boolean = id == null
}
open class DogEntity : Animal() {
@Id
override fun getName(): String {
return super.getName()
}
}
}