From c1d241928ce23b868ea0ab8bdda076a832080720 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:08:12 +0200 Subject: [PATCH 1/2] Rename maxAttemptsReached() to maxElapsedTimeReached() and organize tests --- .../util/backoff/ExponentialBackOffTests.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/util/backoff/ExponentialBackOffTests.java b/spring-core/src/test/java/org/springframework/util/backoff/ExponentialBackOffTests.java index e7e58cb33cf..bdc2b79a9b5 100644 --- a/spring-core/src/test/java/org/springframework/util/backoff/ExponentialBackOffTests.java +++ b/spring-core/src/test/java/org/springframework/util/backoff/ExponentialBackOffTests.java @@ -79,7 +79,17 @@ class ExponentialBackOffTests { } @Test - void maxAttemptsReached() { + void maxIntervalReachedImmediately() { + ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0); + backOff.setMaxInterval(50L); + + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(50L); + assertThat(execution.nextBackOff()).isEqualTo(50L); + } + + @Test + void maxElapsedTimeReached() { ExponentialBackOff backOff = new ExponentialBackOff(2000L, 2.0); backOff.setMaxElapsedTime(4000L); @@ -90,6 +100,19 @@ class ExponentialBackOffTests { assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); } + @Test + void maxAttempts() { + ExponentialBackOff backOff = new ExponentialBackOff(); + backOff.setInitialInterval(1000L); + backOff.setMultiplier(2.0); + backOff.setMaxInterval(10000L); + backOff.setMaxAttempts(6); + List delays = new ArrayList<>(); + BackOffExecution execution = backOff.start(); + IntStream.range(0, 7).forEach(i -> delays.add(execution.nextBackOff())); + assertThat(delays).containsExactly(1000L, 2000L, 4000L, 8000L, 10000L, 10000L, BackOffExecution.STOP); + } + @Test void startReturnsDifferentInstances() { ExponentialBackOff backOff = new ExponentialBackOff(); @@ -128,16 +151,6 @@ class ExponentialBackOffTests { assertThatNoException().isThrownBy(execution::nextBackOff); } - @Test - void maxIntervalReachedImmediately() { - ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0); - backOff.setMaxInterval(50L); - - BackOffExecution execution = backOff.start(); - assertThat(execution.nextBackOff()).isEqualTo(50L); - assertThat(execution.nextBackOff()).isEqualTo(50L); - } - @Test void toStringContent() { ExponentialBackOff backOff = new ExponentialBackOff(2000L, 2.0); @@ -159,17 +172,4 @@ class ExponentialBackOffTests { assertThat(execution).asString().isEqualTo("ExponentialBackOffExecution[currentInterval=4000ms, multiplier=2.0, attempts=2]"); } - @Test - void maxAttempts() { - ExponentialBackOff backOff = new ExponentialBackOff(); - backOff.setInitialInterval(1000L); - backOff.setMultiplier(2.0); - backOff.setMaxInterval(10000L); - backOff.setMaxAttempts(6); - List delays = new ArrayList<>(); - BackOffExecution execution = backOff.start(); - IntStream.range(0, 7).forEach(i -> delays.add(execution.nextBackOff())); - assertThat(delays).containsExactly(1000L, 2000L, 4000L, 8000L, 10000L, 10000L, BackOffExecution.STOP); - } - } From 35d8c4d06ffdeb0714874ac094da84952cdf9f66 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:31:10 +0200 Subject: [PATCH 2/2] Align synthesized annotation toString() with JDK for NaN/Infinity Closes gh-37244 --- ...izedMergedAnnotationInvocationHandler.java | 28 +++++++++++++++---- .../annotation/MergedAnnotationsTests.java | 24 ++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java index 24c21acf6af..1b60933326a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java @@ -160,14 +160,12 @@ final class SynthesizedMergedAnnotationInvocationHandler i } /** - * This method currently does not address the following issues which we may + * This method currently does not address the following issue which we may * choose to address at a later point in time. * *
    *
  • non-ASCII, non-visible, and non-printable characters within a character * or String literal are not escaped.
  • - *
  • formatting for float and double values does not take into account whether - * a value is not a number (NaN) or infinite.
  • *
* @param value the attribute value to format * @return the formatted string representation @@ -199,10 +197,30 @@ final class SynthesizedMergedAnnotationInvocationHandler
i return Long.toString((Long) value) + 'L'; } if (type == Float.class) { - return Float.toString((Float) value) + 'f'; + float floatValue = (Float) value; + if (Float.isNaN(floatValue)) { + return "0.0f/0.0f"; + } + if (floatValue == Float.POSITIVE_INFINITY) { + return "1.0f/0.0f"; + } + if (floatValue == Float.NEGATIVE_INFINITY) { + return "-1.0f/0.0f"; + } + return Float.toString(floatValue) + 'f'; } if (type == Double.class) { - return Double.toString((Double) value); + double doubleValue = (Double) value; + if (Double.isNaN(doubleValue)) { + return "0.0/0.0"; + } + if (doubleValue == Double.POSITIVE_INFINITY) { + return "1.0/0.0"; + } + if (doubleValue == Double.NEGATIVE_INFINITY) { + return "-1.0/0.0"; + } + return Double.toString(doubleValue); } if (value instanceof Enum e) { return e.name(); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index c5c386e71d6..61fc0ffe367 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -2060,6 +2060,30 @@ class MergedAnnotationsTests { .isEqualTo("@%s({\"FromValueAttributeMeta\"})", ValueAttribute.class.getCanonicalName()); } + @Test // gh-37244 + void toStringForSynthesizedAnnotationsWithNonFiniteFloatingPointValues() { + Map attributes = Map.of( + "name", "test", + "floatValue", Float.NaN, + "doubleValue", Double.NaN); + RequestMapping mapping = MergedAnnotation.of(RequestMapping.class, attributes).synthesize(); + assertThat(mapping).asString().contains("floatValue=0.0f/0.0f", "doubleValue=0.0/0.0"); + + attributes = Map.of( + "name", "test", + "floatValue", Float.POSITIVE_INFINITY, + "doubleValue", Double.POSITIVE_INFINITY); + mapping = MergedAnnotation.of(RequestMapping.class, attributes).synthesize(); + assertThat(mapping).asString().contains("floatValue=1.0f/0.0f", "doubleValue=1.0/0.0"); + + attributes = Map.of( + "name", "test", + "floatValue", Float.NEGATIVE_INFINITY, + "doubleValue", Double.NEGATIVE_INFINITY); + mapping = MergedAnnotation.of(RequestMapping.class, attributes).synthesize(); + assertThat(mapping).asString().contains("floatValue=-1.0f/0.0f", "doubleValue=-1.0/0.0"); + } + @Test void equalsForSynthesizedAnnotations() throws Exception { Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute");