Browse Source
Add a simple `BootstrapRegistry` that can be used to store and share object instances across `EnvironmentPostProcessors`. The registry can be injected into the constructor of any `EnvironmentPostProcessor`. Registrations can also perform additional actions when the `ApplicationContext` has been prepared. For example, they could register the the bootstrap instances as beans so that they become available to the application. See gh-22956pull/23048/head
11 changed files with 542 additions and 85 deletions
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright 2012-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.env; |
||||
|
||||
import java.util.function.BiConsumer; |
||||
import java.util.function.Supplier; |
||||
|
||||
import org.springframework.boot.context.event.ApplicationPreparedEvent; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.core.env.Environment; |
||||
|
||||
/** |
||||
* A simple object registry that is available during {@link Environment} post-processing |
||||
* up to the point that the {@link ApplicationContext} is prepared. The registry can be |
||||
* used to store objects that may be expensive to create, or need to be shared by |
||||
* different {@link EnvironmentPostProcessor EnvironmentPostProcessors}. |
||||
* <p> |
||||
* The registry uses the object type as a key, meaning that only a single instance of a |
||||
* given class can be stored. |
||||
* <p> |
||||
* Registered instances may optionally use |
||||
* {@link Registration#onApplicationContextPrepared(BiConsumer) |
||||
* onApplicationContextPrepared(...)} to perform an action when the |
||||
* {@link ApplicationContext} is {@link ApplicationPreparedEvent prepared}. For example, |
||||
* an instance may choose to register itself as a regular Spring bean so that it is |
||||
* available for the application to use. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 2.4.0 |
||||
* @see EnvironmentPostProcessor |
||||
*/ |
||||
public interface BootstrapRegistry { |
||||
|
||||
/** |
||||
* Get an instance from the registry, creating one if it does not already exist. |
||||
* @param <T> the instance type |
||||
* @param type the instance type |
||||
* @param instanceSupplier a supplier used to create the instance if it doesn't |
||||
* already exist |
||||
* @return the registered instance |
||||
*/ |
||||
<T> T get(Class<T> type, Supplier<T> instanceSupplier); |
||||
|
||||
/** |
||||
* Get an instance from the registry, creating one if it does not already exist. |
||||
* @param <T> the instance type |
||||
* @param type the instance type |
||||
* @param instanceSupplier a supplier used to create the instance if it doesn't |
||||
* already exist |
||||
* @param onApplicationContextPreparedAction the action that should be called when the |
||||
* application context is prepared. This action is ignored if the registration already |
||||
* exists. |
||||
* @return the registered instance |
||||
*/ |
||||
<T> T get(Class<T> type, Supplier<T> instanceSupplier, |
||||
BiConsumer<ConfigurableApplicationContext, T> onApplicationContextPreparedAction); |
||||
|
||||
/** |
||||
* Register an instance with the registry and return a {@link Registration} that can |
||||
* be used to provide further configuration. This method will replace any existing |
||||
* registration. |
||||
* @param <T> the instance type |
||||
* @param type the instance type |
||||
* @param instanceSupplier a supplier used to create the instance if it doesn't |
||||
* already exist |
||||
* @return an instance registration |
||||
*/ |
||||
<T> Registration<T> register(Class<T> type, Supplier<T> instanceSupplier); |
||||
|
||||
/** |
||||
* Return if a registration exists for the given type. |
||||
* @param <T> the instance type |
||||
* @param type the instance type |
||||
* @return {@code true} if the type has already been registered |
||||
*/ |
||||
<T> boolean isRegistered(Class<T> type); |
||||
|
||||
/** |
||||
* Return any existing {@link Registration} for the given type. |
||||
* @param <T> the instance type |
||||
* @param type the instance type |
||||
* @return the existing registration or {@code null} |
||||
*/ |
||||
<T> Registration<T> getRegistration(Class<T> type); |
||||
|
||||
/** |
||||
* A single registration contained in the registry. |
||||
* |
||||
* @param <T> the instance type |
||||
*/ |
||||
interface Registration<T> { |
||||
|
||||
/** |
||||
* Get or crearte the registered object instance. |
||||
* @return the object instance |
||||
*/ |
||||
T get(); |
||||
|
||||
/** |
||||
* Add an action that should run when the {@link ApplicationContext} has been |
||||
* prepared. |
||||
* @param action the action to run |
||||
*/ |
||||
void onApplicationContextPrepared(BiConsumer<ConfigurableApplicationContext, T> action); |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
/* |
||||
* Copyright 2012-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.env; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.BiConsumer; |
||||
import java.util.function.Supplier; |
||||
|
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
|
||||
/** |
||||
* Default implementation of {@link BootstrapRegistry}. |
||||
* |
||||
* @author Phillip Webb |
||||
* @since 2.4.0 |
||||
*/ |
||||
public class DefaultBootstrapRegisty implements BootstrapRegistry { |
||||
|
||||
private final Map<Class<?>, DefaultRegistration<?>> registrations = new HashMap<>(); |
||||
|
||||
@Override |
||||
public <T> T get(Class<T> type, Supplier<T> instanceSupplier) { |
||||
return get(type, instanceSupplier, null); |
||||
} |
||||
|
||||
@Override |
||||
public <T> T get(Class<T> type, Supplier<T> instanceSupplier, |
||||
BiConsumer<ConfigurableApplicationContext, T> onApplicationContextPreparedAction) { |
||||
Registration<T> registration = getRegistration(type); |
||||
if (registration != null) { |
||||
return registration.get(); |
||||
} |
||||
registration = register(type, instanceSupplier); |
||||
registration.onApplicationContextPrepared(onApplicationContextPreparedAction); |
||||
return registration.get(); |
||||
} |
||||
|
||||
@Override |
||||
public <T> Registration<T> register(Class<T> type, Supplier<T> instanceSupplier) { |
||||
DefaultRegistration<T> registration = new DefaultRegistration<>(instanceSupplier); |
||||
this.registrations.put(type, registration); |
||||
return registration; |
||||
} |
||||
|
||||
@Override |
||||
public <T> boolean isRegistered(Class<T> type) { |
||||
return getRegistration(type) != null; |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public <T> Registration<T> getRegistration(Class<T> type) { |
||||
return (Registration<T>) this.registrations.get(type); |
||||
} |
||||
|
||||
/** |
||||
* Method to be called when the {@link ApplicationContext} is prepared. |
||||
* @param applicationContext the prepared context |
||||
*/ |
||||
public void applicationContextPrepared(ConfigurableApplicationContext applicationContext) { |
||||
this.registrations.values() |
||||
.forEach((registration) -> registration.applicationContextPrepared(applicationContext)); |
||||
} |
||||
|
||||
/** |
||||
* Default implementation of {@link Registration}. |
||||
*/ |
||||
private static class DefaultRegistration<T> implements Registration<T> { |
||||
|
||||
private Supplier<T> instanceSupplier; |
||||
|
||||
private volatile T instance; |
||||
|
||||
private List<BiConsumer<ConfigurableApplicationContext, T>> applicationContextPreparedActions = new ArrayList<>(); |
||||
|
||||
DefaultRegistration(Supplier<T> instanceSupplier) { |
||||
this.instanceSupplier = instanceSupplier; |
||||
} |
||||
|
||||
@Override |
||||
public T get() { |
||||
T instance = this.instance; |
||||
if (instance == null) { |
||||
synchronized (this.instanceSupplier) { |
||||
instance = this.instanceSupplier.get(); |
||||
this.instance = instance; |
||||
} |
||||
} |
||||
return instance; |
||||
} |
||||
|
||||
@Override |
||||
public void onApplicationContextPrepared(BiConsumer<ConfigurableApplicationContext, T> action) { |
||||
if (action != null) { |
||||
this.applicationContextPreparedActions.add(action); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Method called when the {@link ApplicationContext} is prepared. |
||||
* @param applicationContext the prepared context |
||||
*/ |
||||
void applicationContextPrepared(ConfigurableApplicationContext applicationContext) { |
||||
this.applicationContextPreparedActions.forEach((consumer) -> consumer.accept(applicationContext, get())); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,199 @@
@@ -0,0 +1,199 @@
|
||||
/* |
||||
* Copyright 2012-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.boot.env; |
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
||||
import org.assertj.core.api.AbstractAssert; |
||||
import org.assertj.core.api.AssertProvider; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.env.BootstrapRegistry.Registration; |
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.context.support.StaticApplicationContext; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Test for {@link DefaultBootstrapRegisty}. |
||||
* |
||||
* @author Phillip Webb |
||||
*/ |
||||
class DefaultBootstrapRegistyTests { |
||||
|
||||
private DefaultBootstrapRegisty registy = new DefaultBootstrapRegisty(); |
||||
|
||||
private AtomicInteger counter = new AtomicInteger(); |
||||
|
||||
private StaticApplicationContext context = new StaticApplicationContext(); |
||||
|
||||
@Test |
||||
void getWhenNotRegisteredCreateInstance() { |
||||
Integer result = this.registy.get(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(result).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
void getWhenAlreadyRegisteredReturnsExisting() { |
||||
this.registy.get(Integer.class, this.counter::getAndIncrement); |
||||
Integer result = this.registy.get(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(result).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
void getWithPreparedActionRegistersAction() { |
||||
TestApplicationPreparedAction action = new TestApplicationPreparedAction(); |
||||
Integer result = this.registy.get(Integer.class, this.counter::getAndIncrement, action::run); |
||||
this.registy.applicationContextPrepared(this.context); |
||||
assertThat(result).isEqualTo(0); |
||||
assertThat(action).wasCalledOnlyOnce().hasInstanceValue(0); |
||||
} |
||||
|
||||
@Test |
||||
void getWithPreparedActionWhenAlreadyRegisteredIgnoresRegistersAction() { |
||||
TestApplicationPreparedAction action1 = new TestApplicationPreparedAction(); |
||||
TestApplicationPreparedAction action2 = new TestApplicationPreparedAction(); |
||||
this.registy.get(Integer.class, this.counter::getAndIncrement, action1::run); |
||||
this.registy.get(Integer.class, this.counter::getAndIncrement, action2::run); |
||||
this.registy.applicationContextPrepared(this.context); |
||||
assertThat(action1).wasCalledOnlyOnce().hasInstanceValue(0); |
||||
assertThat(action2).wasNotCalled(); |
||||
} |
||||
|
||||
@Test |
||||
void registerAddsRegistration() { |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(registration.get()).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
void registerWhenAlreadyRegisteredReplacesPreviousRegistration() { |
||||
Registration<Integer> registration1 = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
Registration<Integer> registration2 = this.registy.register(Integer.class, () -> -1); |
||||
assertThat(registration2).isNotEqualTo(registration1); |
||||
assertThat(registration1.get()).isEqualTo(0); |
||||
assertThat(registration2.get()).isEqualTo(-1); |
||||
assertThat(this.registy.get(Integer.class, this.counter::getAndIncrement)).isEqualTo(-1); |
||||
} |
||||
|
||||
@Test |
||||
void isRegisteredWhenNotRegisteredReturnsFalse() { |
||||
assertThat(this.registy.isRegistered(Integer.class)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void isRegisteredWhenRegisteredReturnsTrue() { |
||||
this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(this.registy.isRegistered(Integer.class)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void getRegistrationWhenNotRegisteredReturnsNull() { |
||||
assertThat(this.registy.getRegistration(Integer.class)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void getRegistrationWhenRegisteredReturnsRegistration() { |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(this.registy.getRegistration(Integer.class)).isSameAs(registration); |
||||
} |
||||
|
||||
@Test |
||||
void applicationContextPreparedTriggersActions() { |
||||
TestApplicationPreparedAction action1 = new TestApplicationPreparedAction(); |
||||
TestApplicationPreparedAction action2 = new TestApplicationPreparedAction(); |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
registration.onApplicationContextPrepared(action1::run); |
||||
registration.onApplicationContextPrepared(action2::run); |
||||
this.registy.applicationContextPrepared(this.context); |
||||
assertThat(action1).wasCalledOnlyOnce().hasInstanceValue(0); |
||||
assertThat(action2).wasCalledOnlyOnce().hasInstanceValue(0); |
||||
} |
||||
|
||||
@Test |
||||
void registrationGetReturnsInstance() { |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(registration.get()).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
void registrationGetWhenCalledMultipleTimesReturnsSingleInstance() { |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
assertThat(registration.get()).isEqualTo(0); |
||||
assertThat(registration.get()).isEqualTo(0); |
||||
assertThat(registration.get()).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
void registrationOnApplicationContextPreparedAddsAction() { |
||||
TestApplicationPreparedAction action = new TestApplicationPreparedAction(); |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
registration.onApplicationContextPrepared(action::run); |
||||
this.registy.applicationContextPrepared(this.context); |
||||
assertThat(action).wasCalledOnlyOnce().hasInstanceValue(0); |
||||
} |
||||
|
||||
@Test |
||||
void registrationOnApplicationContextPreparedWhenActionIsNullDoesNotAddAction() { |
||||
Registration<Integer> registration = this.registy.register(Integer.class, this.counter::getAndIncrement); |
||||
registration.onApplicationContextPrepared(null); |
||||
this.registy.applicationContextPrepared(this.context); |
||||
} |
||||
|
||||
private static class TestApplicationPreparedAction implements AssertProvider<ApplicationPreparedActionAssert> { |
||||
|
||||
private Integer instance; |
||||
|
||||
private int called; |
||||
|
||||
void run(ConfigurableApplicationContext context, Integer instance) { |
||||
this.instance = instance; |
||||
this.called++; |
||||
} |
||||
|
||||
@Override |
||||
public ApplicationPreparedActionAssert assertThat() { |
||||
return new ApplicationPreparedActionAssert(this); |
||||
} |
||||
|
||||
} |
||||
|
||||
private static class ApplicationPreparedActionAssert |
||||
extends AbstractAssert<ApplicationPreparedActionAssert, TestApplicationPreparedAction> { |
||||
|
||||
ApplicationPreparedActionAssert(TestApplicationPreparedAction actual) { |
||||
super(actual, ApplicationPreparedActionAssert.class); |
||||
} |
||||
|
||||
ApplicationPreparedActionAssert hasInstanceValue(Integer expected) { |
||||
assertThat(this.actual.instance).isEqualTo(expected); |
||||
return this; |
||||
} |
||||
|
||||
ApplicationPreparedActionAssert wasCalledOnlyOnce() { |
||||
assertThat(this.actual.called).as("action calls").isEqualTo(1); |
||||
return this; |
||||
} |
||||
|
||||
ApplicationPreparedActionAssert wasNotCalled() { |
||||
assertThat(this.actual.called).as("action calls").isEqualTo(0); |
||||
return this; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue