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
pull/24597/head
Phillip Webb 4 years ago
parent f568aa489c
commit e1b158ec66

@ -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> {

Loading…
Cancel
Save