From d3957dfa3eeb09f5b21a5e5da20a1b5e3d0b3a64 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 12 Sep 2022 19:09:54 -0700 Subject: [PATCH] Provide a public API for SpringApplication hooks Create a new public SpringApplication Hook API based on the existing `SpringApplicationRunListener` interface. The previous package-private `SpringApplicationHooks` class has been replaced with a public `SpringApplicationHook` interface which acts as a factory that can create additional `SpringApplicationRunListener` instances to hook in. The boolean result from the previous `preRefresh` method has been replaced with an `AbandonedRunException` which can be thrown from the `SpringApplicationRunListener`. Closes gh-32301 --- .../springframework/boot/AotProcessor.java | 83 ++++------ .../boot/SpringApplication.java | 120 +++++++++++--- .../boot/SpringApplicationHook.java | 41 +++++ .../boot/SpringApplicationHooks.java | 149 ------------------ .../boot/SpringApplicationRunListeners.java | 8 +- .../boot/AotProcessorTests.java | 3 +- .../boot/SpringApplicationTests.java | 55 ++++--- 7 files changed, 217 insertions(+), 242 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHook.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHooks.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/AotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/AotProcessor.java index c7b9b46fdc..1dee6b53a8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/AotProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/AotProcessor.java @@ -17,7 +17,7 @@ package org.springframework.boot; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -35,7 +35,7 @@ import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.nativex.FileNativeConfigurationWriter; -import org.springframework.boot.SpringApplicationHooks.Hook; +import org.springframework.boot.SpringApplication.AbandonedRunException; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.aot.ApplicationContextAotGenerator; @@ -43,6 +43,8 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.javapoet.ClassName; import org.springframework.util.Assert; import org.springframework.util.FileSystemUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingSupplier; /** * Entry point for AOT processing of a {@link SpringApplication}. @@ -98,11 +100,10 @@ public class AotProcessor { */ public void process() { deleteExistingOutput(); - AotProcessorHook hook = new AotProcessorHook(); - SpringApplicationHooks.withHook(hook, this::callApplicationMainMethod); - GenericApplicationContext applicationContext = hook.getApplicationContext(); - Assert.notNull(applicationContext, "No application context available after calling main method of '" - + this.application.getName() + "'. Does it run a SpringApplication?"); + GenericApplicationContext applicationContext = new AotProcessorHook().run(() -> { + Method mainMethod = this.application.getMethod("main", String[].class); + return ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { this.applicationArgs }); + }); performAotProcessing(applicationContext); } @@ -121,22 +122,6 @@ public class AotProcessor { } } - private void callApplicationMainMethod() { - try { - this.application.getMethod("main", String[].class).invoke(null, new Object[] { this.applicationArgs }); - } - catch (InvocationTargetException ex) { - Throwable targetException = ex.getTargetException(); - if (!(targetException instanceof MainMethodSilentExitException)) { - throw (targetException instanceof RuntimeException runtimeEx) ? runtimeEx - : new RuntimeException(targetException); - } - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - private void performAotProcessing(GenericApplicationContext applicationContext) { FileSystemGeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this::getRoot); DefaultGenerationContext generationContext = new DefaultGenerationContext( @@ -214,39 +199,39 @@ public class AotProcessor { } /** - * Hook used to capture the {@link ApplicationContext} and trigger early exit of main - * method. + * {@link SpringApplicationHook} used to capture the {@link ApplicationContext} and + * trigger early exit of main method. */ - private static class AotProcessorHook implements Hook { - - private GenericApplicationContext context; + private class AotProcessorHook implements SpringApplicationHook { @Override - public boolean preRefresh(SpringApplication application, ConfigurableApplicationContext context) { - Assert.isInstanceOf(GenericApplicationContext.class, context, - () -> "AOT processing requires a GenericApplicationContext but got a " - + context.getClass().getName()); - this.context = (GenericApplicationContext) context; - return false; + public SpringApplicationRunListener getRunListener(SpringApplication application) { + return new SpringApplicationRunListener() { + + @Override + public void contextLoaded(ConfigurableApplicationContext context) { + throw new AbandonedRunException(context); + } + + }; } - @Override - public void postRun(SpringApplication application, ConfigurableApplicationContext context) { - throw new MainMethodSilentExitException(); + private GenericApplicationContext run(ThrowingSupplier action) { + try { + SpringApplication.withHook(this, action); + } + catch (AbandonedRunException ex) { + ApplicationContext context = ex.getApplicationContext(); + Assert.isInstanceOf(GenericApplicationContext.class, context, + () -> "AOT processing requires a GenericApplicationContext but got a " + + context.getClass().getName()); + return (GenericApplicationContext) context; + } + throw new IllegalStateException( + "No application context available after calling main method of '%s'. Does it run a SpringApplication?" + .formatted(AotProcessor.this.application.getName())); } - GenericApplicationContext getApplicationContext() { - return this.context; - } - - } - - /** - * Internal exception used to prevent main method to continue once - * {@code SpringApplication#run} completes. - */ - private static class MainMethodSilentExitException extends RuntimeException { - } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 89426423e8..fc4f28edb7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -93,6 +93,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; +import org.springframework.util.function.ThrowingSupplier; /** * Class that can be used to bootstrap and launch a Spring application from a Java main @@ -188,6 +189,8 @@ public class SpringApplication { static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook(); + private static final ThreadLocal applicationHook = new ThreadLocal<>(); + private Set> primarySources; private Set sources = new LinkedHashSet<>(); @@ -294,7 +297,6 @@ public class SpringApplication { * @return a running {@link ApplicationContext} */ public ConfigurableApplicationContext run(String... args) { - SpringApplicationHooks.hooks().preRun(this); long startTime = System.nanoTime(); DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null; @@ -309,18 +311,19 @@ public class SpringApplication { context = createApplicationContext(); context.setApplicationStartup(this.applicationStartup); prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); - if (refreshContext(context)) { - afterRefresh(context, applicationArguments); - Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); - if (this.logStartupInfo) { - new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), - timeTakenToStartup); - } - listeners.started(context, timeTakenToStartup); - callRunners(context, applicationArguments); + refreshContext(context); + afterRefresh(context, applicationArguments); + Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); + if (this.logStartupInfo) { + new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup); } + listeners.started(context, timeTakenToStartup); + callRunners(context, applicationArguments); } catch (Throwable ex) { + if (ex instanceof AbandonedRunException) { + throw ex; + } handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); } @@ -331,10 +334,12 @@ public class SpringApplication { } } catch (Throwable ex) { + if (ex instanceof AbandonedRunException) { + throw ex; + } handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } - SpringApplicationHooks.hooks().postRun(this, context); return context; } @@ -420,15 +425,11 @@ public class SpringApplication { } } - private boolean refreshContext(ConfigurableApplicationContext context) { - if (!SpringApplicationHooks.hooks().preRefresh(this, context)) { - return false; - } + private void refreshContext(ConfigurableApplicationContext context) { if (this.registerShutdownHook) { shutdownHook.registerApplicationContext(context); } refresh(context); - return true; } private void configureHeadlessProperty() { @@ -439,16 +440,22 @@ public class SpringApplication { private SpringApplicationRunListeners getRunListeners(String[] args) { ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this); argumentResolver = argumentResolver.and(String[].class, args); - Collection listeners = getSpringFactoriesInstances( - SpringApplicationRunListener.class, argumentResolver); + List listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class, + argumentResolver); + SpringApplicationHook hook = applicationHook.get(); + SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null; + if (hookListener != null) { + listeners = new ArrayList<>(listeners); + listeners.add(0, hookListener); + } return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup); } - private Collection getSpringFactoriesInstances(Class type) { + private List getSpringFactoriesInstances(Class type) { return getSpringFactoriesInstances(type, null); } - private Collection getSpringFactoriesInstances(Class type, ArgumentResolver argumentResolver) { + private List getSpringFactoriesInstances(Class type, ArgumentResolver argumentResolver) { return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver); } @@ -1359,6 +1366,41 @@ public class SpringApplication { return exitCode; } + /** + * Perform the given action with the given {@link SpringApplicationHook} attached if + * the action triggers an {@link SpringApplication#run(String...) application run}. + * @param hook the hook to apply + * @param action the action to run + * @since 3.0.0 + * @see #withHook(SpringApplicationHook, ThrowingSupplier) + */ + public static void withHook(SpringApplicationHook hook, Runnable action) { + withHook(hook, () -> { + action.run(); + return null; + }); + } + + /** + * Perform the given action with the given {@link SpringApplicationHook} attached if + * the action triggers an {@link SpringApplication#run(String...) application run}. + * @param the result type + * @param hook the hook to apply + * @param action the action to run + * @return the result of the action + * @since 3.0.0 + * @see #withHook(SpringApplicationHook, Runnable) + */ + public static T withHook(SpringApplicationHook hook, ThrowingSupplier action) { + applicationHook.set(hook); + try { + return action.get(); + } + finally { + applicationHook.set(null); + } + } + private static void close(ApplicationContext context) { if (context instanceof ConfigurableApplicationContext closable) { closable.close(); @@ -1405,4 +1447,42 @@ public class SpringApplication { } + /** + * Exception that can be thrown to silently exit a running {@link SpringApplication} + * without handling run failures. + * + * @since 3.0.0 + */ + public static class AbandonedRunException extends RuntimeException { + + private final ConfigurableApplicationContext applicationContext; + + /** + * Create a new {@link AbandonedRunException} instance. + */ + public AbandonedRunException() { + this(null); + } + + /** + * Create a new {@link AbandonedRunException} instance with the given application + * context. + * @param applicationContext the application context that was available when the + * run was abandoned + */ + public AbandonedRunException(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Return the application context that was available when the run was abandoned or + * {@code null} if no context was available. + * @return the application context + */ + public ConfigurableApplicationContext getApplicationContext() { + return this.applicationContext; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHook.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHook.java new file mode 100644 index 0000000000..b94f8c56b3 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHook.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2022 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; + +/** + * Low-level hook that can be used to attach a {@link SpringApplicationRunListener} to a + * {@link SpringApplication} in order to observe or modify its behavior. Hooks are managed + * on a per-thread basis providing isolation when multiple applications are executed in + * parallel. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.0.0 + * @see SpringApplication#withHook + */ +@FunctionalInterface +public interface SpringApplicationHook { + + /** + * Return the {@link SpringApplicationRunListener} that should be hooked into the + * given {@link SpringApplication}. + * @param springApplication the source {@link SpringApplication} instance + * @return the {@link SpringApplicationRunListener} to attach + */ + SpringApplicationRunListener getRunListener(SpringApplication springApplication); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHooks.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHooks.java deleted file mode 100644 index c245388491..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationHooks.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2012-2022 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; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.util.function.ThrowingSupplier; - -/** - * Low-level hooks that can observe a {@link SpringApplication} and modify its behavior. - * Hooks are managed on a per-thread basis providing isolation when multiple applications - * are executed in parallel. - * - * @author Andy Wilkinson - */ -final class SpringApplicationHooks { - - private static final ThreadLocal hooks = ThreadLocal.withInitial(Hooks::new); - - private SpringApplicationHooks() { - } - - /** - * Runs the given {@code action} with the given {@code hook} attached. - * @param hook the hook to attach - * @param action the action to run - * @param the type of the action's result - * @return the result of the action - * @throws Exception if a failure occurs while performing the action - */ - static T withHook(Hook hook, ThrowingSupplier action) throws Exception { - hooks.get().add(hook); - try { - return action.getWithException(); - } - finally { - hooks.get().remove(hook); - } - } - - /** - * Runs the given {@code action} with the given {@code hook} attached. - * @param hook the hook to attach - * @param action the action to run - */ - static void withHook(Hook hook, Runnable action) { - hooks.get().add(hook); - try { - action.run(); - } - finally { - hooks.get().remove(hook); - } - } - - static Hooks hooks() { - return hooks.get(); - } - - /** - * A hook that can observe and modify the behavior of a {@link SpringApplication}. - */ - interface Hook { - - /** - * Called at the beginning of {@link SpringApplication#run(String...)}. Provides - * an opportunity to inspect and customise the application. - * @param application the application that is being run - */ - default void preRun(SpringApplication application) { - } - - /** - * Called at the end of {@link SpringApplication#run(String...)}. Provides access - * to the {@link ConfigurableApplicationContext context} that has been created for - * the application. - * @param application the application that has been run - * @param context the application's context - */ - default void postRun(SpringApplication application, ConfigurableApplicationContext context) { - } - - /** - * Called immediately before the given {@code context} is refreshed. - * @param application the application for which the context is being refreshed - * @param context the application's context - * @return whether to continue with refresh processing - */ - default boolean preRefresh(SpringApplication application, ConfigurableApplicationContext context) { - return true; - } - - } - - static final class Hooks implements Hook { - - private final List delegates = new ArrayList<>(); - - private void add(Hook hook) { - this.delegates.add(hook); - } - - private void remove(Hook hook) { - this.delegates.remove(hook); - } - - @Override - public void preRun(SpringApplication application) { - for (Hook delegate : this.delegates) { - delegate.preRun(application); - } - } - - @Override - public void postRun(SpringApplication application, ConfigurableApplicationContext context) { - for (Hook delegate : this.delegates) { - delegate.postRun(application, context); - } - } - - @Override - public boolean preRefresh(SpringApplication application, ConfigurableApplicationContext context) { - for (Hook delegate : this.delegates) { - if (!delegate.preRefresh(application, context)) { - return false; - } - } - return true; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java index 74e5375cf4..b6174c5bb0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -17,8 +17,6 @@ package org.springframework.boot; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.function.Consumer; @@ -45,10 +43,10 @@ class SpringApplicationRunListeners { private final ApplicationStartup applicationStartup; - SpringApplicationRunListeners(Log log, Collection listeners, + SpringApplicationRunListeners(Log log, List listeners, ApplicationStartup applicationStartup) { this.log = log; - this.listeners = new ArrayList<>(listeners); + this.listeners = List.copyOf(listeners); this.applicationStartup = applicationStartup; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AotProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AotProcessorTests.java index fe60cbc29f..9175b01f56 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AotProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AotProcessorTests.java @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link AotProcessor}. @@ -59,7 +60,7 @@ class AotProcessorTests { void processApplicationWithMainMethodThatDoesNotRun(@TempDir Path directory) { AotProcessor processor = new AotProcessor(BrokenApplication.class, new String[0], directory.resolve("source"), directory.resolve("resource"), directory.resolve("class"), "com.example", "example"); - assertThatIllegalArgumentException().isThrownBy(processor::process) + assertThatIllegalStateException().isThrownBy(processor::process) .withMessageContaining("Does it run a SpringApplication?"); assertThat(directory).isEmptyDirectory(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index eabbf1164f..70e4727641 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -56,7 +56,6 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.boot.BootstrapRegistry.InstanceSupplier; import org.springframework.boot.SpringApplication.SpringApplicationRuntimeHints; -import org.springframework.boot.SpringApplicationHooks.Hook; import org.springframework.boot.availability.AvailabilityChangeEvent; import org.springframework.boot.availability.AvailabilityState; import org.springframework.boot.availability.LivenessState; @@ -117,7 +116,6 @@ import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import org.springframework.util.function.ThrowingSupplier; import org.springframework.web.context.ConfigurableWebEnvironment; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.StandardServletEnvironment; @@ -1278,30 +1276,51 @@ class SpringApplicationTests { } @Test - void hookIsCalledWhenApplicationIsRun() throws Exception { - Hook hook = mock(Hook.class); + void withRunnableHookRunsWithHook() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - given(hook.preRefresh(eq(application), any(ConfigurableApplicationContext.class))).willReturn(true); - this.context = SpringApplicationHooks.withHook(hook, - (ThrowingSupplier) application::run); - then(hook).should().preRun(application); - then(hook).should().preRefresh(application, this.context); - then(hook).should().postRun(application, this.context); + SpringApplicationRunListener runListener = mock(SpringApplicationRunListener.class); + SpringApplicationHook hook = (springApplication) -> runListener; + SpringApplication.withHook(hook, () -> this.context = application.run()); + then(runListener).should().starting(any()); + then(runListener).should().contextPrepared(this.context); + then(runListener).should().ready(eq(this.context), any()); assertThat(this.context.isRunning()).isTrue(); } @Test - void hookIsCalledAndCanPreventRefreshWhenApplicationIsRun() throws Exception { - Hook hook = mock(Hook.class); + void withCallableHookRunsWithHook() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - this.context = SpringApplicationHooks.withHook(hook, - (ThrowingSupplier) application::run); - then(hook).should().preRun(application); - then(hook).should().preRefresh(application, this.context); - then(hook).should().postRun(application, this.context); - assertThat(this.context.isRunning()).isFalse(); + SpringApplicationRunListener runListener = mock(SpringApplicationRunListener.class); + SpringApplicationHook hook = (springApplication) -> runListener; + this.context = SpringApplication.withHook(hook, () -> application.run()); + then(runListener).should().starting(any()); + then(runListener).should().contextPrepared(this.context); + then(runListener).should().ready(eq(this.context), any()); + assertThat(this.context.isRunning()).isTrue(); + } + + @Test + void withHookWhenHookThrowsAbandonedRunExceptionAbandonsRun() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + SpringApplicationRunListener runListener = spy(new SpringApplicationRunListener() { + + @Override + public void contextLoaded(ConfigurableApplicationContext context) { + throw new SpringApplication.AbandonedRunException(context); + } + + }); + SpringApplicationHook hook = (springApplication) -> runListener; + assertThatExceptionOfType(SpringApplication.AbandonedRunException.class) + .isThrownBy(() -> SpringApplication.withHook(hook, () -> application.run())) + .satisfies((ex) -> assertThat(ex.getApplicationContext().isRunning()).isFalse()); + then(runListener).should().starting(any()); + then(runListener).should().contextPrepared(any()); + then(runListener).should(never()).ready(any(), any()); + then(runListener).should(never()).failed(any(), any()); } @Test