From b6d7f1d586d5bce86d6ec1205736a33a3caefda7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Sat, 19 Sep 2026 12:12:10 +0200 Subject: [PATCH] Avoid duplicate date-related converters registration Prior to this commit, `DefaultFormattingConversionService` would register converters with both `DateTimeFormatterRegistrar` and `DateFormatterRegistrar`, the former also registering the legace date converters that the latter contributes. While we cannot change the behavior for `DateTimeFormatterRegistrar` or `DateFormatterRegistrar` because of their public contract, we can update the `DefaultFormattingConversionService` to not use `DateFormatterRegistrar` and register manually the annotation support that it contributes. Closes gh-36951 --- .../DefaultFormattingConversionService.java | 9 ++--- .../FormattingConversionServiceTests.java | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java index e7df7c25805..4653da317a3 100644 --- a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -20,7 +20,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.FormatterRegistry; -import org.springframework.format.datetime.DateFormatterRegistrar; +import org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory; import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; import org.springframework.format.number.money.CurrencyUnitFormatter; @@ -115,11 +115,12 @@ public class DefaultFormattingConversionService extends FormattingConversionServ // Default handling of date-time values - // just handling JSR-310 specific date and time types + // Handling of JSR-310 specific date and time types, along with the legacy + // Date/Calendar/Long converters (see DateTimeConverters). new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry); - // regular DateFormat-based Date, Calendar, Long converters - new DateFormatterRegistrar().registerFormatters(formatterRegistry); + // Support for the @DateTimeFormat annotation on legacy Date/Calendar fields + formatterRegistry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory()); } } diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java index 3f2468baade..530e26d38d4 100644 --- a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java @@ -16,6 +16,8 @@ package org.springframework.format.support; +import java.util.Calendar; +import java.util.Date; import java.util.Locale; import org.junit.jupiter.api.AfterEach; @@ -30,6 +32,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.Formatter; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.number.NumberStyleFormatter; import static org.assertj.core.api.Assertions.assertThat; @@ -176,6 +179,28 @@ class FormattingConversionServiceTests { assertThat(formattingService.convert("1", Integer.class)).isEqualTo(Integer.valueOf(1)); } + @Test // gh-36951 + void defaultFormattingConversionServiceRegistersLegacyDateConvertersOnlyOnce() { + DefaultFormattingConversionService defaultService = new DefaultFormattingConversionService(); + String convertersDescription = defaultService.toString(); + assertThat(convertersDescription) + .containsOnlyOnce("DateFormatterRegistrar$DateToLongConverter") + .containsOnlyOnce("DateFormatterRegistrar$CalendarToDateConverter"); + } + + @Test // gh-36951 + void defaultFormattingConversionServiceStillAppliesDateTimeFormatAnnotationToLegacyDateAndCalendarFields() throws Exception { + DefaultFormattingConversionService defaultService = new DefaultFormattingConversionService(); + TypeDescriptor dateDescriptor = new TypeDescriptor(AnnotatedDateBean.class.getDeclaredField("date")); + TypeDescriptor calendarDescriptor = new TypeDescriptor(AnnotatedDateBean.class.getDeclaredField("calendar")); + + Date date = (Date) defaultService.convert("2026-09-19", TypeDescriptor.valueOf(String.class), dateDescriptor); + Calendar calendar = (Calendar) defaultService.convert("2026-09-19", TypeDescriptor.valueOf(String.class), calendarDescriptor); + + assertThat(defaultService.convert(date, dateDescriptor, TypeDescriptor.valueOf(String.class))).isEqualTo("2026-09-19"); + assertThat(defaultService.convert(calendar, calendarDescriptor, TypeDescriptor.valueOf(String.class))).isEqualTo("2026-09-19"); + } + static class NullReturningFormatter implements Formatter { @@ -214,4 +239,14 @@ class FormattingConversionServiceTests { } } + + private static class AnnotatedDateBean { + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date date; + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Calendar calendar; + } + }