diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java
index 0b77a49eb36..95fdb49a52c 100644
--- a/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java
+++ b/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java
@@ -16,6 +16,7 @@
package org.springframework.aot.hint.support;
+import java.time.Instant;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
@@ -34,6 +35,7 @@ import org.springframework.aot.hint.TypeReference;
* {@code org.springframework.core.convert.support.ObjectToObjectConverter}.
*
* @author Sebastien Deleuze
+ * @author Sam Brannen
* @since 6.0
*/
class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar {
@@ -41,6 +43,7 @@ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
ReflectionHints reflectionHints = hints.reflection();
+
TypeReference sqlDateTypeReference = TypeReference.of("java.sql.Date");
reflectionHints.registerTypeIfPresent(classLoader, sqlDateTypeReference.getName(), hint -> hint
.withMethod("toLocalDate", Collections.emptyList(), ExecutableMode.INVOKE)
@@ -48,8 +51,14 @@ class ObjectToObjectConverterRuntimeHints implements RuntimeHintsRegistrar {
.withMethod("valueOf", List.of(TypeReference.of(LocalDate.class)), ExecutableMode.INVOKE)
.onReachableType(sqlDateTypeReference));
+ TypeReference sqlTimestampTypeReference = TypeReference.of("java.sql.Timestamp");
+ reflectionHints.registerTypeIfPresent(classLoader, sqlTimestampTypeReference.getName(), hint -> hint
+ .withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE)
+ .onReachableType(sqlTimestampTypeReference));
+
reflectionHints.registerTypeIfPresent(classLoader, "org.springframework.http.HttpMethod",
builder -> builder.withMethod("valueOf", List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE));
+
reflectionHints.registerTypeIfPresent(classLoader, "java.net.URI", MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DateToInstantConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/DateToInstantConverter.java
new file mode 100644
index 00000000000..74d1b9c70fd
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/core/convert/support/DateToInstantConverter.java
@@ -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}.
+ *
+ *
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 {
+
+ @Override
+ public Instant convert(Date date) {
+ return date.toInstant();
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java
index e448a4e363e..05017f5a227 100644
--- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java
+++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java
@@ -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());
diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/InstantToDateConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/InstantToDateConverter.java
new file mode 100644
index 00000000000..56185c16790
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/core/convert/support/InstantToDateConverter.java
@@ -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}.
+ *
+ * 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 {
+
+ @Override
+ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
+ return targetType.getType().equals(Date.class);
+ }
+
+ @Override
+ public Date convert(Instant instant) {
+ return Date.from(instant);
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java
index 5bd0f03c578..b69faa2a05b 100644
--- a/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java
+++ b/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java
@@ -17,6 +17,7 @@
package org.springframework.aot.hint.support;
import java.net.URI;
+import java.time.Instant;
import java.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
@@ -24,38 +25,45 @@ import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
-import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
/**
* Tests for {@link ObjectToObjectConverterRuntimeHints}.
*
* @author Sebastien Deleuze
+ * @author Sam Brannen
*/
class ObjectToObjectConverterRuntimeHintsTests {
- private RuntimeHints hints;
+ private final RuntimeHints hints = new RuntimeHints();
+
@BeforeEach
void setup() {
- this.hints = new RuntimeHints();
+ ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories")
- .load(RuntimeHintsRegistrar.class).forEach(registrar -> registrar
- .registerHints(this.hints, ClassUtils.getDefaultClassLoader()));
+ .load(RuntimeHintsRegistrar.class)
+ .forEach(registrar -> registrar.registerHints(this.hints, classLoader));
}
@Test
void javaSqlDateHasHints() throws NoSuchMethodException {
- assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(java.sql.Date.class, "toLocalDate")).accepts(this.hints);
- assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints);
+ assertThat(reflection().onMethodInvocation(java.sql.Date.class, "toLocalDate")).accepts(this.hints);
+ assertThat(reflection().onMethodInvocation(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints);
+ }
+
+ @Test // gh-35156
+ void javaSqlTimestampHasHints() throws NoSuchMethodException {
+ assertThat(reflection().onMethodInvocation(java.sql.Timestamp.class.getMethod("from", Instant.class))).accepts(this.hints);
}
@Test
- void uriHasHints() throws NoSuchMethodException {
- assertThat(RuntimeHintsPredicates.reflection().onType(URI.class)).accepts(this.hints);
+ void uriHasHints() {
+ assertThat(reflection().onType(URI.class)).accepts(this.hints);
}
}
diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java
index c670e10ba1a..09deb980d77 100644
--- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java
+++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java
@@ -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;
@@ -1066,6 +1070,88 @@ class DefaultConversionServiceTests {
}
}
+ @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