Introduce Date-to-Instant and Instant-to-Date converters

In order to avoid unnecessary use of reflection and to simplify native
image deployments, this commit introduces explicit support for
automatic conversions from java.util.Date to java.time.Instant and vice
versa.

To achieve that, this commit introduces an InstantToDateConverter and a
DateToInstantConverter and registers them automatically in
DefaultConversionService.

See gh-35156
Closes gh-35175
This commit is contained in:
Sam Brannen
2025-07-09 12:31:44 +02:00
parent 6bd12e8680
commit 7900315f23
4 changed files with 182 additions and 0 deletions
@@ -0,0 +1,45 @@
/*
* Copyright 2002-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.time.Instant;
import java.util.Date;
import org.springframework.core.convert.converter.Converter;
/**
* Convert a {@link java.util.Date} to a {@link java.time.Instant}.
*
* <p>This includes conversion support for {@link java.sql.Timestamp} and other
* subtypes of {@code java.util.Date}. Note, however, that an attempt to convert
* a {@link java.sql.Date} or {@link java.sql.Time} to a {@code java.time.Instant}
* results in an {@link UnsupportedOperationException} since those types do not
* have time or date components, respectively.
*
* @author Sam Brannen
* @since 6.2.9
* @see Date#toInstant()
* @see InstantToDateConverter
*/
final class DateToInstantConverter implements Converter<Date, Instant> {
@Override
public Instant convert(Date date) {
return date.toInstant();
}
}
@@ -91,6 +91,8 @@ public class DefaultConversionService extends GenericConversionService {
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new DateToInstantConverter());
converterRegistry.addConverter(new InstantToDateConverter());
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
@@ -0,0 +1,49 @@
/*
* Copyright 2002-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.time.Instant;
import java.util.Date;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
/**
* Convert a {@link java.time.Instant} to a {@link java.util.Date}.
*
* <p>This does not include conversion support for target types which are subtypes
* of {@code java.util.Date}.
*
* @author Sam Brannen
* @since 6.2.9
* @see Date#from(Instant)
* @see DateToInstantConverter
*/
final class InstantToDateConverter implements ConditionalConverter, Converter<Instant, Date> {
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.getType().equals(Date.class);
}
@Override
public Date convert(Instant instant) {
return Date.from(instant);
}
}
@@ -22,12 +22,16 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.ZoneId;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -973,6 +977,88 @@ class DefaultConversionServiceTests {
assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty());
}
@Test // gh-35175
void convertDateToInstant() {
TypeDescriptor dateDescriptor = TypeDescriptor.valueOf(Date.class);
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
Date date = new Date();
// Conversion performed by DateToInstantConverter.
assertThat(conversionService.convert(date, dateDescriptor, instantDescriptor))
.isEqualTo(date.toInstant());
}
@Test // gh-35175
void convertSqlDateToInstant() {
TypeDescriptor sqlDateDescriptor = TypeDescriptor.valueOf(java.sql.Date.class);
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
// DateToInstantConverter blindly invokes toInstant() on any java.util.Date
// subtype, which results in an UnsupportedOperationException since
// java.sql.Date does not have a time component. However, even if
// DateToInstantConverter were not registered, ObjectToObjectConverter
// would still attempt to invoke toInstant() on a java.sql.Date by convention,
// which results in the same UnsupportedOperationException.
assertThatExceptionOfType(ConversionFailedException.class)
.isThrownBy(() -> conversionService.convert(sqlDate, sqlDateDescriptor, instantDescriptor))
.withCauseExactlyInstanceOf(UnsupportedOperationException.class);
}
@Test // gh-35175
void convertSqlTimeToInstant() {
TypeDescriptor timeDescriptor = TypeDescriptor.valueOf(Time.class);
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
Time time = new Time(System.currentTimeMillis());
// DateToInstantConverter blindly invokes toInstant() on any java.util.Date
// subtype, which results in an UnsupportedOperationException since
// java.sql.Date does not have a time component. However, even if
// DateToInstantConverter were not registered, ObjectToObjectConverter
// would still attempt to invoke toInstant() on a java.sql.Date by convention,
// which results in the same UnsupportedOperationException.
assertThatExceptionOfType(ConversionFailedException.class)
.isThrownBy(() -> conversionService.convert(time, timeDescriptor, instantDescriptor))
.withCauseExactlyInstanceOf(UnsupportedOperationException.class);
}
@Test // gh-35175
void convertSqlTimestampToInstant() {
TypeDescriptor timestampDescriptor = TypeDescriptor.valueOf(Timestamp.class);
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
// Conversion performed by DateToInstantConverter.
assertThat(conversionService.convert(timestamp, timestampDescriptor, instantDescriptor))
.isEqualTo(timestamp.toInstant());
}
@Test // gh-35175
void convertInstantToDate() {
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
TypeDescriptor dateDescriptor = TypeDescriptor.valueOf(Date.class);
Date date = new Date();
Instant instant = date.toInstant();
// Conversion performed by InstantToDateConverter.
assertThat(conversionService.convert(instant, instantDescriptor, dateDescriptor))
.isExactlyInstanceOf(Date.class)
.isEqualTo(date);
}
@Test
void convertInstantToSqlTimestamp() {
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
TypeDescriptor timestampDescriptor = TypeDescriptor.valueOf(Timestamp.class);
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
Instant instant = timestamp.toInstant();
// Conversion performed by ObjectToObjectConverter.
assertThat(conversionService.convert(instant, instantDescriptor, timestampDescriptor))
.isExactlyInstanceOf(Timestamp.class)
.isEqualTo(timestamp);
}
// test fields and helpers