Migrate to GraalVM metadata format 1.2.0

This commit adapts RuntimeHints to be compatible with v1.2.0 of the
GraalVM metadata format. The changes are as follows:

* Resources and resource bundles are now defined at the same level. This
is transparent to users.
* Serialization is no longer a top-level concept, but rather an
attribute of a type or proxy. The top-level concept has been deprecated
and the relevant methods have been added.

Closes gh-36379
This commit is contained in:
Stéphane Nicoll
2026-02-24 17:05:50 +01:00
parent a37447e07f
commit 434da06d1f
26 changed files with 767 additions and 474 deletions
@@ -16,7 +16,6 @@
package org.springframework.aot.agent;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.ResourceBundleHint;
@@ -51,7 +50,9 @@ public enum HintType {
/**
* Java serialization hint, as described by {@link org.springframework.aot.hint.JavaSerializationHint}.
*/
JAVA_SERIALIZATION(JavaSerializationHint.class),
@Deprecated(since = "7.0.5", forRemoval = true)
@SuppressWarnings("removal")
JAVA_SERIALIZATION(org.springframework.aot.hint.JavaSerializationHint.class),
/**
* JDK proxies hint, as described by {@link org.springframework.aot.hint.ProxyHints#jdkProxyHints()}.
@@ -27,7 +27,9 @@ import org.jspecify.annotations.Nullable;
*
* @author Brian Clozel
* @since 6.0
* @deprecated in favor of {@link TypeHint}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
public final class JavaSerializationHint implements ConditionalHint {
private final TypeReference type;
@@ -37,10 +37,13 @@ public final class JdkProxyHint implements ConditionalHint {
private final @Nullable TypeReference reachableType;
private final @Nullable Boolean serializable;
private JdkProxyHint(Builder builder) {
this.proxiedInterfaces = List.copyOf(builder.proxiedInterfaces);
this.reachableType = builder.reachableType;
this.serializable = builder.serializable;
}
/**
@@ -75,11 +78,21 @@ public final class JdkProxyHint implements ConditionalHint {
return this.reachableType;
}
/**
* Return whether to register this proxy for Java serialization.
* @return whether to register this proxy for Java serialization.
* @since 7.0.6
*/
public @Nullable Boolean getSerializable() {
return this.serializable;
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof JdkProxyHint that &&
this.proxiedInterfaces.equals(that.proxiedInterfaces) &&
Objects.equals(this.reachableType, that.reachableType)));
Objects.equals(this.reachableType, that.reachableType) &&
Objects.equals(this.serializable, that.serializable)));
}
@Override
@@ -97,6 +110,8 @@ public final class JdkProxyHint implements ConditionalHint {
private @Nullable TypeReference reachableType;
private @Nullable Boolean serializable;
Builder() {
this.proxiedInterfaces = new LinkedList<>();
}
@@ -131,6 +146,17 @@ public final class JdkProxyHint implements ConditionalHint {
return this;
}
/**
* Specify if this proxy should be registered for Java serialization.
* @param serializable whether to register this proxy for Java serialization.
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public Builder javaSerialization(@Nullable Boolean serializable) {
this.serializable = serializable;
return this;
}
/**
* Create a {@link JdkProxyHint} based on the state of this builder.
* @return a JDK proxy hint
@@ -231,6 +231,17 @@ public class ReflectionHints {
typeHint -> typeHint.withMethod(method.getName(), mapParameters(method), mode));
}
/**
* Register the need for Java Serialization on the specified type.
* @param type the type that should be serializable
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public ReflectionHints registerJavaSerialization(Class<?> type) {
return registerType(TypeReference.of(type),
typeHint -> typeHint.withJavaSerialization(true));
}
private List<TypeReference> mapParameters(Executable executable) {
return TypeReference.listOf(executable.getParameterTypes());
}
@@ -37,6 +37,7 @@ public class RuntimeHints {
private final ResourceHints resources = new ResourceHints();
@SuppressWarnings("removal")
private final SerializationHints serialization = new SerializationHints();
private final ProxyHints proxies = new ProxyHints();
@@ -63,7 +64,10 @@ public class RuntimeHints {
/**
* Provide access to serialization-based hints.
* @return serialization hints
* @deprecated in favor of {@link #reflection()}
*/
@SuppressWarnings("removal")
@Deprecated(since = "7.0.6", forRemoval = true)
public SerializationHints serialization() {
return this.serialization;
}
@@ -30,7 +30,10 @@ import org.jspecify.annotations.Nullable;
* @author Stephane Nicoll
* @since 6.0
* @see Serializable
* @deprecated in favor of {@link ReflectionHints}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
@SuppressWarnings("removal")
public class SerializationHints {
private final Set<JavaSerializationHint> javaSerializationHints;
@@ -53,6 +53,8 @@ public final class TypeHint implements ConditionalHint {
private final Set<MemberCategory> memberCategories;
private final @Nullable Boolean serializable;
private TypeHint(Builder builder) {
this.type = builder.type;
@@ -61,6 +63,7 @@ public final class TypeHint implements ConditionalHint {
this.fields = builder.fields.stream().map(FieldHint::new).collect(Collectors.toSet());
this.constructors = builder.constructors.values().stream().map(ExecutableHint.Builder::build).collect(Collectors.toSet());
this.methods = builder.methods.values().stream().map(ExecutableHint.Builder::build).collect(Collectors.toSet());
this.serializable = builder.serializable;
}
/**
@@ -120,6 +123,15 @@ public final class TypeHint implements ConditionalHint {
return this.memberCategories;
}
/**
* Return whether to register this type for Java serialization.
* @return whether to register this type for Java serialization.
* @since 7.0.6
*/
public @Nullable Boolean getSerializable() {
return this.serializable;
}
@Override
public String toString() {
return TypeHint.class.getSimpleName() + "[type=" + this.type + "]";
@@ -153,6 +165,8 @@ public final class TypeHint implements ConditionalHint {
private final Set<MemberCategory> memberCategories = new HashSet<>();
private @Nullable Boolean serializable;
Builder(TypeReference type) {
this.type = type;
}
@@ -260,6 +274,17 @@ public final class TypeHint implements ConditionalHint {
return this;
}
/**
* Specify if this type should be registered for Java serialization.
* @param serializable whether to register this type for Java serialization.
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public Builder withJavaSerialization(@Nullable Boolean serializable) {
this.serializable = serializable;
return this;
}
/**
* Create a {@link TypeHint} based on the state of this builder.
* @return a type hint
@@ -23,6 +23,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
@@ -297,6 +298,30 @@ public class ReflectionHintsPredicates {
return new FieldHintPredicate(field);
}
/**
* Return a predicate that checks whether Java serialization is configured according to the given flag.
* @param type the type to check
* @param serializable the expected serializable flag
* @return the {@link RuntimeHints} predicate
* @since 7.0.6
*/
public Predicate<RuntimeHints> onJavaSerialization(Class<?> type, @Nullable Boolean serializable) {
Assert.notNull(type, "'type' must not be null");
return new SerializationdHintPredicate(TypeReference.of(type), serializable);
}
/**
* Return a predicate that checks whether Java serialization is configured according to the given flag.
* @param typeReference the type reference to check
* @param serializable the expected serializable flag
* @return the {@link RuntimeHints} predicate
* @since 7.0.6
*/
public Predicate<RuntimeHints> onJavaSerialization(TypeReference typeReference, @Nullable Boolean serializable) {
Assert.notNull(typeReference, "'typeReference' must not be null");
return new SerializationdHintPredicate(typeReference, serializable);
}
public static class TypeHintPredicate implements Predicate<RuntimeHints> {
@@ -509,4 +534,26 @@ public class ReflectionHintsPredicates {
}
}
public static class SerializationdHintPredicate implements Predicate<RuntimeHints> {
private final TypeReference typeReference;
private final @Nullable Boolean serializable;
SerializationdHintPredicate(TypeReference typeReference, @Nullable Boolean serializable) {
this.typeReference = typeReference;
this.serializable = serializable;
}
@Override
public boolean test(RuntimeHints runtimeHints) {
TypeHint typeHint = runtimeHints.reflection().getTypeHint(this.typeReference);
if (typeHint == null) {
return false;
}
return Objects.equals(typeHint.getSerializable(), this.serializable);
}
}
}
@@ -46,6 +46,7 @@ public abstract class RuntimeHintsPredicates {
private static final ResourceHintsPredicates resource = new ResourceHintsPredicates();
@SuppressWarnings("removal")
private static final SerializationHintsPredicates serialization = new SerializationHintsPredicates();
private static final ProxyHintsPredicates proxies = new ProxyHintsPredicates();
@@ -73,7 +74,10 @@ public abstract class RuntimeHintsPredicates {
/**
* Return a predicate generator for {@link SerializationHints serialization hints}.
* @return the predicate generator
* @deprecated in favor of {@link #reflection()}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
@SuppressWarnings("removal")
public static SerializationHintsPredicates serialization() {
return serialization;
}
@@ -29,7 +29,10 @@ import org.springframework.util.Assert;
*
* @author Stephane Nicoll
* @since 6.0
* @deprecated in favor of {@link ReflectionHintsPredicates}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
@SuppressWarnings("removal")
public class SerializationHintsPredicates {
SerializationHintsPredicates() {
@@ -44,12 +44,17 @@ public abstract class NativeConfigurationWriter {
}
private boolean hasAnyHint(RuntimeHints hints) {
return (hints.serialization().javaSerializationHints().findAny().isPresent() ||
hints.proxies().jdkProxyHints().findAny().isPresent() ||
return (hints.proxies().jdkProxyHints().findAny().isPresent() ||
hints.reflection().typeHints().findAny().isPresent() ||
hints.resources().resourcePatternHints().findAny().isPresent() ||
hints.resources().resourceBundleHints().findAny().isPresent() ||
hints.jni().typeHints().findAny().isPresent());
hints.jni().typeHints().findAny().isPresent() ||
hasAnyDeprecatedHint(hints));
}
@SuppressWarnings("removal")
private boolean hasAnyDeprecatedHint(RuntimeHints hints) {
return hints.serialization().javaSerializationHints().findAny().isPresent();
}
/**
@@ -22,6 +22,7 @@ import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -32,6 +33,7 @@ import org.springframework.aot.hint.ConditionalHint;
import org.springframework.aot.hint.ExecutableHint;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.FieldHint;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
@@ -62,16 +64,28 @@ class ReflectionHintsAttributes {
};
public List<Map<String, Object>> reflection(RuntimeHints hints) {
List<Map<String, Object>> reflectionHints = new ArrayList<>();
reflectionHints.addAll(hints.reflection().typeHints()
.sorted(Comparator.comparing(TypeHint::getType))
.map(this::toAttributes).toList());
List<Map<String, Object>> reflectionHints = new ArrayList<>(reflectionHints(hints));
reflectionHints.addAll(hints.proxies().jdkProxyHints()
.sorted(JDK_PROXY_HINT_COMPARATOR)
.map(this::toAttributes).toList());
return reflectionHints;
}
@SuppressWarnings("removal")
private List<Map<String, Object>> reflectionHints(RuntimeHints hints) {
Map<TypeReference, Map<String, Object>> allTypeHints = hints.reflection().typeHints()
.map(this::toAttributes).collect(Collectors.toMap((attributes -> (TypeReference) Objects.requireNonNull(attributes.get("type"))),
attributes -> attributes));
hints.serialization().javaSerializationHints().forEach(hint -> {
allTypeHints.merge(hint.getType(), toAttributes(hint),
(currentAttributes, newAttributes) -> {
handleSerializable(currentAttributes, true);
return currentAttributes;
});
});
return allTypeHints.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).map(Map.Entry::getValue).toList();
}
public List<Map<String, Object>> jni(RuntimeHints hints) {
List<Map<String, Object>> jniHints = new ArrayList<>();
jniHints.addAll(hints.jni().typeHints()
@@ -88,6 +102,16 @@ class ReflectionHintsAttributes {
handleFields(attributes, hint.fields());
handleExecutables(attributes, Stream.concat(
hint.constructors(), hint.methods()).sorted().toList());
handleSerializable(attributes, hint.getSerializable());
return attributes;
}
@SuppressWarnings("removal")
private Map<String, Object> toAttributes(JavaSerializationHint serializationHint) {
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, serializationHint);
attributes.put("type", serializationHint.getType());
handleSerializable(attributes, true);
return attributes;
}
@@ -148,7 +172,14 @@ class ReflectionHintsAttributes {
Map<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, hint);
attributes.put("type", Map.of("proxy", hint.getProxiedInterfaces()));
handleSerializable(attributes, hint.getSerializable());
return attributes;
}
private void handleSerializable(Map<String, Object> attributes, @Nullable Boolean serializable) {
if (serializable != null) {
attributes.put("serializable", serializable);
}
}
}
@@ -16,6 +16,7 @@
package org.springframework.aot.nativex;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -35,8 +36,9 @@ import org.springframework.aot.hint.ResourcePatternHints;
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.0
* @see <a href="https://www.graalvm.org/22.1/reference-manual/native-image/Resources/">Accessing Resources in Native Images</a>
* @see <a href="https://www.graalvm.org/22.1/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata/#resources">Accessing Resources in Native Images</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata/#resource-bundles">Accessing Resource Bundles in Native Images</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/overview/BuildConfiguration">Native Image Build Configuration</a>
*/
class ResourceHintsAttributes {
@@ -48,22 +50,21 @@ class ResourceHintsAttributes {
public List<Map<String, Object>> resources(ResourceHints hint) {
return hint.resourcePatternHints()
List<Map<String, Object>> resourceHints = new ArrayList<>();
resourceHints.addAll(hint.resourcePatternHints()
.map(ResourcePatternHints::getIncludes).flatMap(List::stream).distinct()
.sorted(RESOURCE_PATTERN_HINT_COMPARATOR)
.map(this::toAttributes).toList();
}
public List<Map<String, Object>> resourceBundles(ResourceHints hint) {
return hint.resourceBundleHints()
.map(this::toAttributes).toList());
resourceHints.addAll(hint.resourceBundleHints()
.sorted(RESOURCE_BUNDLE_HINT_COMPARATOR)
.map(this::toAttributes).toList();
.map(this::toAttributes).toList());
return resourceHints;
}
private Map<String, Object> toAttributes(ResourceBundleHint hint) {
Map<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, hint);
attributes.put("name", hint.getBaseName());
attributes.put("bundle", hint.getBaseName());
return attributes;
}
@@ -28,8 +28,9 @@ import org.springframework.core.SpringVersion;
* GraalVM {@code native-image} compiler, typically named {@code reachability-metadata.json}.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 7.0
* @see <a href="https://www.graalvm.org/jdk23/reference-manual/native-image/metadata/#specifying-metadata-with-json">GraalVM Reachability Metadata</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata">GraalVM Reachability Metadata</a>
*/
class RuntimeHintsWriter {
@@ -51,14 +52,6 @@ class RuntimeHintsWriter {
if (!resourceHints.isEmpty()) {
document.put("resources", resourceHints);
}
List<Map<String, Object>> resourceBundles = new ResourceHintsAttributes().resourceBundles(hints.resources());
if (!resourceBundles.isEmpty()) {
document.put("bundles", resourceBundles);
}
List<Map<String, Object>> serialization = new SerializationHintsAttributes().toAttributes(hints.serialization());
if (!serialization.isEmpty()) {
document.put("serialization", serialization);
}
writer.writeObject(document);
}
@@ -1,63 +0,0 @@
/*
* 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.aot.nativex;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.aot.hint.ConditionalHint;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.SerializationHints;
/**
* Collect {@link SerializationHints} as map attributes ready for JSON serialization for the GraalVM
* {@code native-image} compiler.
*
* @author Sebastien Deleuze
* @author Stephane Nicoll
* @author Brian Clozel
* @see <a href="https://www.graalvm.org/jdk23/reference-manual/native-image/overview/BuildConfiguration/">Native Image Build Configuration</a>
*/
class SerializationHintsAttributes {
private static final Comparator<JavaSerializationHint> JAVA_SERIALIZATION_HINT_COMPARATOR =
Comparator.comparing(JavaSerializationHint::getType);
public List<Map<String, Object>> toAttributes(SerializationHints hints) {
return hints.javaSerializationHints()
.sorted(JAVA_SERIALIZATION_HINT_COMPARATOR)
.map(this::toAttributes).toList();
}
private Map<String, Object> toAttributes(JavaSerializationHint serializationHint) {
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, serializationHint);
attributes.put("type", serializationHint.getType());
return attributes;
}
private void handleCondition(Map<String, Object> attributes, ConditionalHint hint) {
if (hint.getReachableType() != null) {
Map<String, Object> conditionAttributes = new LinkedHashMap<>();
conditionAttributes.put("typeReached", hint.getReachableType());
attributes.put("condition", conditionAttributes);
}
}
}
@@ -25,5 +25,7 @@ import java.io.Serializable
* @author Sebastien Deleuze
* @since 6.0.5
*/
@Suppress("removal", "DEPRECATION")
@Deprecated(level = DeprecationLevel.WARNING, message = "Use ReflectionHints")
inline fun <reified T : Serializable> SerializationHints.registerType(noinline serializationHint: (JavaSerializationHint.Builder) -> Unit = {}) =
registerType(T::class.java, serializationHint::invoke)
@@ -80,6 +80,18 @@ class ProxyHintsTests {
assertThat(this.proxyHints.jdkProxyHints()).singleElement().satisfies(proxiedInterfaces(Function.class));
}
@Test
void registerJdkProxyWithJavaSerialization() {
this.proxyHints.registerJdkProxy(hint -> {
hint.proxiedInterfaces(TypeReference.of("com.example.Test"));
hint.javaSerialization(true);
});
assertThat(this.proxyHints.jdkProxyHints()).singleElement().satisfies(hint -> {
assertThat(hint.getProxiedInterfaces()).containsExactly(TypeReference.of("com.example.Test"));
assertThat(hint.getSerializable()).isTrue();
});
}
private static Consumer<JdkProxyHint.Builder> springProxy(String proxiedInterface) {
return builder -> builder.proxiedInterfaces(toTypeReferences(
@@ -53,6 +53,8 @@ class RuntimeHintsTests {
}
@Test
@Deprecated
@SuppressWarnings("removal")
void javaSerializationHintWithClass() {
this.hints.serialization().registerType(String.class);
assertThat(this.hints.serialization().javaSerializationHints().map(JavaSerializationHint::getType))
@@ -27,6 +27,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Stephane Nicoll
*/
@Deprecated
@SuppressWarnings("removal")
class SerializationHintsTests {
private final SerializationHints serializationHints = new SerializationHints();
@@ -435,6 +435,36 @@ class ReflectionHintsPredicatesTests {
}
@Nested
class JavaSerialization {
@Test
void javaSerializationMatchesRegisteredClass() {
runtimeHints.reflection().registerJavaSerialization(SampleClass.class);
assertPredicateMatches(reflection.onJavaSerialization(SampleClass.class, true));
}
@Test
void javaSerializationMatchesRegisteredTypeReference() {
runtimeHints.reflection().registerType(TypeReference.of(SampleClass.class),
type -> type.withJavaSerialization(false));
assertPredicateMatches(reflection.onJavaSerialization(TypeReference.of(SampleClass.class), false));
}
@Test
void javaSerializationDoesNotMatchOnMissingType() {
runtimeHints.reflection().registerJavaSerialization(SampleClass.class);
assertPredicateDoesNotMatch(reflection.onJavaSerialization(Integer.class, true));
}
@Test
void javaSerializationDoesNotMatchOnInvalidSerializationFlag() {
runtimeHints.reflection().registerJavaSerialization(SampleClass.class);
assertPredicateDoesNotMatch(reflection.onJavaSerialization(SampleClass.class, false));
}
}
private void assertPredicateMatches(Predicate<RuntimeHints> predicate) {
assertThat(predicate).accepts(this.runtimeHints);
}
@@ -28,6 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Stephane Nicoll
*/
@Deprecated
@SuppressWarnings("removal")
class SerializationHintsPredicatesTests {
private final SerializationHintsPredicates serialization = new SerializationHintsPredicates();
@@ -35,7 +35,6 @@ import org.springframework.aot.hint.ProxyHints;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.ResourceHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.SerializationHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.codec.StringDecoder;
@@ -47,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sebastien Deleuze
* @author Janne Valkealahti
* @author Sam Brannen
* @author Stephane Nicoll
*/
class FileNativeConfigurationWriterTests {
@@ -66,15 +66,15 @@ class FileNativeConfigurationWriterTests {
void serializationConfig() throws IOException, JSONException {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
SerializationHints serializationHints = hints.serialization();
serializationHints.registerType(Integer.class);
serializationHints.registerType(Long.class);
ReflectionHints reflectionHints = hints.reflection();
reflectionHints.registerJavaSerialization(Integer.class);
reflectionHints.registerJavaSerialization(Long.class);
generator.write(hints);
assertEquals("""
{
"serialization": [
{ "type": "java.lang.Integer" },
{ "type": "java.lang.Long" }
"reflection": [
{ "type": "java.lang.Integer", "serializable": true },
{ "type": "java.lang.Long", "serializable": true }
]
}
""");
@@ -65,7 +65,7 @@ class RuntimeHintsWriterTests {
builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.graalvm.org/", "classpath:org/springframework/aot/nativex/"))
);
SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build();
JSON_SCHEMA = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.graalvm.org/reachability-metadata-schema-v1.0.0.json"), config);
JSON_SCHEMA = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.graalvm.org/reachability-metadata-schema-v1.2.0.json"), config);
}
@Nested
@@ -89,7 +89,8 @@ class RuntimeHintsWriterTests {
.withField("DEFAULT_CHARSET")
.withField("defaultCharset")
.withField("aScore")
.withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE));
.withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE)
.withJavaSerialization(true));
assertEquals("""
{
"reflection": [
@@ -110,7 +111,8 @@ class RuntimeHintsWriterTests {
],
"methods": [
{ "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
]
],
"serializable": true
}
]
}
@@ -204,6 +206,23 @@ class RuntimeHintsWriterTests {
""", hints);
}
@Test
void serializationEnabled() throws JSONException {
RuntimeHints hints = new RuntimeHints();
hints.reflection().registerType(Integer.class, builder -> builder.withJavaSerialization(true));
assertEquals("""
{
"reflection": [
{
"type": "java.lang.Integer",
"serializable": true
}
]
}
""", hints);
}
@Test
void ignoreLambda() throws JSONException {
Runnable anonymousRunnable = () -> {};
@@ -409,15 +428,17 @@ class RuntimeHintsWriterTests {
hints.resources().registerResourceBundle("com.example.message");
assertEquals("""
{
"bundles": [
{ "name": "com.example.message"},
{ "name": "com.example.message2"}
"resources": [
{ "bundle": "com.example.message"},
{ "bundle": "com.example.message2"}
]
}""", hints);
}
}
@Nested
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
class SerializationHintsTests {
@Test
@@ -432,8 +453,8 @@ class RuntimeHintsWriterTests {
hints.serialization().registerType(TypeReference.of(String.class));
assertEquals("""
{
"serialization": [
{ "type": "java.lang.String" }
"reflection": [
{ "type": "java.lang.String", "serializable": true }
]
}
""", hints);
@@ -447,9 +468,9 @@ class RuntimeHintsWriterTests {
.registerType(TypeReference.of(String.class));
assertEquals("""
{
"serialization": [
{ "type": "java.lang.String" },
{ "type": "org.springframework.core.env.Environment" }
"reflection": [
{ "type": "java.lang.String", "serializable": true },
{ "type": "org.springframework.core.env.Environment", "serializable": true }
]
}
""", hints);
@@ -462,8 +483,8 @@ class RuntimeHintsWriterTests {
builder -> builder.onReachableType(TypeReference.of("org.example.Test")));
assertEquals("""
{
"serialization": [
{ "condition": { "typeReached": "org.example.Test" }, "type": "java.lang.String" }
"reflection": [
{ "condition": { "typeReached": "org.example.Test" }, "type": "java.lang.String", "serializable": true }
]
}
""", hints);
@@ -567,6 +588,27 @@ class RuntimeHintsWriterTests {
""", hints);
}
@Test
void shouldWriteSerialization() throws JSONException {
RuntimeHints hints = new RuntimeHints();
hints.proxies().registerJdkProxy(hint -> {
hint.proxiedInterfaces(Function.class);
hint.javaSerialization(true);
});
assertEquals("""
{
"reflection": [
{
"type": {
"proxy": ["java.util.function.Function"]
},
"serializable": true
}
]
}
""", hints);
}
}
private void assertEquals(String expectedString, RuntimeHints hints) throws JSONException {
@@ -27,6 +27,7 @@ import java.util.function.Consumer
*
* @author Sebastien Deleuze
*/
@Suppress("REMOVAL", "DEPRECATION")
class SerializationHintsExtensionsTests {
private val serializationHints = mockk<SerializationHints>()
@@ -1,362 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://www.graalvm.org/reachability-metadata-schema-v1.0.0.json",
"title": "JSON schema for the reachability metadata used by GraalVM Native Image",
"type": "object",
"default": {},
"properties": {
"comment": {
"title": "A comment applying to the whole file (e.g., generation date, author, etc.)",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"default": ""
},
"reflection": {
"title": "Metadata to ensure elements are reachable through reflection",
"$ref": "#/$defs/reflection"
},
"jni": {
"title": "Metadata to ensure elements are reachable through JNI",
"$ref": "#/$defs/reflection"
},
"serialization": {
"title": "Metadata for types that are serialized or deserialized at run time. The types must extend 'java.io.Serializable'.",
"type": "array",
"default": [],
"items": {
"title": "Enables serializing and deserializing objects of the class specified by <type>",
"type": "object",
"properties": {
"reason": {
"title": "Reason for the type's inclusion in the serialization metadata",
"$ref": "#/$defs/reason"
},
"condition": {
"title": "Condition under which the class should be registered for serialization",
"$ref": "#/$defs/condition"
},
"type": {
"title": "Type descriptor of the class that should be registered for serialization",
"$ref": "#/$defs/type"
},
"customTargetConstructorClass": {
"title": "Fully qualified name of the class whose constructor should be used to serialize the class specified by <type>",
"type": "string"
}
},
"required": [
"type"
],
"additionalProperties": false
}
},
"resources": {
"title": "Metadata to ensure resources are available",
"type": "array",
"default": [],
"items": {
"title": "Resource that should be available",
"type": "object",
"properties": {
"reason": {
"title": "Reason for the resource's inclusion in the metadata",
"$ref": "#/$defs/reason"
},
"condition": {
"title": "Condition under which the resource should be registered for runtime access",
"$ref": "#/$defs/condition"
},
"module": {
"title": "Module containing the resource",
"type": "string",
"default": ""
},
"glob": {
"title": "Resource name or pattern matching multiple resources (accepts * and ** wildcards)",
"type": "string"
}
},
"required": [
"glob"
],
"additionalProperties": false
}
},
"bundles": {
"title": "Metadata to ensure resource bundles are available",
"type": "array",
"default": [],
"items": {
"title": "Resource bundle that should be available",
"type": "object",
"properties": {
"reason": {
"title": "Reason for the resource bundle's inclusion in the metadata",
"$ref": "#/$defs/reason"
},
"condition": {
"title": "Condition under which the resource bundle should be registered for runtime access",
"$ref": "#/$defs/condition"
},
"name": {
"title": "Name of the resource bundle",
"type": "string"
},
"locales": {
"title": "List of locales that should be registered for this resource bundle",
"type": "array",
"default": [],
"items": {
"type": "string"
}
}
},
"required": [
"name"
],
"additionalProperties": false
}
}
},
"required": [],
"additionalProperties": false,
"$defs": {
"reflection": {
"type": "array",
"default": [],
"items": {
"title": "Elements that should be registered for reflection for a specified type",
"type": "object",
"properties": {
"reason": {
"title": "Reason for the element's inclusion",
"$ref": "#/$defs/reason"
},
"condition": {
"title": "Condition under which the class should be registered for reflection",
"$ref": "#/$defs/condition"
},
"type": {
"title": "Type descriptor of the class that should be registered for reflection",
"$ref": "#/$defs/type"
},
"methods": {
"title": "List of methods that should be registered for the type declared in <type>",
"type": "array",
"default": [],
"items": {
"title": "Method descriptor of the method that should be registered for reflection",
"$ref": "#/$defs/method"
}
},
"fields": {
"title": "List of class fields that can be read or written to for the type declared in <type>",
"type": "array",
"default": [],
"items": {
"title": "Field descriptor of the field that should be registered for reflection",
"$ref": "#/$defs/field"
}
},
"allDeclaredMethods": {
"title": "Register all declared methods from the type for reflective invocation",
"type": "boolean",
"default": false
},
"allDeclaredFields": {
"title": "Register all declared fields from the type for reflective access",
"type": "boolean",
"default": false
},
"allDeclaredConstructors": {
"title": "Register all declared constructors from the type for reflective invocation",
"type": "boolean",
"default": false
},
"allPublicMethods": {
"title": "Register all public methods from the type for reflective invocation",
"type": "boolean",
"default": false
},
"allPublicFields": {
"title": "Register all public fields from the type for reflective access",
"type": "boolean",
"default": false
},
"allPublicConstructors": {
"title": "Register all public constructors from the type for reflective invocation",
"type": "boolean",
"default": false
},
"unsafeAllocated": {
"title": "Allow objects of this class to be instantiated with a call to jdk.internal.misc.Unsafe#allocateInstance or JNI's AllocObject",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
},
"jni": {
"type": "array",
"default": [],
"items": {
"title": "Elements that should be registered for JNI for a specified type",
"type": "object",
"properties": {
"reason": {
"title": "Reason for the element's inclusion",
"$ref": "#/$defs/reason"
},
"condition": {
"title": "Condition under which the class should be registered for JNI",
"$ref": "#/$defs/condition"
},
"type": {
"title": "Type descriptor of the class that should be registered for JNI",
"$ref": "#/$defs/type"
},
"methods": {
"title": "List of methods that should be registered for the type declared in <type>",
"type": "array",
"default": [],
"items": {
"title": "Method descriptor of the method that should be registered for JNI",
"$ref": "#/$defs/method"
}
},
"fields": {
"title": "List of class fields that can be read or written to for the type declared in <type>",
"type": "array",
"default": [],
"items": {
"title": "Field descriptor of the field that should be registered for JNI",
"$ref": "#/$defs/field"
}
},
"allDeclaredMethods": {
"title": "Register all declared methods from the type for JNI access",
"type": "boolean",
"default": false
},
"allDeclaredFields": {
"title": "Register all declared fields from the type for JNI access",
"type": "boolean",
"default": false
},
"allDeclaredConstructors": {
"title": "Register all declared constructors from the type for JNI access",
"type": "boolean",
"default": false
},
"allPublicMethods": {
"title": "Register all public methods from the type for JNI access",
"type": "boolean",
"default": false
},
"allPublicFields": {
"title": "Register all public fields from the type for JNI access",
"type": "boolean",
"default": false
},
"allPublicConstructors": {
"title": "Register all public constructors from the type for JNI access",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
},
"reason": {
"type": "string",
"default": []
},
"condition": {
"title": "Condition used by GraalVM Native Image metadata files",
"type": "object",
"properties": {
"typeReached": {
"title": "Type descriptor of a class that must be reached in order to enable the corresponding registration",
"$ref": "#/$defs/type"
}
},
"required": [
"typeReached"
],
"additionalProperties": false
},
"type": {
"title": "Type descriptors used by GraalVM Native Image metadata files",
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"proxy": {
"title": "List of interfaces defining the proxy class",
"type": "array",
"default": [],
"items": {
"title": "Fully qualified name of the interface defining the proxy class",
"type": "string"
}
}
},
"required": [
"proxy"
],
"additionalProperties": false
}
]
},
"method": {
"title": "Method descriptors used by GraalVM Native Image metadata files",
"type": "object",
"properties": {
"name": {
"title": "Method name that should be registered for this class",
"type": "string"
},
"parameterTypes": {
"default": [],
"items": {
"title": "List of the method's parameter types",
"type": "string"
},
"type": "array"
}
},
"required": [
"name"
],
"additionalProperties": false
},
"field": {
"title": "Field descriptors used by GraalVM Native Image metadata files",
"type": "object",
"properties": {
"name": {
"title": "Name of the field that should be registered for reflection",
"type": "string"
}
},
"required": [
"name"
],
"additionalProperties": false
}
}
}
@@ -0,0 +1,469 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/reachability-metadata-schema-v1.2.0.json",
"title": "JSON schema for the reachability metadata used by GraalVM Native Image",
"version": "1.2.0",
"type": "object",
"default": {},
"properties": {
"comment": {
"title": "Comment(s) applying to the whole file (e.g., generation date, author, etc.)",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"default": ""
},
"reflection": {
"title": "Metadata to ensure elements are accessible via reflection",
"type": "array",
"default": [],
"items": {
"title": "Elements that should be registered for reflection for a specified type",
"type": "object",
"required": [
"type"
],
"properties": {
"reason": {
"$ref": "#/$defs/reason"
},
"condition": {
"$ref": "#/$defs/condition"
},
"type": {
"title": "Type descriptor of the class that should be registered for reflection",
"$ref": "#/$defs/type"
},
"methods": {
"title": "List of methods that should be registered for the type declared in <type>",
"type": "array",
"default": [],
"items": {
"title": "Method descriptor of the method that should be registered for reflection",
"$ref": "#/$defs/method"
}
},
"fields": {
"title": "List of class fields that can be read or written to for the type declared in <type>",
"type": "array",
"default": [],
"items": {
"title": "Field descriptor of the field that should be registered for reflection",
"type": "object",
"properties": {
"name": {
"title": "Name of the field that should be registered for reflection",
"type": "string",
"pattern": "^[^.;\\[/]+$"
}
},
"required": [
"name"
],
"additionalProperties": false
}
},
"allDeclaredMethods": {
"title": "Register all declared methods from the type for reflective invocation",
"type": "boolean",
"default": false
},
"allDeclaredFields": {
"title": "Register all declared fields from the type for reflective access",
"type": "boolean",
"default": false
},
"allDeclaredConstructors": {
"title": "Register all declared constructors from the type for reflective invocation",
"type": "boolean",
"default": false
},
"allPublicMethods": {
"title": "Register all public methods from the type for reflective invocation",
"type": "boolean",
"default": false
},
"allPublicFields": {
"title": "Register all public fields from the type for reflective access",
"type": "boolean",
"default": false
},
"allPublicConstructors": {
"title": "Register all public constructors from the type for reflective invocation",
"type": "boolean",
"default": false
},
"unsafeAllocated": {
"title": "Allow objects of this class to be instantiated with a call to jdk.internal.misc.Unsafe#allocateInstance or JNI's AllocObject",
"type": "boolean",
"default": false
},
"serializable": {
"title": "Allow objects of this class to be serialized and deserialized",
"type": "boolean",
"default": false
},
"jniAccessible": {
"title": "Register the type, including all registered fields and methods, for runtime JNI access",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
},
"resources": {
"title": "Metadata to ensure resources are accessible",
"type": "array",
"default": [],
"items": {
"oneOf": [
{
"title": "Resource that should be accessible",
"type": "object",
"properties": {
"reason": {
"$ref": "#/$defs/reason"
},
"condition": {
"$ref": "#/$defs/condition"
},
"module": {
"title": "Module containing the resource",
"type": "string",
"default": ""
},
"glob": {
"title": "Resource name or pattern matching multiple resources (accepts * and ** wildcards)",
"type": "string"
}
},
"required": [
"glob"
],
"additionalProperties": false
},
{
"title": "Resource bundle that should be available",
"type": "object",
"properties": {
"reason": {
"$ref": "#/$defs/reason"
},
"condition": {
"$ref": "#/$defs/condition"
},
"module": {
"title": "Module containing the resource bundle",
"type": "string",
"default": ""
},
"bundle": {
"title": "Resource bundle name",
"type": "string"
}
},
"required": [
"bundle"
],
"additionalProperties": false
}
]
}
},
"foreign": {
"properties": {
"downcalls": {
"default": [],
"items": {
"properties": {
"reason": {
"$ref": "#/$defs/reason"
},
"condition": {
"$ref": "#/$defs/condition"
},
"returnType": {
"type": "string",
"title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)"
},
"parameterTypes": {
"default": [],
"items": {
"type": "string",
"title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)"
},
"type": "array",
"title": "List of the function descriptor's parameter types"
},
"options": {
"type": "object",
"title": "Linker options (see `java.lang.foreign.Linker.Option`)",
"properties": {
"captureCallState": {
"type": "boolean",
"title": "Specifies whether a call state should be captured. The specific states to capture are determined at run time. See also: `java.lang.foreign.Linker.Option.captureCallState`"
},
"critical": {
"type": "object",
"title": "See `java.lang.foreign.Linker.Option.critical`",
"properties": {
"allowHeapAccess": {
"type": "boolean"
}
},
"additionalProperties": false
},
"firstVariadicArg": {
"type": "integer",
"title": "See `java.lang.foreign.Linker.Option.firstVariadicArg`"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"returnType",
"parameterTypes"
],
"type": "object",
"title": "Function descriptor to be registered for a downcall"
},
"type": "array",
"title": "List of function descriptors that should be registered for downcalls"
},
"upcalls": {
"default": [],
"items": {
"properties": {
"reason": {
"$ref": "#/$defs/reason"
},
"condition": {
"$ref": "#/$defs/condition"
},
"returnType": {
"type": "string",
"title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)"
},
"parameterTypes": {
"default": [],
"items": {
"type": "string",
"title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)"
},
"type": "array",
"title": "List of the function descriptor's parameter types"
},
"options": {
"type": "object",
"title": "Linker options (see `java.lang.foreign.Linker.Option`)",
"description": "Currently, no linker options are allowed for upcalls. This may change in the future.",
"properties": {},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"returnType",
"parameterTypes"
],
"type": "object",
"title": "Function descriptor to be registered for an upcall"
},
"type": "array",
"title": "List of function descriptors that should be registered for upcalls"
},
"directUpcalls": {
"default": [],
"items": {
"properties": {
"reason": {
"$ref": "#/$defs/reason"
},
"condition": {
"$ref": "#/$defs/condition"
},
"class": {
"$ref": "#/$defs/className",
"title": "Fully-qualified class name (e.g., `org.package.OuterClass$InnerClass`)"
},
"method": {
"type": "string",
"title": "Method name"
},
"returnType": {
"type": "string",
"title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)"
},
"parameterTypes": {
"default": [],
"items": {
"type": "string",
"title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)"
},
"type": "array",
"title": "List of the function descriptor's parameter types"
},
"options": {
"type": "object",
"title": "Linker options (see `java.lang.foreign.Linker.Option`)",
"description": "Currently, no linker options are allowed for direct upcalls. This may change in the future.",
"properties": {},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"class",
"method"
],
"type": "object",
"title": "Java method and function descriptor to be registered for a direct upcall"
},
"type": "array",
"title": "List of Java methods and function descriptors that should be registered for direct upcalls"
}
},
"type": "object",
"additionalProperties": false,
"title": "JSON schema for the FFM API configuration used by GraalVM Native Image",
"description": "For a description and examples of writing an FFM API configuration, see: https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/FFM-API.md"
}
},
"required": [],
"additionalProperties": false,
"$defs": {
"className": {
"type": "string",
"pattern": "^[^.;\\[/]+(\\.[^.;\\[/]+)*$"
},
"typeName": {
"type": "string",
"pattern": "^[^.;\\[/]+(\\.[^.;\\[/]+)*(\\[])*$"
},
"reason": {
"title": "Reason(s) for including this element (e.g., needed for establishing connections, or needed by method X#Y for task Z)",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"default": ""
},
"condition": {
"title": "Condition used to determine if the element will be accessible at run time",
"type": "object",
"properties": {
"typeReached": {
"title": "Type descriptor of a class that must be reached in order to allow access to the element",
"$ref": "#/$defs/type"
}
},
"required": [
"typeReached"
],
"additionalProperties": false
},
"type": {
"title": "Type descriptors used by GraalVM Native Image metadata files",
"oneOf": [
{
"$ref": "#/$defs/typeName"
},
{
"type": "object",
"properties": {
"proxy": {
"title": "A list of interfaces defining the proxy class",
"type": "array",
"items": {
"title": "Fully-qualified name of the interface defining the proxy class",
"$ref": "#/$defs/className"
}
},
"lambda": {
"title": "Lambda class descriptor",
"type": "object",
"properties": {
"declaringClass": {
"title": "The class in which the lambda class is defined",
"$ref": "#/$defs/className"
},
"declaringMethod": {
"title": "The method in which the lambda class is defined",
"$ref": "#/$defs/method"
},
"interfaces": {
"title": "Non-empty list of interfaces implemented by the lambda class",
"type": "array",
"items": {
"title": "Fully-qualified name of the interface implemented by the lambda class",
"$ref": "#/$defs/className"
},
"minItems": 1
}
},
"required": [
"declaringClass",
"interfaces"
],
"additionalProperties": false
}
},
"oneOf": [
{
"required": [
"proxy"
]
},
{
"required": [
"lambda"
]
}
],
"additionalProperties": false
}
]
},
"method": {
"title": "Method descriptors used by GraalVM Native Image metadata files",
"type": "object",
"properties": {
"name": {
"title": "Method name that should be registered for this class",
"type": "string",
"pattern": "^[^.;\\[/]+$"
},
"parameterTypes": {
"items": {
"title": "List of the method's parameter types",
"$ref": "#/$defs/typeName"
},
"type": "array"
}
},
"required": [
"name",
"parameterTypes"
],
"additionalProperties": false
}
}
}