Skip to content

Commit f5397d6

Browse files
committed
Consistent processing of overridden bean methods
Closes gh-28286
1 parent 2a1b30d commit f5397d6

File tree

5 files changed

+103
-19
lines changed

5 files changed

+103
-19
lines changed

Diff for: spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java

+20-8
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.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.context.annotation;
1818

19+
import java.util.Map;
20+
1921
import org.springframework.beans.factory.parsing.Problem;
2022
import org.springframework.beans.factory.parsing.ProblemReporter;
2123
import org.springframework.core.type.MethodMetadata;
@@ -52,22 +54,24 @@ public void validate(ProblemReporter problemReporter) {
5254
return;
5355
}
5456

55-
if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) {
56-
if (!getMetadata().isOverridable()) {
57-
// instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB
58-
problemReporter.error(new NonOverridableMethodError());
59-
}
57+
Map<String, Object> attributes =
58+
getConfigurationClass().getMetadata().getAnnotationAttributes(Configuration.class.getName());
59+
if (attributes != null && (Boolean) attributes.get("proxyBeanMethods") && !getMetadata().isOverridable()) {
60+
// instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB
61+
problemReporter.error(new NonOverridableMethodError());
6062
}
6163
}
6264

6365
@Override
6466
public boolean equals(@Nullable Object other) {
65-
return (this == other || (other instanceof BeanMethod that && this.metadata.equals(that.metadata)));
67+
return (this == other || (other instanceof BeanMethod that &&
68+
this.configurationClass.equals(that.configurationClass) &&
69+
getLocalMethodIdentifier(this.metadata).equals(getLocalMethodIdentifier(that.metadata))));
6670
}
6771

6872
@Override
6973
public int hashCode() {
70-
return this.metadata.hashCode();
74+
return this.configurationClass.hashCode() * 31 + getLocalMethodIdentifier(this.metadata).hashCode();
7175
}
7276

7377
@Override
@@ -76,6 +80,14 @@ public String toString() {
7680
}
7781

7882

