Browse Source

Introduce ConfigurableApplicationContext.pause() and SmartLifecycle.isPauseable()

Closes gh-35269
pull/34146/merge
Juergen Hoeller 5 months ago
parent
commit
149d468ce4
  1. 1
      spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java
  2. 15
      spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java
  3. 9
      spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java
  4. 25
      spring-context/src/main/java/org/springframework/context/SmartLifecycle.java
  5. 46
      spring-context/src/main/java/org/springframework/context/event/ContextPausedEvent.java
  6. 4
      spring-context/src/main/java/org/springframework/context/event/ContextRestartedEvent.java
  7. 7
      spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java
  8. 65
      spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java
  9. 26
      spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java
  10. 6
      spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java
  11. 2
      spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
  12. 38
      spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java

1
spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java

@ -391,7 +391,6 @@ class QuartzSupportTests {
try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class));
assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse();
ctx.stop();
ctx.restart(); ctx.restart();
} }
} }

15
spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java

@ -221,16 +221,27 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life
void refresh() throws BeansException, IllegalStateException; void refresh() throws BeansException, IllegalStateException;
/** /**
* Stop all beans in this application context if necessary, and subsequently * Pause all beans in this application context if necessary, and subsequently
* restart all auto-startup beans, effectively restoring the lifecycle state * restart all auto-startup beans, effectively restoring the lifecycle state
* after {@link #refresh()} (typically after a preceding {@link #stop()} call * after {@link #refresh()} (typically after a preceding {@link #stop()} call
* when a full {@link #start()} of even lazy-starting beans is to be avoided). * when a full {@link #start()} of even lazy-starting beans is to be avoided).
* @since 7.0 * @since 7.0
* @see #stop() * @see #pause()
* @see #start()
* @see SmartLifecycle#isAutoStartup() * @see SmartLifecycle#isAutoStartup()
*/ */
void restart(); void restart();
/**
* Stop all beans in this application context unless they explicitly opt out of
* pausing through {@link SmartLifecycle#isPauseable()} returning {@code false}.
* @since 7.0
* @see #restart()
* @see #stop()
* @see SmartLifecycle#isPauseable()
*/
void pause();
/** /**
* Register a shutdown hook with the JVM runtime, closing this context * Register a shutdown hook with the JVM runtime, closing this context
* on JVM shutdown unless it has already been closed at that time. * on JVM shutdown unless it has already been closed at that time.

9
spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java

@ -44,6 +44,15 @@ public interface LifecycleProcessor extends Lifecycle {
start(); start();
} }
/**
* Notification of context pause for auto-stopping components.
* @since 7.0
* @see ConfigurableApplicationContext#pause()
*/
default void onPause() {
stop();
}
/** /**
* Notification of context close phase for auto-stopping components * Notification of context close phase for auto-stopping components
* before destruction. * before destruction.

25
spring-context/src/main/java/org/springframework/context/SmartLifecycle.java

@ -85,7 +85,7 @@ public interface SmartLifecycle extends Lifecycle, Phased {
/** /**
* Returns {@code true} if this {@code Lifecycle} component should get * Returns {@code true} if this {@code Lifecycle} component should get
* started automatically by the container at the time that the containing * started automatically by the container at the time that the containing
* {@link ApplicationContext} gets refreshed. * {@link ApplicationContext} gets refreshed or restarted.
* <p>A value of {@code false} indicates that the component is intended to * <p>A value of {@code false} indicates that the component is intended to
* be started through an explicit {@link #start()} call instead, analogous * be started through an explicit {@link #start()} call instead, analogous
* to a plain {@link Lifecycle} implementation. * to a plain {@link Lifecycle} implementation.
@ -93,12 +93,35 @@ public interface SmartLifecycle extends Lifecycle, Phased {
* @see #start() * @see #start()
* @see #getPhase() * @see #getPhase()
* @see LifecycleProcessor#onRefresh() * @see LifecycleProcessor#onRefresh()
* @see LifecycleProcessor#onRestart()
* @see ConfigurableApplicationContext#refresh() * @see ConfigurableApplicationContext#refresh()
* @see ConfigurableApplicationContext#restart()
*/ */
default boolean isAutoStartup() { default boolean isAutoStartup() {
return true; return true;
} }
/**
* Returns {@code true} if this {@code Lifecycle} component is able to
* participate in a restart sequence, receiving corresponding {@link #stop()}
* and {@link #start()} calls with a potential pause in-between.
* <p>A value of {@code false} indicates that the component prefers to
* be skipped in a pause scenario, neither receiving a {@link #stop()}
* call nor a subsequent {@link #start()} call, analogous to a plain
* {@link Lifecycle} implementation. It will only receive a {@link #stop()}
* call on close and on explicit context-wide stopping but not on pause.
* <p>The default implementation returns {@code true}.
* @since 7.0
* @see #stop()
* @see LifecycleProcessor#onPause()
* @see LifecycleProcessor#onClose()
* @see ConfigurableApplicationContext#pause()
* @see ConfigurableApplicationContext#close()
*/
default boolean isPauseable() {
return true;
}
/** /**
* Indicates that a Lifecycle component must stop if it is currently running. * Indicates that a Lifecycle component must stop if it is currently running.
* <p>The provided callback is used by the {@link LifecycleProcessor} to support * <p>The provided callback is used by the {@link LifecycleProcessor} to support

46
spring-context/src/main/java/org/springframework/context/event/ContextPausedEvent.java

@ -0,0 +1,46 @@
/*
* Copyright 2002-present 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.context.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Event raised when an {@code ApplicationContext} gets paused.
*
* <p>Note that {@code ContextPausedEvent} is a specialization of
* {@link ContextStoppedEvent}.
*
* @author Juergen Hoeller
* @since 7.0
* @see ConfigurableApplicationContext#pause()
* @see ContextRestartedEvent
* @see ContextStoppedEvent
*/
@SuppressWarnings("serial")
public class ContextPausedEvent extends ContextStoppedEvent {
/**
* Create a new {@code ContextRestartedEvent}.
* @param source the {@code ContextPausedEvent} that has been restarted
* (must not be {@code null})
*/
public ContextPausedEvent(ApplicationContext source) {
super(source);
}
}

