Skip to content

Commit 9c74690

Browse files
committed
Test status quo for Bean Override singleton semantics
The tests introduced in this commit reveal the following issues in our Bean Override support. - If a FactoryBean signals it does not manage a singleton, the Bean Override support silently replaces it with a singleton. - An attempt to override a prototype-scoped bean or a bean configured with a custom scope results in one of the following. - If the bean type of the original bean definition is a concrete class, an attempt will be made to invoke the default constructor which will either succeed with undesirable results or fail with an exception if the bean type does not have a default constructor. - If the bean type of the original bean definition is an interface or a FactoryBean that claims to create a bean of a certain interface type, an attempt will be made to instantiate the interface which will always fail with a BeanCreationException.
1 parent c3ff6cf commit 9c74690

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed

spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java

+184
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.junit.jupiter.api.Test;
2626

2727
import org.springframework.beans.BeanWrapper;
28+
import org.springframework.beans.factory.BeanCreationException;
2829
import org.springframework.beans.factory.FactoryBean;
2930
import org.springframework.beans.factory.annotation.Qualifier;
3031
import org.springframework.beans.factory.config.BeanDefinition;
@@ -37,10 +38,12 @@
3738
import org.springframework.core.Ordered;
3839
import org.springframework.core.ResolvableType;
3940
import org.springframework.test.context.MergedContextConfiguration;
41+
import org.springframework.test.context.bean.override.convention.TestBean;
4042
import org.springframework.test.util.ReflectionTestUtils;
4143
import org.springframework.util.Assert;
4244

4345
import static org.assertj.core.api.Assertions.assertThat;
46+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4447
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
4548
import static org.assertj.core.api.Assertions.assertThatNoException;
4649
import static org.mockito.Mockito.mock;
@@ -212,6 +215,130 @@ void replaceBeanByNameWithMatchingBeanDefinitionWithExplicitSingletonScope() {
212215
assertThat(context.getBean("descriptionBean")).isEqualTo("overridden");
213216
}
214217

