Merge branch '3.3.x'

Closes gh-43536
This commit is contained in:
Phillip Webb 2024-12-16 17:16:47 -08:00
commit a824b23cd7
2 changed files with 58 additions and 9 deletions

View File

@ -17,7 +17,6 @@
package org.springframework.boot; package org.springframework.boot;
import java.util.Collections; import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
@ -104,16 +103,16 @@ class SpringApplicationShutdownHook implements Runnable {
public void run() { public void run() {
Set<ConfigurableApplicationContext> contexts; Set<ConfigurableApplicationContext> contexts;
Set<ConfigurableApplicationContext> closedContexts; Set<ConfigurableApplicationContext> closedContexts;
Set<Runnable> actions; Set<Handler> handlers;
synchronized (SpringApplicationShutdownHook.class) { synchronized (SpringApplicationShutdownHook.class) {
this.inProgress = true; this.inProgress = true;
contexts = new LinkedHashSet<>(this.contexts); contexts = new LinkedHashSet<>(this.contexts);
closedContexts = new LinkedHashSet<>(this.closedContexts); closedContexts = new LinkedHashSet<>(this.closedContexts);
actions = new LinkedHashSet<>(this.handlers.getActions()); handlers = new LinkedHashSet<>(this.handlers.getActions());
} }
contexts.forEach(this::closeAndWait); contexts.forEach(this::closeAndWait);
closedContexts.forEach(this::closeAndWait); closedContexts.forEach(this::closeAndWait);
actions.forEach(Runnable::run); handlers.forEach(Handler::run);
} }
boolean isApplicationContextRegistered(ConfigurableApplicationContext context) { boolean isApplicationContextRegistered(ConfigurableApplicationContext context) {
@ -171,7 +170,7 @@ class SpringApplicationShutdownHook implements Runnable {
*/ */
private final class Handlers implements SpringApplicationShutdownHandlers, Runnable { private final class Handlers implements SpringApplicationShutdownHandlers, Runnable {
private final Set<Runnable> actions = Collections.newSetFromMap(new IdentityHashMap<>()); private final Set<Handler> actions = new LinkedHashSet<>();
@Override @Override
public void add(Runnable action) { public void add(Runnable action) {
@ -179,7 +178,7 @@ class SpringApplicationShutdownHook implements Runnable {
addRuntimeShutdownHookIfNecessary(); addRuntimeShutdownHookIfNecessary();
synchronized (SpringApplicationShutdownHook.class) { synchronized (SpringApplicationShutdownHook.class) {
assertNotInProgress(); assertNotInProgress();
this.actions.add(action); this.actions.add(new Handler(action));
} }
} }
@ -188,11 +187,11 @@ class SpringApplicationShutdownHook implements Runnable {
Assert.notNull(action, "Action must not be null"); Assert.notNull(action, "Action must not be null");
synchronized (SpringApplicationShutdownHook.class) { synchronized (SpringApplicationShutdownHook.class) {
assertNotInProgress(); assertNotInProgress();
this.actions.remove(action); this.actions.remove(new Handler(action));
} }
} }
Set<Runnable> getActions() { Set<Handler> getActions() {
return this.actions; return this.actions;
} }
@ -204,6 +203,36 @@ class SpringApplicationShutdownHook implements Runnable {
} }
/**
* A single handler that uses object identity for {@link #equals(Object)} and
* {@link #hashCode()}.
*
* @param runnable the handler runner
*/
record Handler(Runnable runnable) {
@Override
public int hashCode() {
return System.identityHashCode(this.runnable);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.runnable == ((Handler) obj).runnable;
}
void run() {
this.runnable.run();
}
}
/** /**
* {@link ApplicationListener} to track closed contexts. * {@link ApplicationListener} to track closed contexts.
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
@ -39,6 +40,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link SpringApplicationShutdownHook}. * Tests for {@link SpringApplicationShutdownHook}.
@ -203,6 +206,23 @@ class SpringApplicationShutdownHookTests {
assertThat(shutdownHook.isApplicationContextRegistered(context)).isFalse(); assertThat(shutdownHook.isApplicationContextRegistered(context)).isFalse();
} }
@Test
void handlersRunInDeterministicOrder() {
TestSpringApplicationShutdownHook shutdownHook = new TestSpringApplicationShutdownHook();
Runnable r1 = mock(Runnable.class);
Runnable r2 = mock(Runnable.class);
Runnable r3 = mock(Runnable.class);
shutdownHook.getHandlers().add(r2);
shutdownHook.getHandlers().add(r1);
shutdownHook.getHandlers().add(r3);
shutdownHook.run();
InOrder ordered = inOrder(r1, r2, r3);
ordered.verify(r2).run();
ordered.verify(r1).run();
ordered.verify(r3).run();
ordered.verifyNoMoreInteractions();
}
static class TestSpringApplicationShutdownHook extends SpringApplicationShutdownHook { static class TestSpringApplicationShutdownHook extends SpringApplicationShutdownHook {
private boolean runtimeShutdownHookAdded; private boolean runtimeShutdownHookAdded;