Skip to content

Commit 7900315

Browse files
committed
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
1 parent 6bd12e8 commit 7900315

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.convert.support;
18+
19+
import java.time.Instant;
20+
import java.util.Date;
21+
22+
import org.springframework.core.convert.converter.Converter;
23+
24+
/**
25+
* Convert a {@link java.util.Date} to a {@link java.time.Instant}.
26+
*
27+
* <p>This includes conversion support for {@link java.sql.Timestamp} and other
28+
* subtypes of {@code java.util.Date}. Note, however, that an attempt to convert
29+
* a {@link java.sql.Date} or {@link java.sql.Time} to a {@code java.time.Instant}
30+
* results in an {@link UnsupportedOperationException} since those types do not
31+
* have time or date components, respectively.
32+
*
33+
* @author Sam Brannen
34+
* @since 6.2.9
35+
* @see Date#toInstant()
36+
* @see InstantToDateConverter
37+
*/
38+
final class DateToInstantConverter implements Converter<Date, Instant> {
39+
40+
@Override
41+
public Instant convert(Date date) {
42+
return date.toInstant();
43+
}
44+
45+
}

spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public static void addDefaultConverters(ConverterRegistry converterRegistry) {
9191
addCollectionConverters(converterRegistry);
9292

9393
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
94+
converterRegistry.addConverter(new DateToInstantConverter());
95+
converterRegistry.addConverter(new InstantToDateConverter());
9496
converterRegistry.addConverter(new StringToTimeZoneConverter());
9597
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
9698
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.convert.support;
18+
19+
import java.time.Instant;
20+
import java.util.Date;
21+
22+
import org.springframework.core.convert.TypeDescriptor;
23+
import org.springframework.core.convert.converter.ConditionalConverter;
24+
import org.springframework.core.convert.converter.Converter;
25+
26+
/**
27+
* Convert a {@link java.time.Instant} to a {@link java.util.Date}.
28+
*
29+
* <p>This does not include conversion support for target types which are subtypes
30+
* of {@code java.util.Date}.
31+
*
32+
* @author Sam Brannen
33+
* @since 6.2.9
34+
* @see Date#from(Instant)
35+
* @see DateToInstantConverter
36+
*/
37+
final class InstantToDateConverter implements ConditionalConverter, Converter<Instant, Date> {
38+
39+
@Override
40+
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
41+
return targetType.getType().equals(Date.class);
42+
}
43+
44+
@Override
45+
public Date convert(Instant instant) {
46+
return Date.from(instant);
47+
}
48+
49+
}

spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222
import java.math.BigInteger;
2323
import java.nio.charset.Charset;
2424
import java.nio.charset.StandardCharsets;
25+
import java.sql.Time;
26+
import java.sql.Timestamp;
27+
import java.time.Instant;
2528
import java.time.ZoneId;
2629
import java.util.AbstractList;
2730
import java.util.ArrayList;
2831
import java.util.Collection;
2932
import java.util.Collections;
3033
import java.util.Currency;
34+
import java.util.Date;
3135
import java.util.EnumSet;
3236
import java.util.HashMap;
3337
import java.util.LinkedHashMap;
@@ -973,6 +977,88 @@ void convertExistingOptional() {
973977
assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty());
974978
}
975979

980+
@Test // gh-35175
981+
void convertDateToInstant() {
982+
TypeDescriptor dateDescriptor = TypeDescriptor.valueOf(Date.class);
983+
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
984+
Date date = new Date();
985+
986+
// Conversion performed by DateToInstantConverter.
987+
assertThat(conversionService.convert(date, dateDescriptor, instantDescriptor))
988+
.isEqualTo(date.toInstant());
989+
}
990+
991+
@Test // gh-35175
992+
void convertSqlDateToInstant() {
993+
TypeDescriptor sqlDateDescriptor = TypeDescriptor.valueOf(java.sql.Date.class);
994+
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
995+
java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
996+
997+
// DateToInstantConverter blindly invokes toInstant() on any java.util.Date
998+
// subtype, which results in an UnsupportedOperationException since
999+
// java.sql.Date does not have a time component. However, even if
1000+
// DateToInstantConverter were not registered, ObjectToObjectConverter
1001+
// would still attempt to invoke toInstant() on a java.sql.Date by convention,
1002+
// which results in the same UnsupportedOperationException.
1003+
assertThatExceptionOfType(ConversionFailedException.class)
1004+
.isThrownBy(() -> conversionService.convert(sqlDate, sqlDateDescriptor, instantDescriptor))
1005+
.withCauseExactlyInstanceOf(UnsupportedOperationException.class);
1006+
}
1007+
1008+
@Test // gh-35175
1009+
void convertSqlTimeToInstant() {
1010+
TypeDescriptor timeDescriptor = TypeDescriptor.valueOf(Time.class);
1011+
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
1012+
Time time = new Time(System.currentTimeMillis());
1013+
1014+
// DateToInstantConverter blindly invokes toInstant() on any java.util.Date
1015+
// subtype, which results in an UnsupportedOperationException since
1016+
// java.sql.Date does not have a time component. However, even if
1017+
// DateToInstantConverter were not registered, ObjectToObjectConverter
1018+
// would still attempt to invoke toInstant() on a java.sql.Date by convention,
1019+
// which results in the same UnsupportedOperationException.
1020+
assertThatExceptionOfType(ConversionFailedException.class)
1021+
.isThrownBy(() -> conversionService.convert(time, timeDescriptor, instantDescriptor))
1022+
.withCauseExactlyInstanceOf(UnsupportedOperationException.class);
1023+
}
1024+
1025+
@Test // gh-35175
1026+
void convertSqlTimestampToInstant() {
1027+
TypeDescriptor timestampDescriptor = TypeDescriptor.valueOf(Timestamp.class);
1028+
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
1029+
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
1030+
1031+
// Conversion performed by DateToInstantConverter.
1032+
assertThat(conversionService.convert(timestamp, timestampDescriptor, instantDescriptor))
1033+
.isEqualTo(timestamp.toInstant());
1034+
}
1035+
1036+
@Test // gh-35175
1037+
void convertInstantToDate() {
1038+
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
1039+
TypeDescriptor dateDescriptor = TypeDescriptor.valueOf(Date.class);
1040+
Date date = new Date();
1041+
Instant instant = date.toInstant();
1042+
1043+
// Conversion performed by InstantToDateConverter.
1044+
assertThat(conversionService.convert(instant, instantDescriptor, dateDescriptor))
1045+
.isExactlyInstanceOf(Date.class)
1046+
.isEqualTo(date);
1047+
}
1048+
1049+
@Test
1050+
void convertInstantToSqlTimestamp() {
1051+
TypeDescriptor instantDescriptor = TypeDescriptor.valueOf(Instant.class);
1052+
TypeDescriptor timestampDescriptor = TypeDescriptor.valueOf(Timestamp.class);
1053+
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
1054+
Instant instant = timestamp.toInstant();
1055+
1056+
// Conversion performed by ObjectToObjectConverter.
1057+
assertThat(conversionService.convert(instant, instantDescriptor, timestampDescriptor))
1058+
.isExactlyInstanceOf(Timestamp.class)
1059+
.isEqualTo(timestamp);
1060+
}
1061+
9761062

9771063
// test fields and helpers
9781064

0 commit comments

Comments
 (0)