218+
@Test
219+
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedSingletonFactoryBean() {
220+
String beanName = "descriptionBean";
221+
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
222+
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonStringFactoryBean.class);
223+
context.registerBeanDefinition(beanName, factoryBeanDefinition);
224+
225+
assertThatNoException().isThrownBy(context::refresh);
226+
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
227+
assertThat(context.getBean(beanName)).isEqualTo("overridden");
228+
}
229+
230+
@Test
231+
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBean() {
232+
String beanName = "descriptionBean";
233+
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
234+
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonStringFactoryBean.class);
235+
context.registerBeanDefinition(beanName, factoryBeanDefinition);
236+
237+
assertThatNoException().isThrownBy(context::refresh);
238+
// Even though the FactoryBean signals it does not manage a singleton,
239+
// the Bean Override support currently replaces it with a singleton.
240+
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
241+
assertThat(context.getBean(beanName)).isEqualTo("overridden");
242+
}
243+
244+
@Test
245+
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedSingletonFactoryBean() {
246+
String beanName = "messageServiceBean";
247+
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
248+
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class);
249+
context.registerBeanDefinition(beanName, factoryBeanDefinition);
250+
251+
assertThatNoException().isThrownBy(context::refresh);
252+
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
253+
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
254+
}
255+
256+
@Test
257+
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBean() {
258+
String beanName = "messageServiceBean";
259+
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
260+
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonMessageServiceFactoryBean.class);
261+
context.registerBeanDefinition(beanName, factoryBeanDefinition);
262+
263+
assertThatNoException().isThrownBy(context::refresh);
264+
// Even though the FactoryBean signals it does not manage a singleton,
265+
// the Bean Override support currently replaces it with a singleton.
266+
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
267+
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
268+
}
269+
270+
@Test
271+
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScope() {
272+
String beanName = "descriptionBean";
273+
274+
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
275+
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
276+
definition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
277+
context.registerBeanDefinition(beanName, definition);
278+
279+
assertThatNoException().isThrownBy(context::refresh);
280+
// The Bean Override support currently creates a "dummy" BeanDefinition that
281+
// retains the prototype scope of the original BeanDefinition.
282+
assertThat(context.isSingleton(beanName)).as("isSingleton").isFalse();
283+
assertThat(context.isPrototype(beanName)).as("isPrototype").isTrue();
284+
// Since the "dummy" BeanDefinition has prototype scope, a manual singleton
285+
// is not registered, and the "dummy" BeanDefinition is used to create a
286+
// new java.lang.String using the default constructor, which results in an
287+
// empty string instead of "overridden". In other words, the bean is not
288+
// actually overridden as expected, and no exception is thrown which
289+
// silently masks the issue.
290+
assertThat(context.getBean(beanName)).isEqualTo("");
291+
}
292+
293+
@Test
294+
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScope() {
295+
String beanName = "descriptionBean";
296+
String scope = "customScope";
297+
298+
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
299+
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
300+
beanFactory.registerScope(scope, new SimpleThreadScope());
301+
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
302+
definition.setScope(scope);
303+
context.registerBeanDefinition(beanName, definition);
304+
305+
assertThatNoException().isThrownBy(context::refresh);
306+
// The Bean Override support currently creates a "dummy" BeanDefinition that
307+
// retains the custom scope of the original BeanDefinition.
308+
assertThat(context.isSingleton(beanName)).as("isSingleton").isFalse();
309+
assertThat(context.isPrototype(beanName)).as("isPrototype").isFalse();
310+
assertThat(beanFactory.getBeanDefinition(beanName).getScope()).isEqualTo(scope);
311+
// Since the "dummy" BeanDefinition has a custom scope, a manual singleton
312+
// is not registered, and the "dummy" BeanDefinition is used to create a
313+
// new java.lang.String using the default constructor, which results in an
314+
// empty string instead of "overridden". In other words, the bean is not
315+
// actually overridden as expected, and no exception is thrown which
316+
// silently masks the issue.
317+
assertThat(context.getBean(beanName)).isEqualTo("");
318+
}
319+
320+
@Test
321+
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBean() {
322+
String beanName = "messageServiceBean";
323+
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
324+
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class);
325+
factoryBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
326+
context.registerBeanDefinition(beanName, factoryBeanDefinition);
327+
328+
assertThatNoException().isThrownBy(context::refresh);
329+
// The Bean Override support currently creates a "dummy" BeanDefinition that
330+
// retains the prototype scope of the original BeanDefinition.
331+
assertThat(context.isSingleton(beanName)).as("isSingleton").isFalse();
332+
assertThat(context.isPrototype(beanName)).as("isPrototype").isTrue();
333+
// Since the "dummy" BeanDefinition has prototype scope, a manual singleton
334+
// is not registered, and the "dummy" BeanDefinition is used to create a
335+
// new MessageService using the default constructor, which results in an
336+
// error since MessageService is an interface.
337+
assertThatExceptionOfType(BeanCreationException.class)
338+
.isThrownBy(() -> context.getBean(beanName))
339+
.withMessageContaining("Specified class is an interface");
340+
}
341+
215342
@Test
216343
void replaceBeanByNameWithMatchingBeanDefinitionRetainsPrimaryFallbackAndScopeProperties() {
217344
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
@@ -330,6 +457,63 @@ public boolean isSingleton() {
330457
}
331458
}
332459

460+
static class SingletonStringFactoryBean implements FactoryBean<String> {
461+
462+
@Override
463+
public String getObject() {
464+
return "test";
465+
}
466+
467+
@Override
468+
public Class<?> getObjectType() {
469+
return String.class;
470+
}
471+
}
472+
473+
static class NonSingletonStringFactoryBean extends SingletonStringFactoryBean {
474+
475+
@Override
476+
public boolean isSingleton() {
477+
return false;
478+
}
479+
}
480+
481+
static class SingletonMessageServiceFactoryBean implements FactoryBean<MessageService> {
482+
483+
@Override
484+
public MessageService getObject() {
485+
return () -> "test";
486+
}
487+
488+
@Override
489+
public Class<?> getObjectType() {
490+
return MessageService.class;
491+
}
492+
}
493+
494+
static class NonSingletonMessageServiceFactoryBean extends SingletonMessageServiceFactoryBean {
495+
496+
@Override
497+
public boolean isSingleton() {
498+
return false;
499+
}
500+
}
501+
502+
@FunctionalInterface
503+
interface MessageService {
504+
String getMessage();
505+
}
506+
507+
static class MessageServiceTestCase {
508+
509+
@TestBean(name = "messageServiceBean")
510+
MessageService messageService;
511+
512+
static MessageService messageService() {
513+
return () -> "overridden";
514+
}
515+
}
516+
333517
static class FactoryBeanRegisteringPostProcessor implements BeanFactoryPostProcessor, Ordered {
334518

335519
@Override

0 commit comments

Comments
 (0)