Skip to content

Commit af5acb6

Browse files
committed
Avoid pre-conversion attempt in case of overloaded write methods
Closes gh-32159 See gh-31872
1 parent 067638a commit af5acb6

File tree

4 files changed

+105
-21
lines changed

4 files changed

+105
-21
lines changed

spring-beans/src/main/java/org/springframework/beans/BeanUtils.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -607,6 +607,22 @@ public static Class<?> findPropertyType(String propertyName, @Nullable Class<?>.
607607
return Object.class;
608608
}
609609

610+
/**
611+
* Determine whether the specified property has a unique write method,
612+
* i.e. is writable but does not declare overloaded setter methods.
613+
* @param pd the PropertyDescriptor for the property
614+
* @return {@code true} if writable and unique, {@code false} otherwise
615+
* @since 6.1.4
616+
*/
617+
public static boolean hasUniqueWriteMethod(PropertyDescriptor pd) {
618+
if (pd instanceof GenericTypeAwarePropertyDescriptor gpd) {
619+
return gpd.hasUniqueWriteMethod();
620+
}
621+
else {
622+
return (pd.getWriteMethod() != null);
623+
}
624+
}
625+
610626
/**
611627
* Obtain a new MethodParameter object for the write method of the
612628
* specified property.

spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -171,6 +171,10 @@ public Method getWriteMethodFallback(@Nullable Class<?> valueType) {
171171
return null;
172172
}
173173

174+
public boolean hasUniqueWriteMethod() {
175+
return (this.writeMethod != null && this.ambiguousWriteMethods == null);
176+
}
177+
174178
public MethodParameter getWriteMethodParameter() {
175179
Assert.state(this.writeMethodParameter != null, "No write method available");
176180
return this.writeMethodParameter;

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@
3939
import org.springframework.beans.BeanWrapper;
4040
import org.springframework.beans.BeanWrapperImpl;
4141
import org.springframework.beans.BeansException;
42+
import org.springframework.beans.InvalidPropertyException;
4243
import org.springframework.beans.MutablePropertyValues;
4344
import org.springframework.beans.PropertyAccessorUtils;
4445
import org.springframework.beans.PropertyValue;
@@ -1683,8 +1684,7 @@ protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrap
16831684
}
16841685
Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
16851686
Object convertedValue = resolvedValue;
1686-
boolean convertible = bw.isWritableProperty(propertyName) &&
1687-
!PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);
1687+
boolean convertible = isConvertibleProperty(propertyName, bw);
16881688
if (convertible) {
16891689
convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter);
16901690
}
@@ -1721,6 +1721,19 @@ else if (convertible && originalValue instanceof TypedStringValue typedStringVal
17211721
}
17221722
}
17231723

1724+
/**
1725+
* Determine whether the factory should cache a converted value for the given property.
1726+
*/
1727+
private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) {
1728+
try {
1729+
return !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName) &&
1730+
BeanUtils.hasUniqueWriteMethod(bw.getPropertyDescriptor(propertyName));
1731+
}
1732+
catch (InvalidPropertyException ex) {
1733+
return false;
1734+
}
1735+
}
1736+
17241737
/**
17251738
* Convert the given value for the specified target property.
17261739
*/

spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.net.MalformedURLException;
2525
import java.text.NumberFormat;
2626
import java.text.ParseException;
27+
import java.time.Duration;
2728
import java.util.ArrayList;
2829
import java.util.HashSet;
2930
import java.util.Iterator;
@@ -666,13 +667,13 @@ void possibleMatches() {
666667
bd.setPropertyValues(pvs);
667668
lbf.registerBeanDefinition("tb", bd);
668669

669-
assertThatExceptionOfType(BeanCreationException.class).as("invalid property").isThrownBy(() ->
670-
lbf.getBean("tb"))
671-
.withCauseInstanceOf(NotWritablePropertyException.class)
672-
.satisfies(ex -> {
673-
NotWritablePropertyException cause = (NotWritablePropertyException) ex.getCause();
674-
assertThat(cause.getPossibleMatches()).containsExactly("age");
675-
});
670+
assertThatExceptionOfType(BeanCreationException.class).as("invalid property")
671+
.isThrownBy(() -> lbf.getBean("tb"))
672+
.withCauseInstanceOf(NotWritablePropertyException.class)
673+
.satisfies(ex -> {
674+
NotWritablePropertyException cause = (NotWritablePropertyException) ex.getCause();
675+
assertThat(cause.getPossibleMatches()).containsExactly("age");
676+
});
676677
}
677678

678679
@Test
@@ -720,9 +721,9 @@ void prototypeCircleLeadsToException() {
720721
p.setProperty("rod.spouse", "*kerry");
721722
registerBeanDefinitions(p);
722723

723-
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() ->
724-
lbf.getBean("kerry"))
725-
.satisfies(ex -> assertThat(ex.contains(BeanCurrentlyInCreationException.class)).isTrue());
724+
assertThatExceptionOfType(BeanCreationException.class)
725+
.isThrownBy(() -> lbf.getBean("kerry"))
726+
.satisfies(ex -> assertThat(ex.contains(BeanCurrentlyInCreationException.class)).isTrue());
726727
}
727728

