mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-21 13:31:46 +00:00
Support Bean Overrides for non-singletons
Prior to this commit, the BeanOverrideBeanFactoryPostProcessor rejected any attempt to override a non-singleton bean; however, due to interest from the community, we have decided to provide support for overriding non-singleton beans via the Bean Override mechanism — for example, when using @MockitoBean, @MockitoSpyBean, and @TestBean. With this commit, we now support Bean Overrides for non-singletons: for standard JVM runtimes as well as AOT processing and AOT runtimes. This commit also documents that non-singletons will effectively be converted to singletons when overridden and logs a warning similar to the following. WARN: BeanOverrideBeanFactoryPostProcessor - Converting 'prototype' scoped bean definition 'myBean' to a singleton. See gh-33602 See gh-32933 See gh-33800 Closes gh-35574
This commit is contained in:
+12
-12
@@ -321,7 +321,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScopeFails() {
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScope() {
|
||||
String beanName = "descriptionBean";
|
||||
|
||||
AnnotationConfigApplicationContext context = createContext(ByNameTestCase.class);
|
||||
@@ -329,13 +329,13 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
||||
definition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
context.registerBeanDefinition(beanName, definition);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
|
||||
assertThat(context.getBean(beanName, String.class)).isEqualTo("overridden");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScopeFails() {
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScope() {
|
||||
String beanName = "descriptionBean";
|
||||
String scope = "customScope";
|
||||
|
||||
@@ -346,22 +346,22 @@ class BeanOverrideBeanFactoryPostProcessorTests {
|
||||
definition.setScope(scope);
|
||||
context.registerBeanDefinition(beanName, definition);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
|
||||
assertThat(context.getBean(beanName, String.class)).isEqualTo("overridden");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBeanFails() {
|
||||
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBean() {
|
||||
String beanName = "messageServiceBean";
|
||||
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
|
||||
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class);
|
||||
factoryBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
context.registerBeanDefinition(beanName, factoryBeanDefinition);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(context::refresh)
|
||||
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden.");
|
||||
assertThatNoException().isThrownBy(context::refresh);
|
||||
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
|
||||
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+24
@@ -21,8 +21,10 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -43,6 +45,10 @@ public class TestBeanByNameLookupIntegrationTests {
|
||||
@TestBean(name = "methodRenamed1", methodName = "field")
|
||||
String methodRenamed1;
|
||||
|
||||
@TestBean("prototypeScoped")
|
||||
String prototypeScoped;
|
||||
|
||||
|
||||
static String field() {
|
||||
return "fieldOverride";
|
||||
}
|
||||
@@ -51,6 +57,11 @@ public class TestBeanByNameLookupIntegrationTests {
|
||||
return "nestedFieldOverride";
|
||||
}
|
||||
|
||||
static String prototypeScoped() {
|
||||
return "prototypeScopedOverride";
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field")).as("applicationContext").isEqualTo("fieldOverride");
|
||||
@@ -63,6 +74,13 @@ public class TestBeanByNameLookupIntegrationTests {
|
||||
assertThat(methodRenamed1).as("injection point").isEqualTo("fieldOverride");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldForPrototypeHasOverride(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("prototypeScoped").isSingleton()).as("isSingleton").isTrue();
|
||||
assertThat(ctx.getBean("prototypeScoped")).as("applicationContext").isEqualTo("prototypeScopedOverride");
|
||||
assertThat(prototypeScoped).as("injection point").isEqualTo("prototypeScopedOverride");
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @TestBean in enclosing class and in @Nested class")
|
||||
@@ -180,6 +198,12 @@ public class TestBeanByNameLookupIntegrationTests {
|
||||
String bean4() {
|
||||
return "NestedProd";
|
||||
}
|
||||
|
||||
@Bean("prototypeScoped")
|
||||
@Scope("prototype")
|
||||
String bean5() {
|
||||
return "PrototypeProd";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
@@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.bean.override.example.CustomQualifier;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
@@ -53,6 +55,9 @@ public class TestBeanByTypeLookupIntegrationTests {
|
||||
@CustomQualifier
|
||||
StringBuilder anyNameForStringBuilder2;
|
||||
|
||||
@TestBean
|
||||
Number prototypeNumber;
|
||||
|
||||
|
||||
static MessageService messageService() {
|
||||
return () -> "mocked nonexistent bean definition";
|
||||
@@ -70,6 +75,9 @@ public class TestBeanByTypeLookupIntegrationTests {
|
||||
return new StringBuilder("CustomQualifier TestBean String");
|
||||
}
|
||||
|
||||
static Number prototypeNumber() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForNonexistentBeanDefinition(ApplicationContext ctx) {
|
||||
@@ -101,6 +109,13 @@ public class TestBeanByTypeLookupIntegrationTests {
|
||||
assertThat(ctx.getBean("one")).as("no qualifier needed").hasToString("Prod One");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForPrototypeBeanDefinition(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("prototypeNumber").isSingleton()).as("isSingleton").isTrue();
|
||||
assertThat(this.prototypeNumber).isSameAs(ctx.getBean(Number.class));
|
||||
assertThat(this.prototypeNumber).isEqualTo(42);
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
@@ -126,6 +141,12 @@ public class TestBeanByTypeLookupIntegrationTests {
|
||||
StringBuilder beanString3() {
|
||||
return new StringBuilder("Prod Three");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
Number prototypeNumber() {
|
||||
return -999;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
+22
@@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
@@ -48,6 +50,9 @@ public class MockitoBeanByNameLookupIntegrationTests {
|
||||
@MockitoBean("nonExistingBean")
|
||||
ExampleService nonExisting;
|
||||
|
||||
@MockitoBean("prototypeScoped")
|
||||
ExampleService prototypeScoped;
|
||||
|
||||
|
||||
@Test
|
||||
void fieldAndRenamedFieldHaveSameOverride(ApplicationContext ctx) {
|
||||
@@ -69,6 +74,17 @@ public class MockitoBeanByNameLookupIntegrationTests {
|
||||
assertThat(nonExisting.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldForPrototypeHasOverride(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("prototypeScoped"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(MockitoAssertions::assertIsMock)
|
||||
.isSameAs(prototypeScoped);
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("prototypeScoped").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
assertThat(prototypeScoped.greeting()).as("mocked greeting").isNull();
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoBean in enclosing class and in @Nested class")
|
||||
@@ -139,6 +155,12 @@ public class MockitoBeanByNameLookupIntegrationTests {
|
||||
ExampleService bean2() {
|
||||
return new RealExampleService("Hello Nested Field");
|
||||
}
|
||||
|
||||
@Bean("prototypeScoped")
|
||||
@Scope("prototype")
|
||||
ExampleService bean3() {
|
||||
return new RealExampleService("Hello Prototype Field");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+31
@@ -21,8 +21,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.test.context.bean.override.example.CustomQualifier;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
@@ -63,6 +65,10 @@ public class MockitoBeanByTypeLookupIntegrationTests {
|
||||
@CustomQualifier
|
||||
StringBuilder ambiguousMeta;
|
||||
|
||||
@MockitoBean
|
||||
YetAnotherService yetAnotherService;
|
||||
|
||||
|
||||
@Test
|
||||
void mockIsCreatedWhenNoCandidateIsFound() {
|
||||
assertIsMock(this.serviceIsNotABean);
|
||||
@@ -122,11 +128,30 @@ public class MockitoBeanByTypeLookupIntegrationTests {
|
||||
verifyNoMoreInteractions(this.ambiguousMeta);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForPrototype(ConfigurableApplicationContext ctx) {
|
||||
assertThat(this.yetAnotherService)
|
||||
.satisfies(MockitoAssertions::assertIsMock)
|
||||
.isSameAs(ctx.getBean("YAS"))
|
||||
.isSameAs(ctx.getBean(YetAnotherService.class));
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("YAS").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
when(this.yetAnotherService.hello()).thenReturn("Mocked greeting");
|
||||
|
||||
assertThat(this.yetAnotherService.hello()).isEqualTo("Mocked greeting");
|
||||
verify(this.yetAnotherService, times(1)).hello();
|
||||
verifyNoMoreInteractions(this.yetAnotherService);
|
||||
}
|
||||
|
||||
|
||||
public interface AnotherService {
|
||||
|
||||
String hello();
|
||||
}
|
||||
|
||||
public interface YetAnotherService {
|
||||
|
||||
String hello();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -150,6 +175,12 @@ public class MockitoBeanByTypeLookupIntegrationTests {
|
||||
StringBuilder bean3() {
|
||||
return new StringBuilder("bean3");
|
||||
}
|
||||
|
||||
@Bean("YAS")
|
||||
@Scope("prototype")
|
||||
YetAnotherService bean4() {
|
||||
return () -> "Production Hello";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
@@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
import org.springframework.test.context.bean.override.example.RealExampleService;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBeanByNameLookupIntegrationTests.Config;
|
||||
@@ -46,6 +48,9 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
|
||||
@MockitoSpyBean("field1")
|
||||
ExampleService field;
|
||||
|
||||
@MockitoSpyBean("field3")
|
||||
ExampleService prototypeScoped;
|
||||
|
||||
|
||||
@Test
|
||||
void fieldHasOverride(ApplicationContext ctx) {
|
||||
@@ -57,6 +62,16 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
|
||||
assertThat(field.greeting()).isEqualTo("bean1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldForPrototypeHasOverride(ConfigurableApplicationContext ctx) {
|
||||
assertThat(ctx.getBean("field3"))
|
||||
.isInstanceOf(ExampleService.class)
|
||||
.satisfies(MockitoAssertions::assertIsSpy)
|
||||
.isSameAs(prototypeScoped);
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("field3").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
assertThat(prototypeScoped.greeting()).isEqualTo("bean3");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("With @MockitoSpyBean in enclosing class and in @Nested class")
|
||||
@@ -102,6 +117,12 @@ public class MockitoSpyBeanByNameLookupIntegrationTests {
|
||||
ExampleService bean2() {
|
||||
return new RealExampleService("bean2");
|
||||
}
|
||||
|
||||
@Bean("field3")
|
||||
@Scope("prototype")
|
||||
ExampleService bean3() {
|
||||
return new RealExampleService("bean3");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
@@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.test.context.bean.override.example.CustomQualifier;
|
||||
import org.springframework.test.context.bean.override.example.ExampleService;
|
||||
@@ -55,6 +57,9 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
||||
@CustomQualifier
|
||||
StringHolder ambiguousMeta;
|
||||
|
||||
@MockitoSpyBean
|
||||
AnotherService prototypeService;
|
||||
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByType(ApplicationContext ctx) {
|
||||
@@ -102,6 +107,19 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
||||
verifyNoMoreInteractions(this.ambiguousMeta);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideIsFoundByTypeForPrototype(ConfigurableApplicationContext ctx) {
|
||||
assertThat(this.prototypeService)
|
||||
.satisfies(MockitoAssertions::assertIsSpy)
|
||||
.isSameAs(ctx.getBean("anotherService"))
|
||||
.isSameAs(ctx.getBean(AnotherService.class));
|
||||
assertThat(ctx.getBeanFactory().getBeanDefinition("anotherService").isSingleton()).as("isSingleton").isTrue();
|
||||
|
||||
assertThat(this.prototypeService.hello()).isEqualTo("Production Hello");
|
||||
verify(this.prototypeService).hello();
|
||||
verifyNoMoreInteractions(this.prototypeService);
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
@@ -124,6 +142,12 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
||||
StringHolder bean3() {
|
||||
return new StringHolder("bean3");
|
||||
}
|
||||
|
||||
@Bean("anotherService")
|
||||
@Scope("prototype")
|
||||
AnotherService bean4() {
|
||||
return new DefaultAnotherService("Production Hello");
|
||||
}
|
||||
}
|
||||
|
||||
static class StringHolder {
|
||||
@@ -143,4 +167,17 @@ public class MockitoSpyBeanByTypeLookupIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
public interface AnotherService {
|
||||
|
||||
String hello();
|
||||
}
|
||||
|
||||
record DefaultAnotherService(String message) implements AnotherService {
|
||||
|
||||
@Override
|
||||
public String hello() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user