Skip to content

Commit 9778fe0

Browse files
wilkinsonasnicoll
authored andcommitted
Introduce hooking of SpringApplication
Closes gh-
1 parent 258ae5e commit 9778fe0

File tree

3 files changed

+217
-10
lines changed

3 files changed

+217
-10
lines changed

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ private Class<?> deduceMainApplicationClass() {
288288
* @return a running {@link ApplicationContext}
289289
*/
290290
public ConfigurableApplicationContext run(String... args) {
291+
SpringApplicationHooks.hooks().preRun(this);
291292
long startTime = System.nanoTime();
292293
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
293294
ConfigurableApplicationContext context = null;
@@ -302,27 +303,32 @@ public ConfigurableApplicationContext run(String... args) {
302303
context = createApplicationContext();
303304
context.setApplicationStartup(this.applicationStartup);
304305
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
305-
refreshContext(context);
306-
afterRefresh(context, applicationArguments);
307-
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
308-
if (this.logStartupInfo) {
309-
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
306+
if (refreshContext(context)) {
307+
afterRefresh(context, applicationArguments);
308+
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
309+
if (this.logStartupInfo) {
310+
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(),
311+
timeTakenToStartup);
312+
}
313+
listeners.started(context, timeTakenToStartup);
314+
callRunners(context, applicationArguments);
310315
}
311-
listeners.started(context, timeTakenToStartup);
312-
callRunners(context, applicationArguments);
313316
}
314317
catch (Throwable ex) {
315318
handleRunFailure(context, ex, listeners);
316319
throw new IllegalStateException(ex);
317320
}
318321
try {
319-
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
320-
listeners.ready(context, timeTakenToReady);
322+
if (context.isRunning()) {
323+
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
324+
listeners.ready(context, timeTakenToReady);
325+
}
321326
}
322327
catch (Throwable ex) {
323328
handleRunFailure(context, ex, null);
324329
throw new IllegalStateException(ex);
325330
}
331+
SpringApplicationHooks.hooks().postRun(this, context);
326332
return context;
327333
}
328334

@@ -397,11 +403,15 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab
397403
listeners.contextLoaded(context);
398404
}
399405

