From e1b158ec6621ca5164f3dad7ffd1e8737b4b7db1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 18 Dec 2020 15:14:04 -0800 Subject: [PATCH 1/2] 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 e38feb398f..c12d65a59a 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 java.util.function.Supplier; 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 BootstrapRegistry { */ 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 @@ public interface BootstrapRegistry { } + /** + * 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 cc73b8ec36..eec1b2a714 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 @@ public class DefaultBootstrapContext implements ConfigurableBootstrapContext { 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 c727f554b6..05dff44cac 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.assertj.core.api.AssertProvider; 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 @@ class DefaultBootstrapContextTests { 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 @@ class DefaultBootstrapContextTests { } @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 @@ class DefaultBootstrapContextTests { 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 { From 5317d8a9bb59af8d47aab020112d7567fdf3846e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 18 Dec 2020 15:16:11 -0800 Subject: [PATCH 2/2] Change scope of bootstrap registered Binder Update `ConfigDataEnvironment` so that the `Binder` is registered as a prototype bootstrap instance. This allows it to be accessed early but still replaced when a more complete version is available. Fixes gh-24559 --- .../boot/context/config/ConfigDataEnvironment.java | 11 ++++++++--- .../boot/context/config/TestConfigDataBootstrap.java | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java index 8232314a9d..2cbf0faa85 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java @@ -22,10 +22,12 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.springframework.boot.BootstrapRegistry.InstanceSupplier; +import org.springframework.boot.BootstrapRegistry.Scope; import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.DefaultPropertiesPropertySource; import org.springframework.boot.context.config.ConfigDataEnvironmentContributors.BinderOption; @@ -220,11 +222,10 @@ class ConfigDataEnvironment { void processAndApply() { ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers, this.loaders); - this.bootstrapContext.register(Binder.class, InstanceSupplier - .from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE))); + registerBootstrapBinder(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)); ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer); Binder initialBinder = contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE); - this.bootstrapContext.register(Binder.class, InstanceSupplier.of(initialBinder)); + registerBootstrapBinder(() -> initialBinder); ConfigDataActivationContext activationContext = createActivationContext(initialBinder); contributors = processWithoutProfiles(contributors, importer, activationContext); activationContext = withProfiles(contributors, activationContext); @@ -232,6 +233,10 @@ class ConfigDataEnvironment { applyToEnvironment(contributors, activationContext); } + private void registerBootstrapBinder(Supplier supplier) { + this.bootstrapContext.register(Binder.class, InstanceSupplier.from(supplier).withScope(Scope.PROTOTYPE)); + } + private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors, ConfigDataImporter importer) { this.logger.trace("Processing initial config data environment contributors without activation context"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java index e3e0be4f22..e61d935994 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java @@ -41,6 +41,7 @@ class TestConfigDataBootstrap { @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { + context.getBootstrapContext().get(Binder.class); // gh-24559 return location.hasPrefix("testbootstrap:"); }