From fec2ed5540d075fc9364302fc82c9d7de3745ee6 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 29 Nov 2024 14:43:55 +0100 Subject: [PATCH] Implement new GraalVM reachability metadata format As of GraalVM 23, a new and simplified reachability metadata format is available. Metadata now consists of a single "reachability-metadata.json" file that contains all the information previously spread in multiple files. The new format does not include some introspection flags, as they're now automatically included when a hint is registered against a type. Also, "typeReachable" has been renamed as "typeReached" to highlight the fact that the event considered is the static initialization of the type, not when the static analysis performed during native compilation is reaching the type. This new format ships with a JSON schema, which this commit is tested against. See gh-33847 --- framework-platform/framework-platform.gradle | 1 + spring-core/spring-core.gradle | 1 + .../nativex/NativeConfigurationWriter.java | 61 +- .../aot/nativex/ProxyHintsWriter.java | 73 --- ...er.java => ReflectionHintsAttributes.java} | 90 +-- ...iter.java => ResourceHintsAttributes.java} | 38 +- .../aot/nativex/RuntimeHintsWriter.java | 66 ++ ...java => SerializationHintsAttributes.java} | 23 +- .../FileNativeConfigurationWriterTests.java | 127 ++-- .../aot/nativex/ProxyHintsWriterTests.java | 112 ---- .../nativex/ReflectionHintsWriterTests.java | 295 --------- .../aot/nativex/ResourceHintsWriterTests.java | 190 ------ .../aot/nativex/RuntimeHintsWriterTests.java | 594 ++++++++++++++++++ .../SerializationHintsWriterTests.java | 81 --- .../reachability-metadata-schema-v1.0.0.json | 362 +++++++++++ 15 files changed, 1170 insertions(+), 944 deletions(-) delete mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java rename spring-core/src/main/java/org/springframework/aot/nativex/{ReflectionHintsWriter.java => ReflectionHintsAttributes.java} (57%) rename spring-core/src/main/java/org/springframework/aot/nativex/{ResourceHintsWriter.java => ResourceHintsAttributes.java} (68%) create mode 100644 spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java rename spring-core/src/main/java/org/springframework/aot/nativex/{SerializationHintsWriter.java => SerializationHintsAttributes.java} (70%) delete mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java delete mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java delete mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java delete mode 100644 spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java create mode 100644 spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 998c4918557..804c6fa58ab 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -34,6 +34,7 @@ dependencies { api("com.google.protobuf:protobuf-java-util:4.28.3") api("com.h2database:h2:2.3.232") api("com.jayway.jsonpath:json-path:2.9.0") + api("com.networknt:json-schema-validator:1.5.3") api("com.oracle.database.jdbc:ojdbc11:21.9.0.0") api("com.rometools:rome:1.19.0") api("com.squareup.okhttp3:mockwebserver:3.14.9") diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a08d0e16672..ebad4fa180a 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -104,6 +104,7 @@ dependencies { testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") testImplementation("org.mockito:mockito-core") + testImplementation("com.networknt:json-schema-validator"); testImplementation("org.skyscreamer:jsonassert") testImplementation("org.xmlunit:xmlunit-assertj") testImplementation("org.xmlunit:xmlunit-matchers") diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java index 5ef7d21e265..d0886760c47 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -18,11 +18,7 @@ package org.springframework.aot.nativex; import java.util.function.Consumer; -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; /** * Write {@link RuntimeHints} as GraalVM native configuration. @@ -30,8 +26,9 @@ import org.springframework.aot.hint.SerializationHints; * @author Sebastien Deleuze * @author Stephane Nicoll * @author Janne Valkealahti + * @author Brian Clozel * @since 6.0 - * @see Native Image Build Configuration + * @see Native Image Build Configuration */ public abstract class NativeConfigurationWriter { @@ -40,24 +37,21 @@ public abstract class NativeConfigurationWriter { * @param hints the hints to handle */ public void write(RuntimeHints hints) { - if (hints.serialization().javaSerializationHints().findAny().isPresent()) { - writeSerializationHints(hints.serialization()); - } - if (hints.proxies().jdkProxyHints().findAny().isPresent()) { - writeProxyHints(hints.proxies()); - } - if (hints.reflection().typeHints().findAny().isPresent()) { - writeReflectionHints(hints.reflection()); - } - if (hints.resources().resourcePatternHints().findAny().isPresent() || - hints.resources().resourceBundleHints().findAny().isPresent()) { - writeResourceHints(hints.resources()); - } - if (hints.jni().typeHints().findAny().isPresent()) { - writeJniHints(hints.jni()); + if (hasAnyHint(hints)) { + writeTo("reachability-metadata.json", + writer -> new RuntimeHintsWriter().write(writer, hints)); } } + private boolean hasAnyHint(RuntimeHints hints) { + return (hints.serialization().javaSerializationHints().findAny().isPresent() + || 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()); + } + /** * Write the specified GraalVM native configuration file, using the * provided {@link BasicJsonWriter}. @@ -66,29 +60,4 @@ public abstract class NativeConfigurationWriter { */ protected abstract void writeTo(String fileName, Consumer writer); - private void writeSerializationHints(SerializationHints hints) { - writeTo("serialization-config.json", writer -> - SerializationHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeProxyHints(ProxyHints hints) { - writeTo("proxy-config.json", writer -> - ProxyHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeReflectionHints(ReflectionHints hints) { - writeTo("reflect-config.json", writer -> - ReflectionHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeResourceHints(ResourceHints hints) { - writeTo("resource-config.json", writer -> - ResourceHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeJniHints(ReflectionHints hints) { - writeTo("jni-config.json", writer -> - ReflectionHintsWriter.INSTANCE.write(writer, hints)); - } - } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java deleted file mode 100644 index 51cd3132efa..00000000000 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2002-2023 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.Map; -import java.util.stream.Collectors; - -import org.springframework.aot.hint.JdkProxyHint; -import org.springframework.aot.hint.ProxyHints; -import org.springframework.aot.hint.TypeReference; - -/** - * Write {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON - * output expected by the GraalVM {@code native-image} compiler, typically named - * {@code proxy-config.json}. - * - * @author Sebastien Deleuze - * @author Stephane Nicoll - * @author Brian Clozel - * @since 6.0 - * @see Dynamic Proxy in Native Image - * @see Native Image Build Configuration - */ -class ProxyHintsWriter { - - public static final ProxyHintsWriter INSTANCE = new ProxyHintsWriter(); - - private static final Comparator JDK_PROXY_HINT_COMPARATOR = - (left, right) -> { - String leftSignature = left.getProxiedInterfaces().stream() - .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); - String rightSignature = right.getProxiedInterfaces().stream() - .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); - return leftSignature.compareTo(rightSignature); - }; - - public void write(BasicJsonWriter writer, ProxyHints hints) { - writer.writeArray(hints.jdkProxyHints().sorted(JDK_PROXY_HINT_COMPARATOR) - .map(this::toAttributes).toList()); - } - - private Map toAttributes(JdkProxyHint hint) { - Map attributes = new LinkedHashMap<>(); - handleCondition(attributes, hint); - attributes.put("interfaces", hint.getProxiedInterfaces()); - return attributes; - } - - private void handleCondition(Map attributes, JdkProxyHint hint) { - if (hint.getReachableType() != null) { - Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); - attributes.put("condition", conditionAttributes); - } - } - -} diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java similarity index 57% rename from spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java index 3d678b0d85c..4d7b2b27a46 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -16,48 +16,72 @@ package org.springframework.aot.nativex; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; +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.JdkProxyHint; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeHint; +import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; /** - * Write {@link ReflectionHints} to the JSON output expected by the GraalVM - * {@code native-image} compiler, typically named {@code reflect-config.json} - * or {@code jni-config.json}. + * Collect {@link ReflectionHints} as map attributes ready for JSON serialization for the GraalVM + * {@code native-image} compiler. * * @author Sebastien Deleuze * @author Stephane Nicoll * @author Janne Valkealahti - * @since 6.0 - * @see Reflection Use in Native Images - * @see Java Native Interface (JNI) in Native Image - * @see Native Image Build Configuration + * @see Reflection Use in Native Images + * @see Java Native Interface (JNI) in Native Image + * @see Native Image Build Configuration */ -class ReflectionHintsWriter { +class ReflectionHintsAttributes { - public static final ReflectionHintsWriter INSTANCE = new ReflectionHintsWriter(); + private static final Comparator JDK_PROXY_HINT_COMPARATOR = + (left, right) -> { + String leftSignature = left.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + String rightSignature = right.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + return leftSignature.compareTo(rightSignature); + }; - public void write(BasicJsonWriter writer, ReflectionHints hints) { - writer.writeArray(hints.typeHints() + public List> reflection(RuntimeHints hints) { + List> reflectionHints = new ArrayList<>(); + reflectionHints.addAll(hints.reflection().typeHints() .sorted(Comparator.comparing(TypeHint::getType)) .map(this::toAttributes).toList()); + reflectionHints.addAll(hints.proxies().jdkProxyHints() + .sorted(JDK_PROXY_HINT_COMPARATOR) + .map(this::toAttributes).toList()); + return reflectionHints; + } + + public List> jni(RuntimeHints hints) { + List> jniHints = new ArrayList<>(); + jniHints.addAll(hints.jni().typeHints() + .sorted(Comparator.comparing(TypeHint::getType)) + .map(this::toAttributes).toList()); + return jniHints; } private Map toAttributes(TypeHint hint) { Map attributes = new LinkedHashMap<>(); - attributes.put("name", hint.getType()); + attributes.put("type", hint.getType()); handleCondition(attributes, hint); handleCategories(attributes, hint.getMemberCategories()); handleFields(attributes, hint.fields()); @@ -66,33 +90,23 @@ class ReflectionHintsWriter { return attributes; } - private void handleCondition(Map attributes, TypeHint hint) { + private void handleCondition(Map attributes, ConditionalHint hint) { if (hint.getReachableType() != null) { - Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); - attributes.put("condition", conditionAttributes); + attributes.put("condition", Map.of("typeReached", hint.getReachableType())); } } private void handleFields(Map attributes, Stream fields) { addIfNotEmpty(attributes, "fields", fields .sorted(Comparator.comparing(FieldHint::getName, String::compareToIgnoreCase)) - .map(this::toAttributes).toList()); - } - - private Map toAttributes(FieldHint hint) { - Map attributes = new LinkedHashMap<>(); - attributes.put("name", hint.getName()); - return attributes; + .map(fieldHint -> Map.of("name", fieldHint.getName())) + .toList()); } private void handleExecutables(Map attributes, List hints) { addIfNotEmpty(attributes, "methods", hints.stream() .filter(h -> h.getMode().equals(ExecutableMode.INVOKE)) .map(this::toAttributes).toList()); - addIfNotEmpty(attributes, "queriedMethods", hints.stream() - .filter(h -> h.getMode().equals(ExecutableMode.INTROSPECT)) - .map(this::toAttributes).toList()); } private Map toAttributes(ExecutableHint hint) { @@ -102,28 +116,19 @@ class ReflectionHintsWriter { return attributes; } + @SuppressWarnings("removal") private void handleCategories(Map attributes, Set categories) { categories.stream().sorted().forEach(category -> { switch (category) { - case PUBLIC_FIELDS -> attributes.put("allPublicFields", true); - case DECLARED_FIELDS -> attributes.put("allDeclaredFields", true); - case INTROSPECT_PUBLIC_CONSTRUCTORS -> - attributes.put("queryAllPublicConstructors", true); - case INTROSPECT_DECLARED_CONSTRUCTORS -> - attributes.put("queryAllDeclaredConstructors", true); + case INVOKE_PUBLIC_FIELDS, PUBLIC_FIELDS -> attributes.put("allPublicFields", true); + case INVOKE_DECLARED_FIELDS, DECLARED_FIELDS -> attributes.put("allDeclaredFields", true); case INVOKE_PUBLIC_CONSTRUCTORS -> attributes.put("allPublicConstructors", true); case INVOKE_DECLARED_CONSTRUCTORS -> attributes.put("allDeclaredConstructors", true); - case INTROSPECT_PUBLIC_METHODS -> - attributes.put("queryAllPublicMethods", true); - case INTROSPECT_DECLARED_METHODS -> - attributes.put("queryAllDeclaredMethods", true); case INVOKE_PUBLIC_METHODS -> attributes.put("allPublicMethods", true); case INVOKE_DECLARED_METHODS -> attributes.put("allDeclaredMethods", true); - case PUBLIC_CLASSES -> attributes.put("allPublicClasses", true); - case DECLARED_CLASSES -> attributes.put("allDeclaredClasses", true); } } ); @@ -135,4 +140,11 @@ class ReflectionHintsWriter { } } + private Map toAttributes(JdkProxyHint hint) { + Map attributes = new LinkedHashMap<>(); + handleCondition(attributes, hint); + attributes.put("type", Map.of("proxy", hint.getProxiedInterfaces())); + return attributes; + } + } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsAttributes.java similarity index 68% rename from spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsAttributes.java index 6829006e902..519a986fd93 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsAttributes.java @@ -21,7 +21,6 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Stream; import org.springframework.aot.hint.ConditionalHint; import org.springframework.aot.hint.ResourceBundleHint; @@ -31,8 +30,8 @@ import org.springframework.aot.hint.ResourcePatternHints; import org.springframework.lang.Nullable; /** - * Write a {@link ResourceHints} to the JSON output expected by the GraalVM - * {@code native-image} compiler, typically named {@code resource-config.json}. + * Collect {@link ResourceHints} as map attributes ready for JSON serialization for the GraalVM + * {@code native-image} compiler. * * @author Sebastien Deleuze * @author Stephane Nicoll @@ -41,9 +40,7 @@ import org.springframework.lang.Nullable; * @see Accessing Resources in Native Images * @see Native Image Build Configuration */ -class ResourceHintsWriter { - - public static final ResourceHintsWriter INSTANCE = new ResourceHintsWriter(); +class ResourceHintsAttributes { private static final Comparator RESOURCE_PATTERN_HINT_COMPARATOR = Comparator.comparing(ResourcePatternHint::getPattern); @@ -52,30 +49,17 @@ class ResourceHintsWriter { Comparator.comparing(ResourceBundleHint::getBaseName); - public void write(BasicJsonWriter writer, ResourceHints hints) { - Map attributes = new LinkedHashMap<>(); - addIfNotEmpty(attributes, "resources", toAttributes(hints)); - handleResourceBundles(attributes, hints.resourceBundleHints()); - writer.writeObject(attributes); - } - - private Map toAttributes(ResourceHints hint) { - Map attributes = new LinkedHashMap<>(); - addIfNotEmpty(attributes, "includes", hint.resourcePatternHints() + public List> resources(ResourceHints hint) { + return hint.resourcePatternHints() .map(ResourcePatternHints::getIncludes).flatMap(List::stream).distinct() .sorted(RESOURCE_PATTERN_HINT_COMPARATOR) - .map(this::toAttributes).toList()); - addIfNotEmpty(attributes, "excludes", hint.resourcePatternHints() - .map(ResourcePatternHints::getExcludes).flatMap(List::stream).distinct() - .sorted(RESOURCE_PATTERN_HINT_COMPARATOR) - .map(this::toAttributes).toList()); - return attributes; + .map(this::toAttributes).toList(); } - private void handleResourceBundles(Map attributes, Stream resourceBundles) { - addIfNotEmpty(attributes, "bundles", resourceBundles + public List> resourceBundles(ResourceHints hint) { + return hint.resourceBundleHints() .sorted(RESOURCE_BUNDLE_HINT_COMPARATOR) - .map(this::toAttributes).toList()); + .map(this::toAttributes).toList(); } private Map toAttributes(ResourceBundleHint hint) { @@ -88,7 +72,7 @@ class ResourceHintsWriter { private Map toAttributes(ResourcePatternHint hint) { Map attributes = new LinkedHashMap<>(); handleCondition(attributes, hint); - attributes.put("pattern", hint.toRegex().toString()); + attributes.put("glob", hint.getPattern()); return attributes; } @@ -111,7 +95,7 @@ class ResourceHintsWriter { private void handleCondition(Map attributes, ConditionalHint hint) { if (hint.getReachableType() != null) { Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); + conditionAttributes.put("typeReached", hint.getReachableType()); attributes.put("condition", conditionAttributes); } } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java new file mode 100644 index 00000000000..782497dee8b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2024 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.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.core.SpringVersion; + +/** + * Write a {@link RuntimeHints} instance to the JSON output expected by the + * GraalVM {@code native-image} compiler, typically named {@code reachability-metadata.json}. + * + * @author Brian Clozel + * @since 7.0 + * @see GraalVM Reachability Metadata + */ +class RuntimeHintsWriter { + + public void write(BasicJsonWriter writer, RuntimeHints hints) { + Map document = new LinkedHashMap<>(); + String springVersion = SpringVersion.getVersion(); + if (springVersion != null) { + document.put("comment", "Spring Framework " + springVersion); + } + List> reflection = new ReflectionHintsAttributes().reflection(hints); + if (!reflection.isEmpty()) { + document.put("reflection", reflection); + } + List> jni = new ReflectionHintsAttributes().jni(hints); + if (!jni.isEmpty()) { + document.put("jni", jni); + } + List> resourceHints = new ResourceHintsAttributes().resources(hints.resources()); + if (!resourceHints.isEmpty()) { + document.put("resources", resourceHints); + } + List> resourceBundles = new ResourceHintsAttributes().resourceBundles(hints.resources()); + if (!resourceBundles.isEmpty()) { + document.put("bundles", resourceBundles); + } + List> serialization = new SerializationHintsAttributes().toAttributes(hints.serialization()); + if (!serialization.isEmpty()) { + document.put("serialization", serialization); + } + + writer.writeObject(document); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsAttributes.java similarity index 70% rename from spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java rename to spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsAttributes.java index 73b25248519..d9ca1d6d5fd 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsAttributes.java @@ -18,6 +18,7 @@ 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; @@ -25,40 +26,36 @@ import org.springframework.aot.hint.JavaSerializationHint; import org.springframework.aot.hint.SerializationHints; /** - * Write a {@link SerializationHints} to the JSON output expected by the - * GraalVM {@code native-image} compiler, typically named - * {@code serialization-config.json}. + * 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 - * @since 6.0 - * @see Native Image Build Configuration + * @see Native Image Build Configuration */ -class SerializationHintsWriter { - - public static final SerializationHintsWriter INSTANCE = new SerializationHintsWriter(); +class SerializationHintsAttributes { private static final Comparator JAVA_SERIALIZATION_HINT_COMPARATOR = Comparator.comparing(JavaSerializationHint::getType); - public void write(BasicJsonWriter writer, SerializationHints hints) { - writer.writeArray(hints.javaSerializationHints() + public List> toAttributes(SerializationHints hints) { + return hints.javaSerializationHints() .sorted(JAVA_SERIALIZATION_HINT_COMPARATOR) - .map(this::toAttributes).toList()); + .map(this::toAttributes).toList(); } private Map toAttributes(JavaSerializationHint serializationHint) { LinkedHashMap attributes = new LinkedHashMap<>(); handleCondition(attributes, serializationHint); - attributes.put("name", serializationHint.getType()); + attributes.put("type", serializationHint.getType()); return attributes; } private void handleCondition(Map attributes, ConditionalHint hint) { if (hint.getReachableType() != null) { Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); + conditionAttributes.put("typeReached", hint.getReachableType()); attributes.put("condition", conditionAttributes); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java index db6d82a421a..3874d9f9675 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -20,8 +20,6 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -40,7 +38,6 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.SerializationHints; import org.springframework.aot.hint.TypeReference; import org.springframework.core.codec.StringDecoder; -import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; @@ -74,10 +71,13 @@ class FileNativeConfigurationWriterTests { serializationHints.registerType(Long.class); generator.write(hints); assertEquals(""" - [ - { "name": "java.lang.Integer" }, - { "name": "java.lang.Long" } - ]""", "serialization-config.json"); + { + "serialization": [ + { "type": "java.lang.Integer" }, + { "type": "java.lang.Long" } + ] + } + """); } @Test @@ -89,10 +89,13 @@ class FileNativeConfigurationWriterTests { proxyHints.registerJdkProxy(Function.class, Consumer.class); generator.write(hints); assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] }, - { "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] } - ]""", "proxy-config.json"); + { + "reflection": [ + { type: {"proxy": [ "java.util.function.Function" ] } }, + { type: {"proxy": [ "java.util.function.Function", "java.util.function.Consumer" ] } } + ] + } + """); } @Test @@ -102,48 +105,36 @@ class FileNativeConfigurationWriterTests { ReflectionHints reflectionHints = hints.reflection(); reflectionHints.registerType(StringDecoder.class, builder -> builder .onReachableType(String.class) - .withMembers(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS, - MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, + .withMembers(MemberCategory.INVOKE_PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) .withField("DEFAULT_CHARSET") .withField("defaultCharset") - .withConstructor(TypeReference.listOf(List.class, boolean.class, MimeType.class), ExecutableMode.INTROSPECT) - .withMethod("setDefaultCharset", TypeReference.listOf(Charset.class), ExecutableMode.INVOKE) - .withMethod("getDefaultCharset", Collections.emptyList(), ExecutableMode.INTROSPECT)); + .withMethod("setDefaultCharset", TypeReference.listOf(Charset.class), ExecutableMode.INVOKE)); generator.write(hints); assertEquals(""" - [ - { - "name": "org.springframework.core.codec.StringDecoder", - "condition": { "typeReachable": "java.lang.String" }, - "allPublicFields": true, - "allDeclaredFields": true, - "queryAllPublicConstructors": true, - "queryAllDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredConstructors": true, - "queryAllPublicMethods": true, - "queryAllDeclaredMethods": true, - "allPublicMethods": true, - "allDeclaredMethods": true, - "allPublicClasses": true, - "allDeclaredClasses": true, - "fields": [ - { "name": "DEFAULT_CHARSET" }, - { "name": "defaultCharset" } - ], - "methods": [ - { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } - ], - "queriedMethods": [ - { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name": "getDefaultCharset", "parameterTypes": [ ] } - ] - } - ]""", "reflect-config.json"); + { + "reflection": [ + { + "type": "org.springframework.core.codec.StringDecoder", + "condition": { "typeReached": "java.lang.String" }, + "allPublicFields": true, + "allDeclaredFields": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "fields": [ + { "name": "DEFAULT_CHARSET" }, + { "name": "defaultCharset" } + ], + "methods": [ + { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } + ] + } + ] + } + """); } @Test @@ -155,12 +146,14 @@ class FileNativeConfigurationWriterTests { jniHints.registerType(StringDecoder.class, builder -> builder.onReachableType(String.class)); generator.write(hints); assertEquals(""" - [ - { - "name": "org.springframework.core.codec.StringDecoder", - "condition": { "typeReachable": "java.lang.String" } - } - ]""", "jni-config.json"); + { + "jni": [ + { + "type": "org.springframework.core.codec.StringDecoder", + "condition": { "typeReached": "java.lang.String" } + } + ] + }"""); } @Test @@ -173,23 +166,21 @@ class FileNativeConfigurationWriterTests { generator.write(hints); assertEquals(""" { - "resources": { - "includes": [ - {"pattern": "\\\\Qcom/example/test.properties\\\\E"}, - {"pattern": "\\\\Q/\\\\E"}, - {"pattern": "\\\\Qcom\\\\E"}, - {"pattern": "\\\\Qcom/example\\\\E"}, - {"pattern": "\\\\Qcom/example/another.properties\\\\E"} - ] - } - }""", "resource-config.json"); + "resources": [ + {"glob": "com/example/test.properties"}, + {"glob": "/"}, + {"glob": "com"}, + {"glob": "com/example"}, + {"glob": "com/example/another.properties"} + ] + }"""); } @Test void namespace() { String groupId = "foo.bar"; String artifactId = "baz"; - String filename = "resource-config.json"; + String filename = "reachability-metadata.json"; FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir, groupId, artifactId); RuntimeHints hints = new RuntimeHints(); ResourceHints resourceHints = hints.resources(); @@ -199,8 +190,8 @@ class FileNativeConfigurationWriterTests { assertThat(jsonFile.toFile()).exists(); } - private void assertEquals(String expectedString, String filename) throws IOException, JSONException { - Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(filename); + private void assertEquals(String expectedString) throws IOException, JSONException { + Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve("reachability-metadata.json"); String content = Files.readString(jsonFile); JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE); } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java deleted file mode 100644 index 6a65db7e9d1..00000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2002-2023 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.io.StringWriter; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.ProxyHints; -import org.springframework.aot.hint.TypeReference; - -/** - * Tests for {@link ProxyHintsWriter}. - * - * @author Sebastien Deleuze - * @author Stephane Nicoll - */ -class ProxyHintsWriterTests { - - @Test - void empty() throws JSONException { - ProxyHints hints = new ProxyHints(); - assertEquals("[]", hints); - } - - @Test - void shouldWriteOneEntry() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Function.class); - assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] } - ]""", hints); - } - - @Test - void shouldWriteMultipleEntries() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Function.class); - hints.registerJdkProxy(Function.class, Consumer.class); - assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] }, - { "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] } - ]""", hints); - } - - @Test - void shouldWriteEntriesInNaturalOrder() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Supplier.class); - hints.registerJdkProxy(Function.class); - assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] }, - { "interfaces": [ "java.util.function.Supplier" ] } - ]""", hints); - } - - @Test - void shouldWriteInnerClass() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Inner.class); - assertEquals(""" - [ - { "interfaces": [ "org.springframework.aot.nativex.ProxyHintsWriterTests$Inner" ] } - ]""", hints); - } - - @Test - void shouldWriteCondition() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(builder -> builder.proxiedInterfaces(Function.class) - .onReachableType(TypeReference.of("org.example.Test"))); - assertEquals(""" - [ - { "condition": { "typeReachable": "org.example.Test"}, "interfaces": [ "java.util.function.Function" ] } - ]""", hints); - } - - private void assertEquals(String expectedString, ProxyHints hints) throws JSONException { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - ProxyHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); - } - - interface Inner { - - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java deleted file mode 100644 index c9fb6901d79..00000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright 2002-2023 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.io.StringWriter; -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.List; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.ExecutableMode; -import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.ReflectionHints; -import org.springframework.aot.hint.TypeReference; -import org.springframework.core.codec.StringDecoder; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ReflectionHintsWriter}. - * - * @author Sebastien Deleuze - * @author Stephane Nicoll - */ -class ReflectionHintsWriterTests { - - @Test - void empty() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - assertEquals("[]", hints); - } - - @Test - void one() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(StringDecoder.class, builder -> builder - .onReachableType(String.class) - .withMembers(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS, - MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) - .withField("DEFAULT_CHARSET") - .withField("defaultCharset") - .withField("aScore") - .withConstructor(TypeReference.listOf(List.class, boolean.class, MimeType.class), ExecutableMode.INTROSPECT) - .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE) - .withMethod("getDefaultCharset", Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertEquals(""" - [ - { - "name": "org.springframework.core.codec.StringDecoder", - "condition": { "typeReachable": "java.lang.String" }, - "allPublicFields": true, - "allDeclaredFields": true, - "queryAllPublicConstructors": true, - "queryAllDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredConstructors": true, - "queryAllPublicMethods": true, - "queryAllDeclaredMethods": true, - "allPublicMethods": true, - "allDeclaredMethods": true, - "allPublicClasses": true, - "allDeclaredClasses": true, - "fields": [ - { "name": "aScore" }, - { "name": "DEFAULT_CHARSET" }, - { "name": "defaultCharset" } - ], - "methods": [ - { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } - ], - "queriedMethods": [ - { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name": "getDefaultCharset", "parameterTypes": [ ] } - ] - } - ]""", hints); - } - - @Test - void two() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - }); - hints.registerType(Long.class, builder -> { - }); - - assertEquals(""" - [ - { "name": "java.lang.Integer" }, - { "name": "java.lang.Long" } - ]""", hints); - } - - @Test - void queriedMethods() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), ExecutableMode.INTROSPECT)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "queriedMethods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String"] - } - ] - } - ] - """, hints); - } - - @Test - void methods() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), ExecutableMode.INVOKE)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "methods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String"] - } - ] - } - ] - """, hints); - } - - @Test - void methodWithInnerClassParameter() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("test", - TypeReference.listOf(Inner.class), ExecutableMode.INVOKE)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "methods": [ - { - "name": "test", - "parameterTypes": ["org.springframework.aot.nativex.ReflectionHintsWriterTests$Inner"] - } - ] - } - ] - """, hints); - } - - @Test - void methodAndQueriedMethods() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), ExecutableMode.INVOKE)); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class, int.class), ExecutableMode.INTROSPECT)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "queriedMethods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String", "int"] - } - ], - "methods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String"] - } - ] - } - ] - """, hints); - } - - @Test - void ignoreLambda() throws JSONException { - Runnable anonymousRunnable = () -> {}; - ReflectionHints hints = new ReflectionHints(); - hints.registerType(anonymousRunnable.getClass()); - assertEquals("[]", hints); - } - - @Test - void sortTypeHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> {}); - hints.registerType(Long.class, builder -> {}); - - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Long.class, builder -> {}); - hints2.registerType(Integer.class, builder -> {}); - - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - @Test - void sortFieldHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - builder.withField("first"); - builder.withField("second"); - }); - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Integer.class, builder -> { - builder.withField("second"); - builder.withField("first"); - }); - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - @Test - void sortConstructorHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); - builder.withConstructor(List.of(TypeReference.of(String.class), - TypeReference.of(Integer.class)), ExecutableMode.INVOKE); - }); - - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Integer.class, builder -> { - builder.withConstructor(List.of(TypeReference.of(String.class), - TypeReference.of(Integer.class)), ExecutableMode.INVOKE); - builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); - }); - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - @Test - void sortMethodHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); - builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); - }); - - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Integer.class, builder -> { - builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); - builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); - }); - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException { - JSONAssert.assertEquals(expectedString, writeJson(hints), JSONCompareMode.STRICT); - } - - private String writeJson(ReflectionHints hints) { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - ReflectionHintsWriter.INSTANCE.write(writer, hints); - return out.toString(); - } - - - static class Inner { - - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java deleted file mode 100644 index b3fef587efa..00000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2002-2023 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.io.StringWriter; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.ResourceHints; -import org.springframework.aot.hint.TypeReference; - -/** - * Tests for {@link ResourceHintsWriter}. - * - * @author Sebastien Deleuze - * @author Brian Clozel - */ -class ResourceHintsWriterTests { - - @Test - void empty() throws JSONException { - ResourceHints hints = new ResourceHints(); - assertEquals("{}", hints); - } - - @Test - void registerExactMatch() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("com/example/test.properties"); - hints.registerPattern("com/example/another.properties"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/another.properties\\\\E"}, - { "pattern": "\\\\Qcom/example/test.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWildcardAtTheBeginningPattern() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("*.properties"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": ".*\\\\Q.properties\\\\E"}, - { "pattern": "\\\\Q\\/\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWildcardInTheMiddlePattern() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("com/example/*.properties"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWildcardAtTheEndPattern() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("static/*"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qstatic\\\\E"}, - { "pattern": "\\\\Qstatic/\\\\E.*"} - ] - } - }""", hints); - } - - @Test - void registerPatternWithIncludesAndExcludes() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern(hint -> hint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties")); - hints.registerPattern(hint -> hint.includes("org/other/*.properties").excludes("org/other/to-ignore.properties")); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E"}, - { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, - { "pattern": "\\\\Qorg\\\\E"}, - { "pattern": "\\\\Qorg/other\\\\E"}, - { "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"} - ], - "excludes": [ - { "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"}, - { "pattern": "\\\\Qorg/other/to-ignore.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWithReachableTypeCondition() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern(builder -> builder.includes(TypeReference.of("com.example.Test"), "com/example/test.properties")); - assertEquals(""" - { - "resources": { - "includes": [ - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Q/\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerType() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerType(String.class); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qjava\\\\E" }, - { "pattern": "\\\\Qjava/lang\\\\E" }, - { "pattern": "\\\\Qjava/lang/String.class\\\\E" } - ] - } - }""", hints); - } - - @Test - void registerResourceBundle() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerResourceBundle("com.example.message2"); - hints.registerResourceBundle("com.example.message"); - assertEquals(""" - { - "bundles": [ - { "name": "com.example.message"}, - { "name": "com.example.message2"} - ] - }""", hints); - } - - private void assertEquals(String expectedString, ResourceHints hints) throws JSONException { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - ResourceHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java new file mode 100644 index 00000000000..e0f832d35ae --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java @@ -0,0 +1,594 @@ +/* + * Copyright 2002-2024 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.io.StringWriter; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.networknt.schema.InputFormat; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RuntimeHintsWriter}. + * + * @author Brian Clozel + * @author Sebastien Deleuze + * @author Stephane Nicoll + */ +class RuntimeHintsWriterTests { + + private static JsonSchema JSON_SCHEMA; + + @BeforeAll + static void setupSchemaValidator() { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909, builder -> + 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); + } + + @Nested + class ReflectionHintsTests { + + @Test + void empty() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void one() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(StringDecoder.class, builder -> builder + .onReachableType(String.class) + .withMembers(MemberCategory.INVOKE_PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_FIELDS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) + .withField("DEFAULT_CHARSET") + .withField("defaultCharset") + .withField("aScore") + .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE)); + assertEquals(""" + { + "reflection": [ + { + "type": "org.springframework.core.codec.StringDecoder", + "condition": { "typeReached": "java.lang.String" }, + "allPublicFields": true, + "allDeclaredFields": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "fields": [ + { "name": "aScore" }, + { "name": "DEFAULT_CHARSET" }, + { "name": "defaultCharset" } + ], + "methods": [ + { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } + ] + } + ] + } + """, hints); + } + + @Test + void two() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + }); + hints.reflection().registerType(Long.class, builder -> { + }); + + assertEquals(""" + { + "reflection": [ + { "type": "java.lang.Integer" }, + { "type": "java.lang.Long" } + ] + } + """, hints); + } + + @Test + void methods() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> builder.withMethod("parseInt", + TypeReference.listOf(String.class), ExecutableMode.INVOKE)); + + assertEquals(""" + { + "reflection": [ + { + "type": "java.lang.Integer", + "methods": [ + { + "name": "parseInt", + "parameterTypes": ["java.lang.String"] + } + ] + } + ] + } + """, hints); + } + + @Test + void methodWithInnerClassParameter() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> builder.withMethod("test", + TypeReference.listOf(InnerClass.class), ExecutableMode.INVOKE)); + + assertEquals(""" + { + "reflection": [ + { + "type": "java.lang.Integer", + "methods": [ + { + "name": "test", + "parameterTypes": ["org.springframework.aot.nativex.RuntimeHintsWriterTests$InnerClass"] + } + ] + } + ] + } + """, hints); + } + + @Test + void methodAndQueriedMethods() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> builder.withMethod("parseInt", + TypeReference.listOf(String.class), ExecutableMode.INVOKE)); + + assertEquals(""" + { + "reflection": [ + { + "type": "java.lang.Integer", + "methods": [ + { + "name": "parseInt", + "parameterTypes": ["java.lang.String"] + } + ] + } + ] + } + """, hints); + } + + @Test + void ignoreLambda() throws JSONException { + Runnable anonymousRunnable = () -> {}; + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(anonymousRunnable.getClass()); + assertEquals("{}", hints); + } + + @Test + void sortTypeHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> {}); + hints.reflection().registerType(Long.class, builder -> {}); + + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Long.class, builder -> {}); + hints2.reflection().registerType(Integer.class, builder -> {}); + + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortFieldHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + builder.withField("first"); + builder.withField("second"); + }); + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Integer.class, builder -> { + builder.withField("second"); + builder.withField("first"); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortConstructorHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); + builder.withConstructor(List.of(TypeReference.of(String.class), + TypeReference.of(Integer.class)), ExecutableMode.INVOKE); + }); + + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Integer.class, builder -> { + builder.withConstructor(List.of(TypeReference.of(String.class), + TypeReference.of(Integer.class)), ExecutableMode.INVOKE); + builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortMethodHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); + builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); + }); + + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Integer.class, builder -> { + builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); + builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + } + + + @Nested + class JniHints { + + // TODO + + } + + + @Nested + class ResourceHintsTests { + + @Test + void empty() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void registerExactMatch() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("com/example/test.properties"); + hints.resources().registerPattern("com/example/another.properties"); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "com"}, + { "glob": "com/example"}, + { "glob": "com/example/another.properties"}, + { "glob": "com/example/test.properties"} + ] + }""", hints); + } + + @Test + void registerWildcardAtTheBeginningPattern() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("*.properties"); + assertEquals(""" + { + "resources": [ + { "glob": "*.properties"}, + { "glob": "/"} + ] + }""", hints); + } + + @Test + void registerWildcardInTheMiddlePattern() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("com/example/*.properties"); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "com"}, + { "glob": "com/example"}, + { "glob": "com/example/*.properties"} + ] + }""", hints); + } + + @Test + void registerWildcardAtTheEndPattern() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("static/*"); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "static"}, + { "glob": "static/*"} + ] + }""", hints); + } + + @Test + void registerPatternWithIncludesAndExcludes() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern(hint -> hint.includes("com/example/*.properties")); + hints.resources().registerPattern(hint -> hint.includes("org/other/*.properties")); + assertEquals(""" + { + "resources": [ + { "glob": "/"}, + { "glob": "com"}, + { "glob": "com/example"}, + { "glob": "com/example/*.properties"}, + { "glob": "org"}, + { "glob": "org/other"}, + { "glob": "org/other/*.properties"} + ] + }""", hints); + } + + @Test + void registerWithReachableTypeCondition() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern(builder -> builder.includes(TypeReference.of("com.example.Test"), "com/example/test.properties")); + assertEquals(""" + { + "resources": [ + { "condition": { "typeReached": "com.example.Test"}, "glob": "/"}, + { "condition": { "typeReached": "com.example.Test"}, "glob": "com"}, + { "condition": { "typeReached": "com.example.Test"}, "glob": "com/example"}, + { "condition": { "typeReached": "com.example.Test"}, "glob": "com/example/test.properties"} + ] + }""", hints); + } + + @Test + void registerType() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerType(String.class); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "java" }, + { "glob": "java/lang" }, + { "glob": "java/lang/String.class" } + ] + }""", hints); + } + + @Test + void registerResourceBundle() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerResourceBundle("com.example.message2"); + hints.resources().registerResourceBundle("com.example.message"); + assertEquals(""" + { + "bundles": [ + { "name": "com.example.message"}, + { "name": "com.example.message2"} + ] + }""", hints); + } + } + + @Nested + class SerializationHintsTests { + + @Test + void shouldWriteEmptyHint() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void shouldWriteSingleHint() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.serialization().registerType(TypeReference.of(String.class)); + assertEquals(""" + { + "serialization": [ + { "type": "java.lang.String" } + ] + } + """, hints); + } + + @Test + void shouldWriteMultipleHints() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.serialization() + .registerType(TypeReference.of(Environment.class)) + .registerType(TypeReference.of(String.class)); + assertEquals(""" + { + "serialization": [ + { "type": "java.lang.String" }, + { "type": "org.springframework.core.env.Environment" } + ] + } + """, hints); + } + + @Test + void shouldWriteSingleHintWithCondition() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.serialization().registerType(TypeReference.of(String.class), + builder -> builder.onReachableType(TypeReference.of("org.example.Test"))); + assertEquals(""" + { + "serialization": [ + { "condition": { "typeReached": "org.example.Test" }, "type": "java.lang.String" } + ] + } + """, hints); + } + + } + + @Nested + class ProxyHintsTests { + + @Test + void empty() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void shouldWriteOneEntry() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(Function.class); + assertEquals(""" + { + "reflection": [ + { + "type": { + "proxy": ["java.util.function.Function"] + } + } + ] + } + """, hints); + } + + @Test + void shouldWriteMultipleEntries() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(Function.class) + .registerJdkProxy(Function.class, Consumer.class); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["java.util.function.Function"] } + }, + { + "type": { "proxy": ["java.util.function.Function", "java.util.function.Consumer"] } + } + ] + } + """, hints); + } + + @Test + void shouldWriteEntriesInNaturalOrder() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(Supplier.class) + .registerJdkProxy(Function.class); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["java.util.function.Function"] } + }, + { + "type": { "proxy": ["java.util.function.Supplier"] } + } + ] + } + """, hints); + } + + @Test + void shouldWriteInnerClass() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(InnerInterface.class); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["org.springframework.aot.nativex.RuntimeHintsWriterTests$InnerInterface"] } + } + ] + } + """, hints); + } + + @Test + void shouldWriteCondition() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(builder -> builder.proxiedInterfaces(Function.class) + .onReachableType(TypeReference.of("org.example.Test"))); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["java.util.function.Function"] }, + "condition": { "typeReached": "org.example.Test" } + } + ] + } + """, hints); + } + + } + + private void assertEquals(String expectedString, RuntimeHints hints) throws JSONException { + String json = writeJson(hints); + JSONAssert.assertEquals(expectedString, json, JSONCompareMode.LENIENT); + Set validationMessages = JSON_SCHEMA.validate(json, InputFormat.JSON, executionContext -> + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertThat(validationMessages).isEmpty(); + } + + private String writeJson(RuntimeHints hints) { + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + new RuntimeHintsWriter().write(writer, hints); + return out.toString(); + } + + + static class InnerClass { + + } + + interface InnerInterface { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java deleted file mode 100644 index bef49222489..00000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2023 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.io.StringWriter; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.SerializationHints; -import org.springframework.aot.hint.TypeReference; -import org.springframework.core.env.Environment; - -/** - * Tests for {@link SerializationHintsWriter}. - * - * @author Sebastien Deleuze - */ -class SerializationHintsWriterTests { - - @Test - void shouldWriteEmptyHint() throws JSONException { - SerializationHints hints = new SerializationHints(); - assertEquals("[]", hints); - } - - @Test - void shouldWriteSingleHint() throws JSONException { - SerializationHints hints = new SerializationHints().registerType(TypeReference.of(String.class)); - assertEquals(""" - [ - { "name": "java.lang.String" } - ]""", hints); - } - - @Test - void shouldWriteMultipleHints() throws JSONException { - SerializationHints hints = new SerializationHints() - .registerType(TypeReference.of(Environment.class)) - .registerType(TypeReference.of(String.class)); - assertEquals(""" - [ - { "name": "java.lang.String" }, - { "name": "org.springframework.core.env.Environment" } - ]""", hints); - } - - @Test - void shouldWriteSingleHintWithCondition() throws JSONException { - SerializationHints hints = new SerializationHints().registerType(TypeReference.of(String.class), - builder -> builder.onReachableType(TypeReference.of("org.example.Test"))); - assertEquals(""" - [ - { "condition": { "typeReachable": "org.example.Test" }, "name": "java.lang.String" } - ]""", hints); - } - - private void assertEquals(String expectedString, SerializationHints hints) throws JSONException { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - SerializationHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); - } - -} diff --git a/spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json b/spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json new file mode 100644 index 00000000000..bb434fb22e2 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json @@ -0,0 +1,362 @@ +{ + "$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": "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": "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": "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": "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": "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": "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 + } + } +} \ No newline at end of file