Skip to content

Commit 8a67ab0

Browse files
committed
Merge branch '2.4.x'
Closes gh-24569
2 parents 0734806 + 5317d8a commit 8a67ab0

File tree

5 files changed

+119
-8
lines changed

5 files changed

+119
-8
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.context.ApplicationContext;
2222
import org.springframework.context.ApplicationListener;
2323
import org.springframework.core.env.Environment;
24+
import org.springframework.util.Assert;
2425

2526
/**
2627
* A simple object registry that is available during startup and {@link Environment}
@@ -47,7 +48,8 @@ public interface BootstrapRegistry {
4748

4849
/**
4950
* Register a specific type with the registry. If the specified type has already been
50-
* registered, but not get obtained, it will be replaced.
51+
* registered and has not been obtained as a {@link Scope#SINGLETON singleton}, it
52+
* will be replaced.
5153
* @param <T> the instance type
5254
* @param type the instance type
5355
* @param instanceSupplier the instance supplier
@@ -87,11 +89,13 @@ public interface BootstrapRegistry {
8789
void addCloseListener(ApplicationListener<BootstrapContextClosedEvent> listener);
8890

8991
/**
90-
* Supplier used to provide the actual instance the first time it is accessed.
92+
* Supplier used to provide the actual instance when needed.
9193
*
9294
* @param <T> the instance type
95+
* @see Scope
9396
*/
94-
public interface InstanceSupplier<T> {
97+
@FunctionalInterface
98+
interface InstanceSupplier<T> {
9599

96100
/**
97101
* Factory method used to create the instance when needed.
@@ -101,6 +105,39 @@ public interface InstanceSupplier<T> {
101105
*/
102106
T get(BootstrapContext context);
103107

108+
/**
109+
* Return the scope of the supplied instance.
110+
* @return the scope
111+
* @since 2.4.2
112+
*/
113+
default Scope getScope() {
114+
return Scope.SINGLETON;
115+
}
116+
117+
/**
118+
* Return a new {@link InstanceSupplier} with an updated {@link Scope}.
119+
* @param scope the new scope
120+
* @return a new {@link InstanceSupplier} instance with the new scope
121+
* @since 2.4.2
122+
*/
123+
default InstanceSupplier<T> withScope(Scope scope) {
124+
Assert.notNull(scope, "Scope must not be null");
125+
InstanceSupplier<T> parent = this;
126+
return new InstanceSupplier<T>() {
127+
128+
@Override
129+
public T get(BootstrapContext context) {
130+
return parent.get(context);
131+
}
132+
133+
@Override
134+
public Scope getScope() {
135+
return scope;
136+
}
137+
138+
};
139+
}
140+
104141
/**
105142
* Factory method that can be used to create a {@link InstanceSupplier} for a
106143
* given instance.
@@ -125,4 +162,24 @@ static <T> InstanceSupplier<T> from(Supplier<T> supplier) {
125162

126163
}
127164

165+
/**
166+
* The scope of a instance.
167+
* @since 2.4.2
168+
*/
169+
enum Scope {
170+
171+
/**
172+
* A singleton instance. The {@link InstanceSupplier} will be called only once and
173+
* the same instance will be returned each time.
174+
*/
175+
SINGLETON,
176+
177+
/**
178+
* A prototype instance. The {@link InstanceSupplier} will be called whenver an
179+
* instance is needed.
180+
*/
181+
PROTOTYPE
182+
183+
}
184+
128185
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ private <T> T getInstance(Class<T> type, InstanceSupplier<?> instanceSupplier) {
117117
T instance = (T) this.instances.get(type);
118118
if (instance == null) {
119119
instance = (T) instanceSupplier.get(this);
120-
this.instances.put(type, instance);
120+
if (instanceSupplier.getScope() == Scope.SINGLETON) {
121+
this.instances.put(type, instance);
122+
}
121123
}
122124
return instance;
123125
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import java.util.LinkedHashSet;
2323
import java.util.List;
2424
import java.util.Set;
25+
import java.util.function.Supplier;
2526

2627
import org.apache.commons.logging.Log;
2728

2829
import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
30+
import org.springframework.boot.BootstrapRegistry.Scope;
2931
import org.springframework.boot.ConfigurableBootstrapContext;
3032
import org.springframework.boot.DefaultPropertiesPropertySource;
3133
import org.springframework.boot.context.config.ConfigDataEnvironmentContributors.BinderOption;
@@ -220,18 +222,21 @@ private ConfigDataEnvironmentContributor createInitialImportContributor(ConfigDa
220222
void processAndApply() {
221223
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
222224
this.loaders);
223-
this.bootstrapContext.register(Binder.class, InstanceSupplier
224-
.from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)));
225+
registerBootstrapBinder(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
225226
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
226227
Binder initialBinder = contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
227-
this.bootstrapContext.register(Binder.class, InstanceSupplier.of(initialBinder));
228+
registerBootstrapBinder(() -> initialBinder);
228229
ConfigDataActivationContext activationContext = createActivationContext(initialBinder);
229230
contributors = processWithoutProfiles(contributors, importer, activationContext);
230231
activationContext = withProfiles(contributors, activationContext);
231232
contributors = processWithProfiles(contributors, importer, activationContext);
232233
applyToEnvironment(contributors, activationContext);
233234
}
234235

236+
private void registerBootstrapBinder(Supplier<Binder> supplier) {
237+
this.bootstrapContext.register(Binder.class, InstanceSupplier.from(supplier).withScope(Scope.PROTOTYPE));
238+
}
239+
235240
private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors,
236241
ConfigDataImporter importer) {
237242
this.logger.trace("Processing initial config data environment contributors without activation context");

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultBootstrapContextTests.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.junit.jupiter.api.Test;
2525

2626
import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
27+
import org.springframework.boot.BootstrapRegistry.Scope;
2728
import org.springframework.context.ApplicationContext;
2829
import org.springframework.context.ApplicationListener;
2930
import org.springframework.context.ConfigurableApplicationContext;
@@ -74,6 +75,24 @@ void registerWhenAlreadyRegisteredRegistersReplacedInstance() {
7475
assertThat(this.context.get(Integer.class)).isEqualTo(100);
7576
}
7677

78+
@Test
79+
void registerWhenSingletonAlreadyCreatedThrowsException() {
80+
this.context.register(Integer.class, InstanceSupplier.from(this.counter::getAndIncrement));
81+
this.context.get(Integer.class);
82+
assertThatIllegalStateException()
83+
.isThrownBy(() -> this.context.register(Integer.class, InstanceSupplier.of(100)))
84+
.withMessage("java.lang.Integer has already been created");
85+
}
86+
87+
@Test
88+
void registerWhenPrototypeAlreadyCreatedReplacesInstance() {
89+
this.context.register(Integer.class,
90+
InstanceSupplier.from(this.counter::getAndIncrement).withScope(Scope.PROTOTYPE));
91+
this.context.get(Integer.class);
92+
this.context.register(Integer.class, InstanceSupplier.of(100));
93+
assertThat(this.context.get(Integer.class)).isEqualTo(100);
94+
}
95+
7796
@Test
7897
void registerWhenAlreadyCreatedThrowsException() {
7998
this.context.register(Integer.class, InstanceSupplier.from(this.counter::getAndIncrement));
@@ -146,12 +165,25 @@ void getWhenRegisteredAsNullReturnsNull() {
146165
}
147166

148167
@Test
149-
void getCreatesOnlyOneInstance() {
168+
void getWhenSingletonCreatesOnlyOneInstance() {
150169
this.context.register(Integer.class, InstanceSupplier.from(this.counter::getAndIncrement));
151170
assertThat(this.context.get(Integer.class)).isEqualTo(0);
152171
assertThat(this.context.get(Integer.class)).isEqualTo(0);
153172
}
154173

174+
@Test
175+
void getWhenPrototypeCreatesOnlyNewInstances() {
176+
this.context.register(Integer.class,
177+
InstanceSupplier.from(this.counter::getAndIncrement).withScope(Scope.PROTOTYPE));
178+
assertThat(this.context.get(Integer.class)).isEqualTo(0);
179+
assertThat(this.context.get(Integer.class)).isEqualTo(1);
180+
}
181+
182+
@Test
183+
void testName() {
184+
185+
}
186+
155187
@Test
156188
void getOrElseWhenNoRegistrationReturnsOther() {
157189
this.context.register(Number.class, InstanceSupplier.of(1));
@@ -228,6 +260,20 @@ void addCloseListenerIgnoresMultipleCallsWithSameListener() {
228260
assertThat(listener).wasCalledOnlyOnce();
229261
}
230262

263+
@Test
264+
void instanceSupplierGetScopeWhenNotConfiguredReturnsSingleton() {
265+
InstanceSupplier<String> supplier = InstanceSupplier.of("test");
266+
assertThat(supplier.getScope()).isEqualTo(Scope.SINGLETON);
267+
assertThat(supplier.get(null)).isEqualTo("test");
268+
}
269+
270+
@Test
271+
void instanceSupplierWithScopeChangesScope() {
272+
InstanceSupplier<String> supplier = InstanceSupplier.of("test").withScope(Scope.PROTOTYPE);
273+
assertThat(supplier.getScope()).isEqualTo(Scope.PROTOTYPE);
274+
assertThat(supplier.get(null)).isEqualTo("test");
275+
}
276+
231277
private static class TestCloseListener
232278
implements ApplicationListener<BootstrapContextClosedEvent>, AssertProvider<CloseListenerAssert> {
233279

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ static class LocationResolver implements ConfigDataLocationResolver<Resource> {
4141

4242
@Override
4343
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
44+
context.getBootstrapContext().get(Binder.class); // gh-24559
4445
return location.hasPrefix("testbootstrap:");
4546
}
4647

0 commit comments

Comments
 (0)