400-
private void refreshContext(ConfigurableApplicationContext context) {
406+
private boolean refreshContext(ConfigurableApplicationContext context) {
407+
if (!SpringApplicationHooks.hooks().preRefresh(this, context)) {
408+
return false;
409+
}
401410
if (this.registerShutdownHook) {
402411
shutdownHook.registerApplicationContext(context);
403412
}
404413
refresh(context);
414+
return true;
405415
}
406416

407417
private void configureHeadlessProperty() {
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.context.ConfigurableApplicationContext;
23+
24+
/**
25+
* Low-level hooks that can observe a {@link SpringApplication} and modify its behavior.
26+
* Hooks are managed on a per-thread basis providing isolation when multiple applications
27+
* are executed in parallel.
28+
*
29+
* @author Andy Wilkinson
30+
*/
31+
final class SpringApplicationHooks {
32+
33+
private static final ThreadLocal<Hooks> hooks = ThreadLocal.withInitial(Hooks::new);
34+
35+
private SpringApplicationHooks() {
36+
37+
}
38+
39+
/**
40+
* Runs the given {@code action} with the given {@code hook} attached.
41+
* @param hook the hook to attach
42+
* @param action the action to run
43+
* @param <T> the type of the action's result
44+
* @return the result of the action
45+
* @throws Exception if a failure occurs while performing the action
46+
*/
47+
static <T> T withHook(Hook hook, Action<T> action) throws Exception {
48+
hooks.get().add(hook);
49+
try {
50+
return action.perform();
51+
}
52+
finally {
53+
hooks.get().remove(hook);
54+
}
55+
}
56+
57+
/**
58+
* Runs the given {@code action} with the given {@code hook} attached.
59+
* @param hook the hook to attach
60+
* @param action the action to run
61+
*/
62+
static void withHook(Hook hook, Runnable action) {
63+
hooks.get().add(hook);
64+
try {
65+
action.run();
66+
}
67+
finally {
68+
hooks.get().remove(hook);
69+
}
70+
}
71+
72+
static Hooks hooks() {
73+
return hooks.get();
74+
}
75+
76+
/**
77+
* A hook that can observe and modify the behavior of a {@link SpringApplication}.
78+
*/
79+
interface Hook {
80+
81+
/**
82+
* Called at the beginning of {@link SpringApplication#run(String...)}. Provides
83+
* an opportunity to inspect and customise the application.
84+
* @param application the application that is being run
85+
*/
86+
default void preRun(SpringApplication application) {
87+
88+
}
89+
90+
/**
91+
* Called at the end of {@link SpringApplication#run(String...)}. Provides access
92+
* to the {@link ConfigurableApplicationContext context} that has been created for
93+
* the application.
94+
* @param application the application that has been run
95+
* @param context the application's context
96+
*/
97+
default void postRun(SpringApplication application, ConfigurableApplicationContext context) {
98+
99+
}
100+
101+
/**
102+
* Called immediately before the given {@code context} is refreshed.
103+
* @param application the application for which the context is being refreshed
104+
* @param context the application's context
105+
* @return whether to continue with refresh processing
106+
*/
107+
default boolean preRefresh(SpringApplication application, ConfigurableApplicationContext context) {
108+
return true;
109+
}
110+
111+
}
112+
113+
/**
114+
* An action that can be performed with a hook attached.
115+
* <p>
116+
* <strong>For internal use only.</strong>
117+
*
118+
* @param <T> the type of the action's result
119+
*/
120+
interface Action<T> {
121+
122+
/**
123+
* Perform the action.
124+
* @return the result of the action
125+
* @throws Exception if a failure occurs
126+
*/
127+
T perform() throws Exception;
128+
129+
}
130+
131+
static final class Hooks implements Hook {
132+
133+
private final List<Hook> delegates = new ArrayList<>();
134+
135+
private void add(Hook hook) {
136+
this.delegates.add(hook);
137+
}
138+
139+
private void remove(Hook hook) {
140+
this.delegates.remove(hook);
141+
}
142+
143+
@Override
144+
public void preRun(SpringApplication application) {
145+
for (Hook delegate : this.delegates) {
146+
delegate.preRun(application);
147+
}
148+
}
149+
150+
@Override
151+
public void postRun(SpringApplication application, ConfigurableApplicationContext context) {
152+
for (Hook delegate : this.delegates) {
153+
delegate.postRun(application, context);
154+
}
155+
}
156+
157+
@Override
158+
public boolean preRefresh(SpringApplication application, ConfigurableApplicationContext context) {
159+
for (Hook delegate : this.delegates) {
160+
if (!delegate.preRefresh(application, context)) {
161+
return false;
162+
}
163+
}
164+
return true;
165+
}
166+
167+
}
168+
169+
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import org.springframework.beans.factory.support.BeanNameGenerator;
5555
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
5656
import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
57+
import org.springframework.boot.SpringApplicationHooks.Action;
58+
import org.springframework.boot.SpringApplicationHooks.Hook;
5759
import org.springframework.boot.availability.AvailabilityChangeEvent;
5860
import org.springframework.boot.availability.AvailabilityState;
5961
import org.springframework.boot.availability.LivenessState;
@@ -126,6 +128,7 @@
126128
import static org.mockito.ArgumentMatchers.any;
127129
import static org.mockito.ArgumentMatchers.anyString;
128130
import static org.mockito.ArgumentMatchers.argThat;
131+
import static org.mockito.ArgumentMatchers.eq;
129132
import static org.mockito.ArgumentMatchers.isA;
130133
import static org.mockito.BDDMockito.given;
131134
import static org.mockito.BDDMockito.then;
@@ -1262,6 +1265,31 @@ void deregistersShutdownHookForFailedApplicationContext() {
12621265
.didNotRegisterApplicationContext(failure.getApplicationContext());
12631266
}
12641267

1268+
@Test
1269+
void hookIsCalledWhenApplicationIsRun() throws Exception {
1270+
Hook hook = mock(Hook.class);
1271+
SpringApplication application = new SpringApplication(ExampleConfig.class);
1272+
application.setWebApplicationType(WebApplicationType.NONE);
1273+
given(hook.preRefresh(eq(application), any(ConfigurableApplicationContext.class))).willReturn(true);
1274+
this.context = SpringApplicationHooks.withHook(hook, (Action<ConfigurableApplicationContext>) application::run);
1275+
then(hook).should().preRun(application);
1276+
then(hook).should().preRefresh(application, this.context);
1277+
then(hook).should().postRun(application, this.context);
1278+
assertThat(this.context.isRunning()).isTrue();
1279+
}
1280+
1281+
@Test
1282+
void hookIsCalledAndCanPreventRefreshWhenApplicationIsRun() throws Exception {
1283+
Hook hook = mock(Hook.class);
1284+
SpringApplication application = new SpringApplication(ExampleConfig.class);
1285+
application.setWebApplicationType(WebApplicationType.NONE);
1286+
this.context = SpringApplicationHooks.withHook(hook, (Action<ConfigurableApplicationContext>) application::run);
1287+
then(hook).should().preRun(application);
1288+
then(hook).should().preRefresh(application, this.context);
1289+
then(hook).should().postRun(application, this.context);
1290+
assertThat(this.context.isRunning()).isFalse();
1291+
}
1292+
12651293
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
12661294
S state) {
12671295
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)

0 commit comments

Comments
 (0)