4
spring-context/src/main/java/org/springframework/context/event/ContextRestartedEvent.java

@ -17,6 +17,7 @@
package org.springframework.context.event; package org.springframework.context.event;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
/** /**
* Event raised when an {@code ApplicationContext} gets restarted. * Event raised when an {@code ApplicationContext} gets restarted.
@ -26,8 +27,9 @@ import org.springframework.context.ApplicationContext;
* *
* @author Sam Brannen * @author Sam Brannen
* @since 7.0 * @since 7.0
* @see ConfigurableApplicationContext#restart()
* @see ContextPausedEvent
* @see ContextStartedEvent * @see ContextStartedEvent
* @see ContextStoppedEvent
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class ContextRestartedEvent extends ContextStartedEvent { public class ContextRestartedEvent extends ContextStartedEvent {

7
spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java

@ -66,6 +66,7 @@ import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.ResourceLoaderAware; import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextPausedEvent;
import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextRestartedEvent; import org.springframework.context.event.ContextRestartedEvent;
import org.springframework.context.event.ContextStartedEvent; import org.springframework.context.event.ContextStartedEvent;
@ -1555,6 +1556,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
publishEvent(new ContextRestartedEvent(this)); publishEvent(new ContextRestartedEvent(this));
} }
@Override
public void pause() {
getLifecycleProcessor().onPause();
publishEvent(new ContextPausedEvent(this));
}
@Override @Override
public boolean isRunning() { public boolean isRunning() {
return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()); return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning());

65
spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java

@ -287,7 +287,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
*/ */
@Override @Override
public void stop() { public void stop() {
stopBeans(); stopBeans(false);
this.running = false; this.running = false;
} }
@ -308,7 +308,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
catch (ApplicationContextException ex) { catch (ApplicationContextException ex) {
// Some bean failed to auto-start within context refresh: // Some bean failed to auto-start within context refresh:
// stop already started beans on context refresh failure. // stop already started beans on context refresh failure.
stopBeans(); stopBeans(false);
throw ex; throw ex;
} }
this.running = true; this.running = true;
@ -318,15 +318,23 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
public void onRestart() { public void onRestart() {
this.stoppedBeans = null; this.stoppedBeans = null;
if (this.running) { if (this.running) {
stopBeans(); stopBeans(true);
} }
startBeans(true); startBeans(true);
this.running = true; this.running = true;
} }
@Override
public void onPause() {
if (this.running) {
stopBeans(true);
this.running = false;
}
}
@Override @Override
public void onClose() { public void onClose() {
stopBeans(); stopBeans(false);
this.running = false; this.running = false;
} }
@ -341,7 +349,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
void stopForRestart() { void stopForRestart() {
if (this.running) { if (this.running) {
this.stoppedBeans = ConcurrentHashMap.newKeySet(); this.stoppedBeans = ConcurrentHashMap.newKeySet();
stopBeans(); stopBeans(false);
this.running = false; this.running = false;
} }
} }
@ -361,7 +369,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> { lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
int startupPhase = getPhase(bean); int startupPhase = getPhase(bean);
phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly)) phases.computeIfAbsent(
startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly, false))
.add(beanName, bean); .add(beanName, bean);
} }
}); });
@ -424,13 +433,14 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
(!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())); (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
} }
private void stopBeans() { private void stopBeans(boolean pauseableOnly) {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans(); Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder()); Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
lifecycleBeans.forEach((beanName, bean) -> { lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean); int shutdownPhase = getPhase(bean);
phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false)) phases.computeIfAbsent(
shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false, pauseableOnly))
.add(beanName, bean); .add(beanName, bean);
}); });
@ -446,13 +456,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
* @param beanName the name of the bean to stop * @param beanName the name of the bean to stop
*/ */
private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName, private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
final CountDownLatch latch, final Set<String> countDownBeanNames) { boolean pauseableOnly, final CountDownLatch latch, final Set<String> countDownBeanNames) {
Lifecycle bean = lifecycleBeans.remove(beanName); Lifecycle bean = lifecycleBeans.remove(beanName);
if (bean != null) { if (bean != null) {
String[] dependentBeans = getBeanFactory().getDependentBeans(beanName); String[] dependentBeans = getBeanFactory().getDependentBeans(beanName);
for (String dependentBean : dependentBeans) { for (String dependentBean : dependentBeans) {
doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames); doStop(lifecycleBeans, dependentBean, pauseableOnly, latch, countDownBeanNames);
} }
try { try {
if (bean.isRunning()) { if (bean.isRunning()) {
@ -461,20 +471,22 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
stoppedBeans.add(beanName); stoppedBeans.add(beanName);
} }
if (bean instanceof SmartLifecycle smartLifecycle) { if (bean instanceof SmartLifecycle smartLifecycle) {
if (logger.isTraceEnabled()) { if (!pauseableOnly || smartLifecycle.isPauseable()) {
logger.trace("Asking bean '" + beanName + "' of type [" + if (logger.isTraceEnabled()) {
bean.getClass().getName() + "] to stop"); logger.trace("Asking bean '" + beanName + "' of type [" +
} bean.getClass().getName() + "] to stop");
countDownBeanNames.add(beanName);
smartLifecycle.stop(() -> {
latch.countDown();
countDownBeanNames.remove(beanName);
if (logger.isDebugEnabled()) {
logger.debug("Bean '" + beanName + "' completed its stop procedure");
} }
}); countDownBeanNames.add(beanName);
smartLifecycle.stop(() -> {
latch.countDown();
countDownBeanNames.remove(beanName);
if (logger.isDebugEnabled()) {
logger.debug("Bean '" + beanName + "' completed its stop procedure");
}
});
}
} }
else { else if (!pauseableOnly) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Stopping bean '" + beanName + "' of type [" + logger.trace("Stopping bean '" + beanName + "' of type [" +
bean.getClass().getName() + "]"); bean.getClass().getName() + "]");
@ -562,14 +574,19 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private final boolean autoStartupOnly; private final boolean autoStartupOnly;
private final boolean pauseableOnly;
private final List<LifecycleGroupMember> members = new ArrayList<>(); private final List<LifecycleGroupMember> members = new ArrayList<>();
private int smartMemberCount; private int smartMemberCount;
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) { public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans,
boolean autoStartupOnly, boolean pauseableOnly) {
this.phase = phase; this.phase = phase;
this.lifecycleBeans = lifecycleBeans; this.lifecycleBeans = lifecycleBeans;
this.autoStartupOnly = autoStartupOnly; this.autoStartupOnly = autoStartupOnly;
this.pauseableOnly = pauseableOnly;
} }
public void add(String name, Lifecycle bean) { public void add(String name, Lifecycle bean) {
@ -621,7 +638,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet()); Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet());
for (LifecycleGroupMember member : this.members) { for (LifecycleGroupMember member : this.members) {
if (lifecycleBeanNames.contains(member.name)) { if (lifecycleBeanNames.contains(member.name)) {
doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames); doStop(this.lifecycleBeans, member.name, this.pauseableOnly, latch, countDownBeanNames);
} }
else if (member.bean instanceof SmartLifecycle) { else if (member.bean instanceof SmartLifecycle) {
// Already removed: must have been a dependent bean from another phase // Already removed: must have been a dependent bean from another phase

26
spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java

@ -355,6 +355,7 @@ class DefaultLifecycleProcessorTests {
TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forShutdownTests(5, 0, stoppedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forShutdownTests(5, 0, stoppedBeans);
TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forShutdownTests(-3, 0, stoppedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forShutdownTests(-3, 0, stoppedBeans);
smartBean2.setAutoStartup(false); smartBean2.setAutoStartup(false);
smartBean2.setPauseable(false);
context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
context.getBeanFactory().registerSingleton("smartBean2", smartBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
@ -375,11 +376,23 @@ class DefaultLifecycleProcessorTests {
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1); assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isTrue(); assertThat(smartBean1.isRunning()).isTrue();
assertThat(smartBean2.isRunning()).isFalse(); assertThat(smartBean2.isRunning()).isFalse();
context.pause();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isFalse();
assertThat(smartBean2.isRunning()).isFalse();
context.restart();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isTrue();
assertThat(smartBean2.isRunning()).isFalse();
context.start(); context.start();
assertThat(smartBean1.isRunning()).isTrue(); assertThat(smartBean1.isRunning()).isTrue();
assertThat(smartBean2.isRunning()).isTrue(); assertThat(smartBean2.isRunning()).isTrue();
context.pause();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isFalse();
assertThat(smartBean2.isRunning()).isTrue();
context.close(); context.close();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean2); assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean1, smartBean2);
} }
@Test @Test
@ -740,6 +753,8 @@ class DefaultLifecycleProcessorTests {
private volatile boolean autoStartup = true; private volatile boolean autoStartup = true;
private volatile boolean pauseable = true;
static TestSmartLifecycleBean forStartupTests(int phase, CopyOnWriteArrayList<Lifecycle> startedBeans) { static TestSmartLifecycleBean forStartupTests(int phase, CopyOnWriteArrayList<Lifecycle> startedBeans) {
return new TestSmartLifecycleBean(phase, 0, startedBeans, null); return new TestSmartLifecycleBean(phase, 0, startedBeans, null);
} }
@ -769,6 +784,15 @@ class DefaultLifecycleProcessorTests {
this.autoStartup = autoStartup; this.autoStartup = autoStartup;
} }
@Override
public boolean isPauseable() {
return this.pauseable;
}
public void setPauseable(boolean pauseable) {
this.pauseable = pauseable;
}
@Override @Override
public void stop(final Runnable callback) { public void stop(final Runnable callback) {
// calling stop() before the delay to preserve // calling stop() before the delay to preserve

6
spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java vendored

@ -93,8 +93,8 @@ public interface ContextCache {
/** /**
* Obtain a cached {@link ApplicationContext} for the given key. * Obtain a cached {@link ApplicationContext} for the given key.
* <p>If the cached application context was previously * <p>If the cached application context was previously
* {@linkplain org.springframework.context.Lifecycle#stop() stopped}, it * {@linkplain org.springframework.context.ConfigurableApplicationContext#pause() paused},
* must be * it must be
* {@linkplain org.springframework.context.support.AbstractApplicationContext#restart() * {@linkplain org.springframework.context.support.AbstractApplicationContext#restart()
* restarted}. This applies to parent contexts as well. * restarted}. This applies to parent contexts as well.
* <p>In addition, the {@linkplain #getHitCount() hit} and * <p>In addition, the {@linkplain #getHitCount() hit} and
@ -187,7 +187,7 @@ public interface ContextCache {
* {@link MergedContextConfiguration} and any of its parents. * {@link MergedContextConfiguration} and any of its parents.
* <p>If no other test classes are actively using the same application * <p>If no other test classes are actively using the same application
* context(s), the application context(s) should be * context(s), the application context(s) should be
* {@linkplain org.springframework.context.Lifecycle#stop() stopped}. * {@linkplain org.springframework.context.ConfigurableApplicationContext#pause() paused}.
* <p>The default implementation of this method does nothing. Concrete * <p>The default implementation of this method does nothing. Concrete
* implementations are therefore highly encouraged to override this * implementations are therefore highly encouraged to override this
* method, {@link #registerContextUsage(MergedContextConfiguration, Class)}, * method, {@link #registerContextUsage(MergedContextConfiguration, Class)},

2
spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java vendored

@ -187,7 +187,7 @@ public class DefaultContextCache implements ContextCache {
activeTestClasses.remove(testClass); activeTestClasses.remove(testClass);
if (activeTestClasses.isEmpty()) { if (activeTestClasses.isEmpty()) {
if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) { if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) {
cac.stop(); cac.pause();
} }
this.contextUsageMap.remove(mergedConfig); this.contextUsageMap.remove(mergedConfig);
} }

38
spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java vendored

@ -69,13 +69,13 @@ class UnusedContextsIntegrationTests {
// No BeforeTestClass, since EventPublishingTestExecutionListener // No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded. // only publishes events for a context that has already been loaded.
"AfterTestClass:TestCase1", "AfterTestClass:TestCase1",
"ContextStopped:TestCase1", "ContextPaused:TestCase1",
// --- TestCase2 ----------------------------------------------- // --- TestCase2 -----------------------------------------------
"ContextRestarted:TestCase1", "ContextRestarted:TestCase1",
"BeforeTestClass:TestCase2", "BeforeTestClass:TestCase2",
"AfterTestClass:TestCase2", "AfterTestClass:TestCase2",
"ContextStopped:TestCase1", "ContextPaused:TestCase1",
// --- TestCase3 ----------------------------------------------- // --- TestCase3 -----------------------------------------------
"ContextRestarted:TestCase1", "ContextRestarted:TestCase1",
@ -90,13 +90,13 @@ class UnusedContextsIntegrationTests {
// No BeforeTestClass, since EventPublishingTestExecutionListener // No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded. // only publishes events for a context that has already been loaded.
"AfterTestClass:TestCase4", "AfterTestClass:TestCase4",
"ContextStopped:TestCase4", "ContextPaused:TestCase4",
// --- TestCase5 ----------------------------------------------- // --- TestCase5 -----------------------------------------------
"ContextRestarted:TestCase4", "ContextRestarted:TestCase4",
"BeforeTestClass:TestCase5", "BeforeTestClass:TestCase5",
"AfterTestClass:TestCase5", "AfterTestClass:TestCase5",
"ContextStopped:TestCase4" "ContextPaused:TestCase4"
); );
} }
@ -130,19 +130,19 @@ class UnusedContextsIntegrationTests {
// using the context // using the context
"AfterTestClass:OverridingNestedTestCase1", "AfterTestClass:OverridingNestedTestCase1",
"ContextStopped:OverridingNestedTestCase1", "ContextPaused:OverridingNestedTestCase1",
// --- OverridingNestedTestCase2 --------------------------- // --- OverridingNestedTestCase2 ---------------------------
"ContextRestarted:OverridingNestedTestCase1", "ContextRestarted:OverridingNestedTestCase1",
"BeforeTestClass:OverridingNestedTestCase2", "BeforeTestClass:OverridingNestedTestCase2",
"AfterTestClass:OverridingNestedTestCase2", "AfterTestClass:OverridingNestedTestCase2",
"ContextStopped:OverridingNestedTestCase1", "ContextPaused:OverridingNestedTestCase1",
"AfterTestClass:NestedTestCase", "AfterTestClass:NestedTestCase",
// No Stopped event, since EnclosingTestCase is still using the context // No Stopped event, since EnclosingTestCase is still using the context
"AfterTestClass:EnclosingTestCase", "AfterTestClass:EnclosingTestCase",
"ContextStopped:EnclosingTestCase" "ContextPaused:EnclosingTestCase"
); );
} }
@ -161,23 +161,23 @@ class UnusedContextsIntegrationTests {
// --- ContextHierarchyLevel1TestCase ------------------------------ // --- ContextHierarchyLevel1TestCase ------------------------------
"ContextRefreshed:ContextHierarchyLevel1TestCase", "ContextRefreshed:ContextHierarchyLevel1TestCase",
"AfterTestClass:ContextHierarchyLevel1TestCase", "AfterTestClass:ContextHierarchyLevel1TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase", "ContextPaused:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel2TestCase ------------------------------ // --- ContextHierarchyLevel2TestCase ------------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase", "ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRefreshed:ContextHierarchyLevel2TestCase", "ContextRefreshed:ContextHierarchyLevel2TestCase",
"AfterTestClass:ContextHierarchyLevel2TestCase", "AfterTestClass:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel2TestCase", "ContextPaused:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase", "ContextPaused:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3a1TestCase ----------------------------- // --- ContextHierarchyLevel3a1TestCase -----------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase", "ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRestarted:ContextHierarchyLevel2TestCase", "ContextRestarted:ContextHierarchyLevel2TestCase",
"ContextRefreshed:ContextHierarchyLevel3a1TestCase", "ContextRefreshed:ContextHierarchyLevel3a1TestCase",
"AfterTestClass:ContextHierarchyLevel3a1TestCase", "AfterTestClass:ContextHierarchyLevel3a1TestCase",
"ContextStopped:ContextHierarchyLevel3a1TestCase", "ContextPaused:ContextHierarchyLevel3a1TestCase",
"ContextStopped:ContextHierarchyLevel2TestCase", "ContextPaused:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase", "ContextPaused:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3a2TestCase ----------------------------- // --- ContextHierarchyLevel3a2TestCase -----------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase", "ContextRestarted:ContextHierarchyLevel1TestCase",
@ -185,18 +185,18 @@ class UnusedContextsIntegrationTests {
"ContextRestarted:ContextHierarchyLevel3a1TestCase", "ContextRestarted:ContextHierarchyLevel3a1TestCase",
"BeforeTestClass:ContextHierarchyLevel3a2TestCase", "BeforeTestClass:ContextHierarchyLevel3a2TestCase",
"AfterTestClass:ContextHierarchyLevel3a2TestCase", "AfterTestClass:ContextHierarchyLevel3a2TestCase",
"ContextStopped:ContextHierarchyLevel3a1TestCase", "ContextPaused:ContextHierarchyLevel3a1TestCase",
"ContextStopped:ContextHierarchyLevel2TestCase", "ContextPaused:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase", "ContextPaused:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3bTestCase ----------------------------- // --- ContextHierarchyLevel3bTestCase -----------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase", "ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRestarted:ContextHierarchyLevel2TestCase", "ContextRestarted:ContextHierarchyLevel2TestCase",
"ContextRefreshed:ContextHierarchyLevel3bTestCase", "ContextRefreshed:ContextHierarchyLevel3bTestCase",
"AfterTestClass:ContextHierarchyLevel3bTestCase", "AfterTestClass:ContextHierarchyLevel3bTestCase",
"ContextStopped:ContextHierarchyLevel3bTestCase", "ContextPaused:ContextHierarchyLevel3bTestCase",
"ContextStopped:ContextHierarchyLevel2TestCase", "ContextPaused:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase" "ContextPaused:ContextHierarchyLevel1TestCase"
); );
} }

Loading…
Cancel
Save