83+
private static String getLocalMethodIdentifier(MethodMetadata metadata) {
84+
String metadataString = metadata.toString();
85+
int index = metadataString.indexOf(metadata.getDeclaringClassName());
86+
return (index >= 0 ? metadataString.substring(index + metadata.getDeclaringClassName().length()) :
87+
metadataString);
88+
}
89+
90+
7991
private class VoidDeclaredMethodError extends Problem {
8092

8193
VoidDeclaredMethodError() {

Diff for: spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,12 @@ void validate(ProblemReporter problemReporter) {
223223
Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
224224

225225
// A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
226-
if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) {
227-
if (this.metadata.isFinal()) {
228-
problemReporter.error(new FinalConfigurationProblem());
229-
}
230-
for (BeanMethod beanMethod : this.beanMethods) {
231-
beanMethod.validate(problemReporter);
232-
}
226+
if (attributes != null && (Boolean) attributes.get("proxyBeanMethods") && this.metadata.isFinal()) {
227+
problemReporter.error(new FinalConfigurationProblem());
228+
}
229+
230+
for (BeanMethod beanMethod : this.beanMethods) {
231+
beanMethod.validate(problemReporter);
233232
}
234233

235234
// A configuration class may not contain overloaded bean methods unless it declares enforceUniqueMethods=false

Diff for: spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.springframework.core.type.StandardMethodMetadata;
5454
import org.springframework.lang.NonNull;
5555
import org.springframework.util.Assert;
56+
import org.springframework.util.ClassUtils;
5657
import org.springframework.util.StringUtils;
5758

5859
/**
@@ -229,8 +230,12 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
229230
beanDef.setUniqueFactoryMethodName(methodName);
230231
}
231232

232-
if (metadata instanceof StandardMethodMetadata sam) {
233-
beanDef.setResolvedFactoryMethod(sam.getIntrospectedMethod());
233+
if (metadata instanceof StandardMethodMetadata smm &&
234+
configClass.getMetadata() instanceof StandardAnnotationMetadata sam) {
235+
Method method = ClassUtils.getMostSpecificMethod(smm.getIntrospectedMethod(), sam.getIntrospectedClass());
236+
if (method == smm.getIntrospectedMethod()) {
237+
beanDef.setResolvedFactoryMethod(method);
238+
}
234239
}
235240

236241
beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);

Diff for: spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java

+56-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
/**
3131
* Tests regarding overloading and overriding of bean methods.
32+
*
3233
* <p>Related to SPR-6618.
3334
*
3435
* @author Chris Beams
@@ -41,6 +42,7 @@ public class BeanMethodPolymorphismTests {
4142
@Test
4243
void beanMethodDetectedOnSuperClass() {
4344
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
45+
4446
assertThat(ctx.getBean("testBean", BaseTestBean.class)).isNotNull();
4547
}
4648

@@ -50,6 +52,7 @@ void beanMethodOverriding() {
5052
ctx.register(OverridingConfig.class);
5153
ctx.setAllowBeanDefinitionOverriding(false);
5254
ctx.refresh();
55+
5356
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse();
5457
assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden");
5558
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue();
@@ -61,17 +64,45 @@ void beanMethodOverridingOnASM() {
6164
ctx.registerBeanDefinition("config", new RootBeanDefinition(OverridingConfig.class.getName()));
6265
ctx.setAllowBeanDefinitionOverriding(false);
6366
ctx.refresh();
67+
6468
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse();
6569
assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden");
6670
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue();
6771
}
6872

73+
@Test
74+
void beanMethodOverridingWithDifferentBeanName() {
75+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
76+
ctx.register(OverridingConfigWithDifferentBeanName.class);
77+
ctx.setAllowBeanDefinitionOverriding(false);
78+
ctx.refresh();
79+
80+
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isFalse();
81+
assertThat(ctx.getBean("myTestBean", BaseTestBean.class).toString()).isEqualTo("overridden");
82+
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isTrue();
83+
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse();
84+
}
85+
86+
@Test
87+
void beanMethodOverridingWithDifferentBeanNameOnASM() {
88+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
89+
ctx.registerBeanDefinition("config", new RootBeanDefinition(OverridingConfigWithDifferentBeanName.class.getName()));
90+
ctx.setAllowBeanDefinitionOverriding(false);
91+
ctx.refresh();
92+
93+
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isFalse();
94+
assertThat(ctx.getBean("myTestBean", BaseTestBean.class).toString()).isEqualTo("overridden");
95+
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("myTestBean")).isTrue();
96+
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse();
97+
}
98+
6999
@Test
70100
void beanMethodOverridingWithNarrowedReturnType() {
71101
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
72102
ctx.register(NarrowedOverridingConfig.class);
73103
ctx.setAllowBeanDefinitionOverriding(false);
74104
ctx.refresh();
105+
75106
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse();
76107
assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden");
77108
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue();
@@ -83,6 +114,7 @@ void beanMethodOverridingWithNarrowedReturnTypeOnASM() {
83114
ctx.registerBeanDefinition("config", new RootBeanDefinition(NarrowedOverridingConfig.class.getName()));
84115
ctx.setAllowBeanDefinitionOverriding(false);
85116
ctx.refresh();
117+
86118
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse();
87119
assertThat(ctx.getBean("testBean", BaseTestBean.class).toString()).isEqualTo("overridden");
88120
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue();
@@ -94,6 +126,7 @@ void beanMethodOverloadingWithoutInheritance() {
94126
ctx.register(ConfigWithOverloading.class);
95127
ctx.setAllowBeanDefinitionOverriding(false);
96128
ctx.refresh();
129+
97130
assertThat(ctx.getBean(String.class)).isEqualTo("regular");
98131
}
99132

@@ -104,6 +137,7 @@ void beanMethodOverloadingWithoutInheritanceAndExtraDependency() {
104137
ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5);
105138
ctx.setAllowBeanDefinitionOverriding(false);
106139
ctx.refresh();
140+
107141
assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5");
108142
}
109143

@@ -113,6 +147,7 @@ void beanMethodOverloadingWithAdditionalMetadata() {
113147
ctx.register(ConfigWithOverloadingAndAdditionalMetadata.class);
114148
ctx.setAllowBeanDefinitionOverriding(false);
115149
ctx.refresh();
150+
116151
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse();
117152
assertThat(ctx.getBean(String.class)).isEqualTo("regular");
118153
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue();
@@ -125,6 +160,7 @@ void beanMethodOverloadingWithAdditionalMetadataButOtherMethodExecuted() {
125160
ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5);
126161
ctx.setAllowBeanDefinitionOverriding(false);
127162
ctx.refresh();
163+
128164
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse();
129165
assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5");
130166
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue();
@@ -136,18 +172,19 @@ void beanMethodOverloadingWithInheritance() {
136172
ctx.register(SubConfig.class);
137173
ctx.setAllowBeanDefinitionOverriding(false);
138174
ctx.refresh();
175+
139176
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse();
140177
assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5");
141178
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue();
142179
}
143180

144-
// SPR-11025
145-
@Test
181+
@Test // SPR-11025
146182
void beanMethodOverloadingWithInheritanceAndList() {
147183
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
148184
ctx.register(SubConfigWithList.class);
149185
ctx.setAllowBeanDefinitionOverriding(false);
150186
ctx.refresh();
187+
151188
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse();
152189
assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5");
153190
assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue();
@@ -161,6 +198,7 @@ void beanMethodOverloadingWithInheritanceAndList() {
161198
@Test
162199
void beanMethodShadowing() {
163200
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ShadowConfig.class);
201+
164202
assertThat(ctx.getBean(String.class)).isEqualTo("shadow");
165203
}
166204

@@ -214,6 +252,22 @@ public String toString() {
214252
}
215253

216254

255+
@Configuration
256+
static class OverridingConfigWithDifferentBeanName extends BaseConfig {
257+
258+
@Bean("myTestBean") @Lazy
259+
@Override
260+
public BaseTestBean testBean() {
261+
return new BaseTestBean() {
262+
@Override
263+
public String toString() {
264+
return "overridden";
265+
}
266+
};
267+
}
268+
}
269+
270+
217271
@Configuration
218272
static class NarrowedOverridingConfig extends BaseConfig {
219273

Diff for: spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java

+14
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ void finalBeanMethod() {
143143
initBeanFactory(ConfigWithFinalBean.class));
144144
}
145145

146+
@Test
147+
void finalBeanMethodWithoutProxy() {
148+
initBeanFactory(ConfigWithFinalBeanWithoutProxy.class);
149+
}
150+
146151
@Test // gh-31007
147152
void voidBeanMethod() {
148153
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() ->
@@ -438,6 +443,15 @@ static class ConfigWithFinalBean {
438443
}
439444

440445

446+
@Configuration(proxyBeanMethods = false)
447+
static class ConfigWithFinalBeanWithoutProxy {
448+
449+
@Bean public final TestBean testBean() {
450+
return new TestBean();
451+
}
452+
}
453+
454+
441455
@Configuration
442456
static class ConfigWithVoidBean {
443457

0 commit comments

Comments
 (0)