From e1b158ec6621ca5164f3dad7ffd1e8737b4b7db1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 18 Dec 2020 15:14:04 -0800 Subject: [PATCH] Add BootstrapRegistry Scope support Update `BootstrapRegistry` so that it can be used to register instances in either a `singleton` or `prototype` scope. The prototype scope has been added so that instances can be registered and replaced later if needed. See gh-24559 --- .../boot/BootstrapRegistry.java | 63 ++++++++++++++++++- .../boot/DefaultBootstrapContext.java | 4 +- .../boot/DefaultBootstrapContextTests.java | 48 +++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java index e38feb398f7d..c12d65a59a12 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java @@ -21,6 +21,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.core.env.Environment; +import org.springframework.util.Assert; /** * A simple object registry that is available during startup and {@link Environment} @@ -47,7 +48,8 @@ public interface BootstrapRegistry { /** * Register a specific type with the registry. If the specified type has already been - * registered, but not get obtained, it will be replaced. + * registered and has not been obtained as a {@link Scope#SINGLETON singleton}, it + * will be replaced. * @param the instance type * @param type the instance type * @param instanceSupplier the instance supplier @@ -87,11 +89,13 @@ public interface BootstrapRegistry { void addCloseListener(ApplicationListener listener); /** - * Supplier used to provide the actual instance the first time it is accessed. + * Supplier used to provide the actual instance when needed. * * @param the instance type + * @see Scope */ - public interface InstanceSupplier { + @FunctionalInterface + interface InstanceSupplier { /** * Factory method used to create the instance when needed. @@ -101,6 +105,39 @@ public interface InstanceSupplier { */ T get(BootstrapContext context); + /** + * Return the scope of the supplied instance. + * @return the scope + * @since 2.4.2 + */ + default Scope getScope() { + return Scope.SINGLETON; + } + + /** + * Return a new {@link InstanceSupplier} with an updated {@link Scope}. + * @param scope the new scope + * @return a new {@link InstanceSupplier} instance with the new scope + * @since 2.4.2 + */ + default InstanceSupplier withScope(Scope scope) { + Assert.notNull(scope, "Scope must not be null"); + InstanceSupplier parent = this; + return new InstanceSupplier() { + + @Override + public T get(BootstrapContext context) { + return parent.get(context); + } + + @Override + public Scope getScope() { + return scope; + } + + }; + } + /** * Factory method that can be used to create a {@link InstanceSupplier} for a * given instance. @@ -125,4 +162,24 @@ static InstanceSupplier from(Supplier supplier) { } + /** + * The scope of a instance. + * @since 2.4.2 + */ + enum Scope { + + /** + * A singleton instance. The {@link InstanceSupplier} will be called only once and + * the same instance will be returned each time. + */ + SINGLETON, + + /** + * A prototype instance. The {@link InstanceSupplier} will be called whenver an + * instance is needed. + */ + PROTOTYPE + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java index cc73b8ec36a3..eec1b2a714cd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java @@ -117,7 +117,9 @@ private T getInstance(Class type, InstanceSupplier instanceSupplier) { T instance = (T) this.instances.get(type); if (instance == null) { instance = (T) instanceSupplier.get(this); - this.instances.put(type, instance); + if (instanceSupplier.getScope() == Scope.SINGLETON) { + this.instances.put(type, instance); + } } return instance; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultBootstrapContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultBootstrapContextTests.java index c727f554b6e8..05dff44cac16 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultBootstrapContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultBootstrapContextTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.BootstrapRegistry.InstanceSupplier; +import org.springframework.boot.BootstrapRegistry.Scope; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; @@ -74,6 +75,24 @@ void registerWhenAlreadyRegisteredRegistersReplacedInstance() { assertThat(this.context.get(Integer.class)).isEqualTo(100); } + @Test + void registerWhenSingletonAlreadyCreatedThrowsException() { + this.context.register(Integer.class, InstanceSupplier.from(this.counter::getAndIncrement)); + this.context.get(Integer.class); + assertThatIllegalStateException() + .isThrownBy(() -> this.context.register(Integer.class, InstanceSupplier.of(100))) + .withMessage("java.lang.Integer has already been created"); + } + + @Test + void registerWhenPrototypeAlreadyCreatedReplacesInstance() { + this.context.register(Integer.class, + InstanceSupplier.from(this.counter::getAndIncrement).withScope(Scope.PROTOTYPE)); + this.context.get(Integer.class); + this.context.register(Integer.class, InstanceSupplier.of(100)); + assertThat(this.context.get(Integer.class)).isEqualTo(100); + } + @Test void registerWhenAlreadyCreatedThrowsException() { this.context.register(Integer.class, InstanceSupplier.from(this.counter::getAndIncrement)); @@ -146,12 +165,25 @@ void getWhenRegisteredAsNullReturnsNull() { } @Test - void getCreatesOnlyOneInstance() { + void getWhenSingletonCreatesOnlyOneInstance() { this.context.register(Integer.class, InstanceSupplier.from(this.counter::getAndIncrement)); assertThat(this.context.get(Integer.class)).isEqualTo(0); assertThat(this.context.get(Integer.class)).isEqualTo(0); } + @Test + void getWhenPrototypeCreatesOnlyNewInstances() { + this.context.register(Integer.class, + InstanceSupplier.from(this.counter::getAndIncrement).withScope(Scope.PROTOTYPE)); + assertThat(this.context.get(Integer.class)).isEqualTo(0); + assertThat(this.context.get(Integer.class)).isEqualTo(1); + } + + @Test + void testName() { + + } + @Test void getOrElseWhenNoRegistrationReturnsOther() { this.context.register(Number.class, InstanceSupplier.of(1)); @@ -228,6 +260,20 @@ void addCloseListenerIgnoresMultipleCallsWithSameListener() { assertThat(listener).wasCalledOnlyOnce(); } + @Test + void instanceSupplierGetScopeWhenNotConfiguredReturnsSingleton() { + InstanceSupplier supplier = InstanceSupplier.of("test"); + assertThat(supplier.getScope()).isEqualTo(Scope.SINGLETON); + assertThat(supplier.get(null)).isEqualTo("test"); + } + + @Test + void instanceSupplierWithScopeChangesScope() { + InstanceSupplier supplier = InstanceSupplier.of("test").withScope(Scope.PROTOTYPE); + assertThat(supplier.getScope()).isEqualTo(Scope.PROTOTYPE); + assertThat(supplier.get(null)).isEqualTo("test"); + } + private static class TestCloseListener implements ApplicationListener, AssertProvider {