728729
@Test
@@ -903,16 +904,16 @@ void beanDefinitionOverridingNotAllowed() {
903904
lbf.registerBeanDefinition("test", oldDef);
904905
lbf.registerAlias("test", "testX");
905906

906-
assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() ->
907-
lbf.registerBeanDefinition("test", newDef))
907+
assertThatExceptionOfType(BeanDefinitionOverrideException.class)
908+
.isThrownBy(() -> lbf.registerBeanDefinition("test", newDef))
908909
.satisfies(ex -> {
909910
assertThat(ex.getBeanName()).isEqualTo("test");
910911
assertThat(ex.getBeanDefinition()).isEqualTo(newDef);
911912
assertThat(ex.getExistingDefinition()).isEqualTo(oldDef);
912913
});
913914

914-
assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() ->
915-
lbf.registerBeanDefinition("testX", newDef))
915+
assertThatExceptionOfType(BeanDefinitionOverrideException.class)
916+
.isThrownBy(() -> lbf.registerBeanDefinition("testX", newDef))
916917
.satisfies(ex -> {
917918
assertThat(ex.getBeanName()).isEqualTo("testX");
918919
assertThat(ex.getBeanDefinition()).isEqualTo(newDef);
@@ -1320,6 +1321,29 @@ void expressionInStringArray() {
13201321
assertThat(properties.getProperty("foo")).isEqualTo("bar");
13211322
}
13221323

1324+
@Test
1325+
void withOverloadedSetters() {
1326+
RootBeanDefinition rbd = new RootBeanDefinition(SetterOverload.class);
1327+
rbd.getPropertyValues().add("object", "a String");
1328+
lbf.registerBeanDefinition("overloaded", rbd);
1329+
assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("a String");
1330+
1331+
rbd = new RootBeanDefinition(SetterOverload.class);
1332+
rbd.getPropertyValues().add("object", 1000);
1333+
lbf.registerBeanDefinition("overloaded", rbd);
1334+
assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000");
1335+
1336+
rbd = new RootBeanDefinition(SetterOverload.class);
1337+
rbd.getPropertyValues().add("value", 1000);
1338+
lbf.registerBeanDefinition("overloaded", rbd);
1339+
assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000i");
1340+
1341+
rbd = new RootBeanDefinition(SetterOverload.class);
1342+
rbd.getPropertyValues().add("value", Duration.ofSeconds(1000));
1343+
lbf.registerBeanDefinition("overloaded", rbd);
1344+
assertThat(lbf.getBean(SetterOverload.class).getObject()).isEqualTo("1000s");
1345+
}
1346+
13231347
@Test
13241348
void autowireWithNoDependencies() {
13251349
RootBeanDefinition bd = new RootBeanDefinition(TestBean.class);
@@ -3219,6 +3243,7 @@ public Class<?> getObjectType() {
32193243
}
32203244
}
32213245

3246+
32223247
public record City(String name) {}
32233248

32243249
public static class CityRepository implements Repository<City, Long> {}
@@ -3328,6 +3353,32 @@ public Resource[] getResourceArray() {
33283353
}
33293354

33303355

3356+
public static class SetterOverload {
3357+
3358+
public String value;
3359+
3360+
public void setObject(Integer length) {
3361+
this.value = length + "i";
3362+
}
3363+
3364+
public void setObject(String object) {
3365+
this.value = object;
3366+
}
3367+
3368+
public String getObject() {
3369+
return this.value;
3370+
}
3371+
3372+
public void setValue(int length) {
3373+
this.value = length + "i";
3374+
}
3375+
3376+
public void setValue(Duration duration) {
3377+
this.value = duration.getSeconds() + "s";
3378+
}
3379+
}
3380+
3381+
33313382
/**
33323383
* Bean with a dependency on a {@link FactoryBean}.
33333384
*/
@@ -3475,6 +3526,7 @@ public NonPublicEnum getNonPublicEnum() {
34753526
}
34763527
}
34773528

3529+
34783530
@Order
34793531
private static class LowestPrecedenceTestBeanFactoryBean implements FactoryBean<TestBean> {
34803532

@@ -3487,9 +3539,9 @@ public TestBean getObject() {
34873539
public Class<?> getObjectType() {
34883540
return TestBean.class;
34893541
}
3490-
34913542
}
34923543

3544+
34933545
@Order(Ordered.HIGHEST_PRECEDENCE)
34943546
private static class HighestPrecedenceTestBeanFactoryBean implements FactoryBean<TestBean> {
34953547

@@ -3502,7 +3554,6 @@ public TestBean getObject() {
35023554
public Class<?> getObjectType() {
35033555
return TestBean.class;
35043556
}
3505-
35063557
}
35073558

35083559
}

0 commit comments

Comments
 (0)