Skip to content

Commit

Permalink
Add BootstrapRegistry Scope support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Dec 18, 2020
1 parent f568aa4 commit e1b158e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -101,6 +105,39 @@ public interface InstanceSupplier<T> {
*/
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.
Expand All @@ -125,4 +162,24 @@ static <T> InstanceSupplier<T> from(Supplier<T> 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

}

}
Expand Up @@ -117,7 +117,9 @@ private <T> T getInstance(Class<T> 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;
}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -228,6 +260,20 @@ void addCloseListenerIgnoresMultipleCallsWithSameListener() {
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> {

Expand Down

0 comments on commit e1b158e

Please sign in to comment.