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
This commit is contained in:
@@ -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 <T> the instance type
|
||||
* @param type the instance type
|
||||
* @param instanceSupplier the instance supplier
|
||||
@@ -87,11 +89,13 @@ public interface BootstrapRegistry {
|
||||
void addCloseListener(ApplicationListener<BootstrapContextClosedEvent> listener);
|
||||
|
||||
/**
|
||||
* Supplier used to provide the actual instance the first time it is accessed.
|
||||
* Supplier used to provide the actual instance when needed.
|
||||
*
|
||||
* @param <T> the instance type
|
||||
* @see Scope
|
||||
*/
|
||||
public interface InstanceSupplier<T> {
|
||||
@FunctionalInterface
|
||||
interface InstanceSupplier<T> {
|
||||
|
||||
/**
|
||||
* 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<T> withScope(Scope scope) {
|
||||
Assert.notNull(scope, "Scope must not be null");
|
||||
InstanceSupplier<T> parent = this;
|
||||
return new InstanceSupplier<T>() {
|
||||
|
||||
@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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<String> supplier = InstanceSupplier.of("test");
|
||||
assertThat(supplier.getScope()).isEqualTo(Scope.SINGLETON);
|
||||
assertThat(supplier.get(null)).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void instanceSupplierWithScopeChangesScope() {
|
||||
InstanceSupplier<String> 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<BootstrapContextClosedEvent>, AssertProvider<CloseListenerAssert> {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user