Skip to content

Commit b99db0c

Browse files
committed
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
1 parent 3e75581 commit b99db0c

File tree

3 files changed

+111
-10
lines changed

3 files changed

+111
-10
lines changed

src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java

+39-7
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,19 @@
2929
import java.beans.SimpleBeanInfo;
3030
import java.lang.reflect.Method;
3131
import java.lang.reflect.Modifier;
32-
import java.util.Arrays;
32+
import java.lang.reflect.Type;
3333
import java.util.Collection;
34-
import java.util.LinkedHashSet;
35-
import java.util.Set;
34+
import java.util.LinkedHashMap;
35+
import java.util.Map;
3636

3737
import org.springframework.beans.BeanInfoFactory;
3838
import org.springframework.beans.BeanUtils;
3939
import org.springframework.core.KotlinDetector;
4040
import org.springframework.core.Ordered;
41+
import org.springframework.lang.Nullable;
42+
import org.springframework.util.ClassUtils;
43+
import org.springframework.util.ReflectionUtils;
44+
import org.springframework.util.StringUtils;
4145

4246
/**
4347
* {@link BeanInfoFactory} specific to Kotlin types using Kotlin reflection to determine bean properties.
@@ -62,7 +66,7 @@ public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
6266

6367
KClass<?> kotlinClass = JvmClassMappingKt.getKotlinClass(beanClass);
6468
Collection<KCallable<?>> members = kotlinClass.getMembers();
65-
Set<PropertyDescriptor> pds = new LinkedHashSet<>(members.size());
69+
Map<String, PropertyDescriptor> descriptors = new LinkedHashMap<>(members.size(), 1.f);
6670

6771
for (KCallable<?> member : members) {
6872

@@ -71,6 +75,16 @@ public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
7175
Method getter = ReflectJvmMapping.getJavaGetter(property);
7276
Method setter = property instanceof KMutableProperty<?> kmp ? ReflectJvmMapping.getJavaSetter(kmp) : null;
7377

78+
if (getter == null) {
79+
Type javaType = ReflectJvmMapping.getJavaType(property.getReturnType());
80+
getter = ReflectionUtils.findMethod(beanClass,
81+
javaType == Boolean.TYPE ? "is" : "get" + StringUtils.capitalize(property.getName()));
82+
}
83+
84+
if (getter != null) {
85+
getter = ClassUtils.getMostSpecificMethod(getter, beanClass);
86+
}
87+
7488
if (getter != null && (Modifier.isStatic(getter.getModifiers()) || getter.getParameterCount() != 0)) {
7589
continue;
7690
}
@@ -82,7 +96,7 @@ public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
8296
}
8397
}
8498

85-
pds.add(new PropertyDescriptor(property.getName(), getter, setter));
99+
descriptors.put(property.getName(), new PropertyDescriptor(property.getName(), getter, setter));
86100
}
87101
}
88102

@@ -95,9 +109,17 @@ public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
95109
if (javaClass != Object.class) {
96110

97111
PropertyDescriptor[] javaPropertyDescriptors = BeanUtils.getPropertyDescriptors(javaClass);
98-
pds.addAll(Arrays.asList(javaPropertyDescriptors));
112+
113+
for (PropertyDescriptor descriptor : javaPropertyDescriptors) {
114+
115+
descriptor = new PropertyDescriptor(descriptor.getName(), specialize(beanClass, descriptor.getReadMethod()),
116+
specialize(beanClass, descriptor.getWriteMethod()));
117+
descriptors.put(descriptor.getName(), descriptor);
118+
}
99119
}
100120

121+
PropertyDescriptor[] propertyDescriptors = descriptors.values().toArray(new PropertyDescriptor[0]);
122+
101123
return new SimpleBeanInfo() {
102124
@Override
103125
public BeanDescriptor getBeanDescriptor() {
@@ -106,11 +128,21 @@ public BeanDescriptor getBeanDescriptor() {
106128

107129
@Override
108130
public PropertyDescriptor[] getPropertyDescriptors() {
109-
return pds.toArray(new PropertyDescriptor[0]);
131+
return propertyDescriptors;
110132
}
111133
};
112134
}
113135

136+
@Nullable
137+
private static Method specialize(Class<?> beanClass, @Nullable Method method) {
138+
139+
if (method == null) {
140+
return method;
141+
}
142+
143+
return ClassUtils.getMostSpecificMethod(method, beanClass);
144+
}
145+
114146
@Override
115147
public int getOrder() {
116148
return LOWEST_PRECEDENCE - 10; // leave some space for customizations.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.util;
17+
18+
public class Animal {
19+
20+
private String name;
21+
22+
public String getName() {
23+
return name;
24+
}
25+
26+
public void setName(String name) {
27+
this.name = name;
28+
}
29+
}

src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt

+43-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package org.springframework.data.util
1818
import org.assertj.core.api.Assertions.assertThat
1919
import org.junit.jupiter.api.Test
2020
import org.springframework.beans.BeanUtils
21+
import org.springframework.data.annotation.Id
22+
import org.springframework.data.domain.Persistable
2123
import org.springframework.data.repository.Repository
2224
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport
2325
import org.springframework.data.repository.core.support.RepositoryFactorySupport
@@ -95,9 +97,11 @@ class KotlinBeanInfoFactoryUnitTests {
9597
@Test // GH-2994
9698
internal fun includesPropertiesFromJavaSupertypes() {
9799

98-
val pds = BeanUtils.getPropertyDescriptors(MyRepositoryFactoryBeanImpl::class.java)
100+
val pds =
101+
BeanUtils.getPropertyDescriptors(MyRepositoryFactoryBeanImpl::class.java)
99102

100-
assertThat(pds).extracting("name").contains("myQueryLookupStrategyKey", "repositoryBaseClass")
103+
assertThat(pds).extracting("name")
104+
.contains("myQueryLookupStrategyKey", "repositoryBaseClass")
101105
}
102106

103107
@Test // GH-2993
@@ -110,6 +114,22 @@ class KotlinBeanInfoFactoryUnitTests {
110114
assertThat(pds[0].readMethod).isNotNull()
111115
}
112116

117+
@Test // GH-3140
118+
internal fun specializesBeanMethods() {
119+
120+
var pds = BeanUtils.getPropertyDescriptors(Entity::class.java)
121+
122+
assertThat(pds.find { it.name == "id" }!!.readMethod!!.declaringClass).isEqualTo(
123+
Entity::class.java
124+
)
125+
126+
pds = BeanUtils.getPropertyDescriptors(DogEntity::class.java)
127+
128+
assertThat(pds.find { it.name == "name" }!!.readMethod!!.declaringClass).isEqualTo(
129+
DogEntity::class.java
130+
)
131+
}
132+
113133
data class SimpleDataClass(val id: String, var name: String)
114134

115135
@JvmInline
@@ -127,7 +147,8 @@ class KotlinBeanInfoFactoryUnitTests {
127147
Foo, Bar
128148
}
129149

130-
class MyRepositoryFactoryBeanImpl<R, E, I>(repository: Class<R>) : RepositoryFactoryBeanSupport<R, E, I>(repository)
150+
class MyRepositoryFactoryBeanImpl<R, E, I>(repository: Class<R>) :
151+
RepositoryFactoryBeanSupport<R, E, I>(repository)
131152
where R : Repository<E, I>, E : Any, I : Any {
132153

133154
private var myQueryLookupStrategyKey: String
@@ -149,4 +170,23 @@ class KotlinBeanInfoFactoryUnitTests {
149170
override var end: Long = -1L
150171
protected set
151172
}
173+
174+
class Entity(
175+
private val id: Long? = null,
176+
177+
val name: String
178+
) : Persistable<Long> {
179+
180+
override fun getId(): Long? = id
181+
182+
override fun isNew(): Boolean = id == null
183+
}
184+
185+
open class DogEntity : Animal() {
186+
187+
@Id
188+
override fun getName(): String {
189+
return super.getName()
190+
}
191+
}
152192
}

0 commit comments

Comments
 (0)