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
This commit is contained in:
Brian Clozel
2026-09-19 12:12:10 +02:00
parent b9fa062918
commit b6d7f1d586
2 changed files with 40 additions and 4 deletions
@@ -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());
}
}
@@ -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<Integer> {
@@ -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;
}
}