diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java index b609a931574..8e5e4ad3a34 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java @@ -17,7 +17,6 @@ package org.springframework.boot; import java.util.Collections; -import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.Set; import java.util.WeakHashMap; @@ -104,16 +103,16 @@ class SpringApplicationShutdownHook implements Runnable { public void run() { Set contexts; Set closedContexts; - Set actions; + Set handlers; synchronized (SpringApplicationShutdownHook.class) { this.inProgress = true; contexts = new LinkedHashSet<>(this.contexts); closedContexts = new LinkedHashSet<>(this.closedContexts); - actions = new LinkedHashSet<>(this.handlers.getActions()); + handlers = new LinkedHashSet<>(this.handlers.getActions()); } contexts.forEach(this::closeAndWait); closedContexts.forEach(this::closeAndWait); - actions.forEach(Runnable::run); + handlers.forEach(Handler::run); } boolean isApplicationContextRegistered(ConfigurableApplicationContext context) { @@ -171,7 +170,7 @@ class SpringApplicationShutdownHook implements Runnable { */ private final class Handlers implements SpringApplicationShutdownHandlers, Runnable { - private final Set actions = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set actions = new LinkedHashSet<>(); @Override public void add(Runnable action) { @@ -179,7 +178,7 @@ class SpringApplicationShutdownHook implements Runnable { addRuntimeShutdownHookIfNecessary(); synchronized (SpringApplicationShutdownHook.class) { 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"); synchronized (SpringApplicationShutdownHook.class) { assertNotInProgress(); - this.actions.remove(action); + this.actions.remove(new Handler(action)); } } - Set getActions() { + Set getActions() { 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. */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java index 0d821496e30..4d4d56947d7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java @@ -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"); * 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.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.springframework.beans.factory.BeanCreationException; 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.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; /** * Tests for {@link SpringApplicationShutdownHook}. @@ -203,6 +206,23 @@ class SpringApplicationShutdownHookTests { 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 { private boolean runtimeShutdownHookAdded;