Implement GraalVM native JSON configuration generation

This commit implements 4 package private Json serializers
for JavaSerializationHints, ProxyHints, ReflectionHints
and ResourceHints to serialize GraalVM native JSON configuration
as documented in
https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/.

It exposes the related functionality via
NativeConfigurationGenerator which allows to generate the
relevant files on the filesystem via the
FileNativeConfigurationGenerator implementation.

The generated *-config.json files have been validated working
with GraalVM 22.0.

Closes gh-27991
This commit is contained in:
Sébastien Deleuze 2022-03-02 10:53:17 +01:00
parent bb9cf7cce1
commit 77e0100f42
15 changed files with 1280 additions and 0 deletions

View File

@ -67,6 +67,7 @@ dependencies {
testImplementation("org.xmlunit:xmlunit-matchers")
testImplementation("io.projectreactor:reactor-test")
testImplementation("io.projectreactor.tools:blockhound")
testImplementation("org.skyscreamer:jsonassert")
testFixturesImplementation("com.google.code.findbugs:jsr305")
testFixturesImplementation("org.junit.platform:junit-platform-launcher")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")

View File

@ -0,0 +1,136 @@
/*
* Copyright 2002-2022 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.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import org.springframework.aot.hint.JavaSerializationHints;
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.lang.Nullable;
/**
* Generate the GraalVM native configuration files from runtime hints.
*
* @author Sebastien Deleuze
* @since 6.0
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
*/
public class FileNativeConfigurationGenerator implements NativeConfigurationGenerator {
private final Path basePath;
private final String groupId;
private final String artifactId;
public FileNativeConfigurationGenerator(Path basePath) {
this(basePath, null, null);
}
public FileNativeConfigurationGenerator(Path basePath, @Nullable String groupId, @Nullable String artifactId) {
this.basePath = basePath;
if ((groupId == null && artifactId != null) || (groupId != null && artifactId == null)) {
throw new IllegalArgumentException("groupId and artifactId must be both null or both non-null");
}
this.groupId = groupId;
this.artifactId = artifactId;
}
@Override
public void generate(RuntimeHints hints) {
try {
if (hints.javaSerialization().types().findAny().isPresent()) {
generateFile(hints.javaSerialization());
}
if (hints.proxies().jdkProxies().findAny().isPresent()) {
generateFile(hints.proxies());
}
if (hints.reflection().typeHints().findAny().isPresent()) {
generateFile(hints.reflection());
}
if (hints.resources().resourcePatterns().findAny().isPresent() ||
hints.resources().resourceBundles().findAny().isPresent()) {
generateFile(hints.resources());
}
}
catch (IOException ex) {
throw new IllegalStateException("Unexpected I/O error while writing the native configuration", ex);
}
}
/**
* Generate the Java serialization native configuration file.
*/
private void generateFile(JavaSerializationHints hints) throws IOException {
JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
File file = createIfNecessary("serialization-config.json");
FileWriter writer = new FileWriter(file);
writer.write(serializer.serialize(hints));
writer.close();
}
/**
* Generate the proxy native configuration file.
*/
private void generateFile(ProxyHints hints) throws IOException {
ProxyHintsSerializer serializer = new ProxyHintsSerializer();
File file = createIfNecessary("proxy-config.json");
FileWriter writer = new FileWriter(file);
writer.write(serializer.serialize(hints));
writer.close();
}
/**
* Generate the reflection native configuration file.
*/
private void generateFile(ReflectionHints hints) throws IOException {
ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
File file = createIfNecessary("reflect-config.json");
FileWriter writer = new FileWriter(file);
writer.write(serializer.serialize(hints));
writer.close();
}
/**
* Generate the resource native configuration file.
*/
private void generateFile(ResourceHints hints) throws IOException {
ResourceHintsSerializer serializer = new ResourceHintsSerializer();
File file = createIfNecessary("resource-config.json");
FileWriter writer = new FileWriter(file);
writer.write(serializer.serialize(hints));
writer.close();
}
private File createIfNecessary(String filename) throws IOException {
Path outputDirectory = this.basePath.resolve("META-INF").resolve("native-image");
if (this.groupId != null && this.artifactId != null) {
outputDirectory = outputDirectory.resolve(this.groupId).resolve(this.artifactId);
}
outputDirectory.toFile().mkdirs();
File file = outputDirectory.resolve(filename).toFile();
file.createNewFile();
return file;
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2002-2022 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.Iterator;
import org.springframework.aot.hint.JavaSerializationHints;
import org.springframework.aot.hint.TypeReference;
/**
* Serialize a {@link JavaSerializationHints} to the JSON file expected by GraalVM {@code native-image} compiler,
* typically named {@code serialization-config.json}.
*
* @author Sebastien Deleuze
* @since 6.0
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
*/
class JavaSerializationHintsSerializer {
public String serialize(JavaSerializationHints hints) {
StringBuilder builder = new StringBuilder();
builder.append("[\n");
Iterator<TypeReference> typeIterator = hints.types().iterator();
while (typeIterator.hasNext()) {
TypeReference type = typeIterator.next();
String name = JsonUtils.escape(type.getCanonicalName());
builder.append("{ \"name\": \"").append(name).append("\" }");
if (typeIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("\n]\n");
return builder.toString();
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2002-2022 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;
/**
* Utility class for JSON.
*
* @author Sebastien Deleuze
*/
abstract class JsonUtils {
/**
* Escape a JSON String.
*/
static String escape(String input) {
StringBuilder builder = new StringBuilder();
input.chars().forEach(c -> {
switch (c) {
case '"':
builder.append("\\\"");
break;
case '\\':
builder.append("\\\\");
break;
case '/':
builder.append("\\/");
break;
case '\b':
builder.append("\\b");
break;
case '\f':
builder.append("\\f");
break;
case '\n':
builder.append("\\n");
break;
case '\r':
builder.append("\\r");
break;
case '\t':
builder.append("\\t");
break;
default:
if (c <= 0x1F) {
builder.append(String.format("\\u%04x", c));
}
else {
builder.append((char) c);
}
break;
}
});
return builder.toString();
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2002-2022 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 org.springframework.aot.hint.RuntimeHints;
/**
* Generate GraalVM native configuration.
*
* @author Sebastien Deleuze
* @since 6.0
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
*/
public interface NativeConfigurationGenerator {
/**
* Generate the GraalVM native configuration from the provided hints.
* @param hints the hints to serialize
*/
void generate(RuntimeHints hints);
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2002-2022 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.Iterator;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.ProxyHints;
import org.springframework.aot.hint.TypeReference;
/**
* Serialize {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON file expected by GraalVM
* {@code native-image} compiler, typically named {@code proxy-config.json}.
*
* @author Sebastien Deleuze
* @since 6.0
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/DynamicProxy/">Dynamic Proxy in Native Image</a>
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
*/
class ProxyHintsSerializer {
public String serialize(ProxyHints hints) {
StringBuilder builder = new StringBuilder();
builder.append("[\n");
Iterator<JdkProxyHint> hintIterator = hints.jdkProxies().iterator();
while (hintIterator.hasNext()) {
builder.append("{ \"interfaces\": [ ");
JdkProxyHint hint = hintIterator.next();
Iterator<TypeReference> interfaceIterator = hint.getProxiedInterfaces().iterator();
while (interfaceIterator.hasNext()) {
String name = JsonUtils.escape(interfaceIterator.next().getCanonicalName());
builder.append("\"").append(name).append("\"");
if (interfaceIterator.hasNext()) {
builder.append(", ");
}
}
builder.append(" ] }");
if (hintIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("\n]\n");
return builder.toString();
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright 2002-2022 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.Iterator;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.aot.hint.ExecutableHint;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.FieldHint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.TypeHint;
import org.springframework.aot.hint.TypeReference;
/**
* Serialize {@link ReflectionHints} to the JSON file expected by GraalVM {@code native-image} compiler,
* typically named {@code reflect-config.json}.
*
* @author Sebastien Deleuze
* @since 6.0
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/Reflection/">Reflection Use in Native Images</a>
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
*/
@SuppressWarnings("serial")
class ReflectionHintsSerializer {
public String serialize(ReflectionHints hints) {
StringBuilder builder = new StringBuilder();
builder.append("[\n");
Iterator<TypeHint> hintIterator = hints.typeHints().iterator();
while (hintIterator.hasNext()) {
TypeHint hint = hintIterator.next();
String name = JsonUtils.escape(hint.getType().getCanonicalName());
builder.append("{\n\"name\": \"").append(name).append("\"");
serializeCondition(hint, builder);
serializeMembers(hint, builder);
serializeFields(hint, builder);
serializeExecutables(hint, builder);
builder.append(" }");
if (hintIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("\n]");
return builder.toString();
}
private void serializeCondition(TypeHint hint, StringBuilder builder) {
if (hint.getReachableType() != null) {
String name = JsonUtils.escape(hint.getReachableType().getCanonicalName());
builder.append(",\n\"condition\": { \"typeReachable\": \"").append(name).append("\" }");
}
}
private void serializeFields(TypeHint hint, StringBuilder builder) {
Iterator<FieldHint> fieldIterator = hint.fields().iterator();
if (fieldIterator.hasNext()) {
builder.append(",\n\"fields\": [\n");
while (fieldIterator.hasNext()) {
FieldHint fieldHint = fieldIterator.next();
String name = JsonUtils.escape(fieldHint.getName());
builder.append("{ \"name\": \"").append(name).append("\"");
if (fieldHint.isAllowWrite()) {
builder.append(", \"allowWrite\": ").append(fieldHint.isAllowWrite());
}
if (fieldHint.isAllowUnsafeAccess()) {
builder.append(", \"allowUnsafeAccess\": ").append(fieldHint.isAllowUnsafeAccess());
}
builder.append(" }");
if (fieldIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("\n]");
}
}
private void serializeExecutables(TypeHint hint, StringBuilder builder) {
List<ExecutableHint> executables = Stream.concat(hint.constructors(), hint.methods()).toList();
Iterator<ExecutableHint> methodIterator = executables.stream().filter(h -> h.getModes().contains(ExecutableMode.INVOKE) || h.getModes().isEmpty()).iterator();
Iterator<ExecutableHint> queriedMethodIterator = executables.stream().filter(h -> h.getModes().contains(ExecutableMode.INTROSPECT)).iterator();
if (methodIterator.hasNext()) {
builder.append(",\n");
serializeMethods("methods", methodIterator, builder);
if (queriedMethodIterator.hasNext()) {
builder.append(",\n");
}
}
if (queriedMethodIterator.hasNext()) {
serializeMethods("queriedMethods", queriedMethodIterator, builder);
}
}
private void serializeMethods(String fieldName, Iterator<ExecutableHint> methodIterator, StringBuilder builder) {
builder.append("\"").append(JsonUtils.escape(fieldName)).append("\": [\n");
while (methodIterator.hasNext()) {
ExecutableHint hint = methodIterator.next();
String name = JsonUtils.escape(hint.getName());
builder.append("{\n\"name\": \"").append(name).append("\", ").append("\"parameterTypes\": [ ");
Iterator<TypeReference> parameterIterator = hint.getParameterTypes().iterator();
while (parameterIterator.hasNext()) {
String parameterName = JsonUtils.escape(parameterIterator.next().getCanonicalName());
builder.append("\"").append(parameterName).append("\"");
if (parameterIterator.hasNext()) {
builder.append(", ");
}
}
builder.append(" ] }\n");
if (methodIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("]\n");
}
private void serializeMembers(TypeHint hint, StringBuilder builder) {
Iterator<MemberCategory> categoryIterator = hint.getMemberCategories().iterator();
if (categoryIterator.hasNext()) {
builder.append(",\n");
while (categoryIterator.hasNext()) {
switch (categoryIterator.next()) {
case PUBLIC_FIELDS -> builder.append("\"allPublicFields\": true");
case DECLARED_FIELDS -> builder.append("\"allDeclaredFields\": true");
case INTROSPECT_PUBLIC_CONSTRUCTORS -> builder.append("\"queryAllPublicConstructors\": true");
case INTROSPECT_DECLARED_CONSTRUCTORS -> builder.append("\"queryAllDeclaredConstructors\": true");
case INVOKE_PUBLIC_CONSTRUCTORS -> builder.append("\"allPublicConstructors\": true");
case INVOKE_DECLARED_CONSTRUCTORS -> builder.append("\"allDeclaredConstructors\": true");
case INTROSPECT_PUBLIC_METHODS -> builder.append("\"queryAllPublicMethods\": true");
case INTROSPECT_DECLARED_METHODS -> builder.append("\"queryAllDeclaredMethods\": true");
case INVOKE_PUBLIC_METHODS -> builder.append("\"allPublicMethods\": true");
case INVOKE_DECLARED_METHODS -> builder.append("\"allDeclaredMethods\": true");
case PUBLIC_CLASSES -> builder.append("\"allPublicClasses\": true");
case DECLARED_CLASSES -> builder.append("\"allDeclaredClasses\": true");
}
if (categoryIterator.hasNext()) {
builder.append(",\n");
}
}
}
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2002-2022 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.Arrays;
import java.util.Iterator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.aot.hint.ResourceBundleHint;
import org.springframework.aot.hint.ResourceHints;
import org.springframework.aot.hint.ResourcePatternHint;
/**
* Serialize a {@link ResourceHints} to the JSON file expected by GraalVM {@code native-image} compiler,
* typically named {@code resource-config.json}.
*
* @author Sebastien Deleuze
* @since 6.0
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/Resources/">Accessing Resources in Native Images</a>
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
*/
class ResourceHintsSerializer {
public String serialize(ResourceHints hints) {
StringBuilder builder = new StringBuilder();
builder.append("{\n\"resources\" : {\n");
serializeInclude(hints, builder);
serializeExclude(hints, builder);
builder.append("},\n");
serializeBundles(hints, builder);
builder.append("}\n");
return builder.toString();
}
private void serializeInclude(ResourceHints hints, StringBuilder builder) {
builder.append("\"includes\" : [\n");
Iterator<ResourcePatternHint> patternIterator = hints.resourcePatterns().iterator();
while (patternIterator.hasNext()) {
ResourcePatternHint hint = patternIterator.next();
Iterator<String> includeIterator = hint.getIncludes().iterator();
while (includeIterator.hasNext()) {
String pattern = JsonUtils.escape(patternToRegexp(includeIterator.next()));
builder.append("{ \"pattern\": \"").append(pattern).append("\" }");
if (includeIterator.hasNext()) {
builder.append(", ");
}
}
if (patternIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("\n],\n");
}
private void serializeExclude(ResourceHints hints, StringBuilder builder) {
builder.append("\"excludes\" : [\n");
Iterator<ResourcePatternHint> patternIterator = hints.resourcePatterns().iterator();
while (patternIterator.hasNext()) {
ResourcePatternHint hint = patternIterator.next();
Iterator<String> excludeIterator = hint.getExcludes().iterator();
while (excludeIterator.hasNext()) {
String pattern = JsonUtils.escape(patternToRegexp(excludeIterator.next()));
builder.append("{ \"pattern\": \"").append(pattern).append("\" }");
if (excludeIterator.hasNext()) {
builder.append(", ");
}
}
if (patternIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("\n]\n");
}
private void serializeBundles(ResourceHints hints, StringBuilder builder) {
builder.append("\"bundles\" : [\n");
Iterator<ResourceBundleHint> bundleIterator = hints.resourceBundles().iterator();
while (bundleIterator.hasNext()) {
String baseName = JsonUtils.escape(bundleIterator.next().getBaseName());
builder.append("{ \"name\": \"").append(baseName).append("\" }");
if (bundleIterator.hasNext()) {
builder.append(",\n");
}
}
builder.append("]\n");
}
private String patternToRegexp(String pattern) {
return Arrays.stream(pattern.split("\\*")).map(Pattern::quote).collect(Collectors.joining(".*"));
}
}

View File

@ -0,0 +1,9 @@
/**
* Support for generating GraalVM native configuration from runtime hints.
*/
@NonNullApi
@NonNullFields
package org.springframework.aot.nativex;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,192 @@
/*
* Copyright 2002-2022 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.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;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.JavaSerializationHints;
import org.springframework.aot.hint.MemberCategory;
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.TypeReference;
import org.springframework.core.codec.StringDecoder;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link FileNativeConfigurationGenerator}.
*
* @author Sebastien Deleuze
*/
public class FileNativeConfigurationGeneratorTests {
@TempDir
static Path tempDir;
@Test
void emptyConfig() {
Path empty = tempDir.resolve("empty");
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(empty);
generator.generate(new RuntimeHints());
assertThat(empty.toFile().listFiles()).isNull();
}
@Test
void serializationConfig() throws IOException, JSONException {
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
RuntimeHints hints = new RuntimeHints();
JavaSerializationHints serializationHints = hints.javaSerialization();
serializationHints.registerType(Integer.class);
serializationHints.registerType(Long.class);
generator.generate(hints);
assertEquals("""
[
{ "name" : "java.lang.Integer" },
{ "name" : "java.lang.Long" }
]""", "serialization-config.json");
}
@Test
void proxyConfig() throws IOException, JSONException {
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
RuntimeHints hints = new RuntimeHints();
ProxyHints proxyHints = hints.proxies();
proxyHints.registerJdkProxy(Function.class);
proxyHints.registerJdkProxy(Function.class, Consumer.class);
generator.generate(hints);
assertEquals("""
[
{ "interfaces" : [ "java.util.function.Function" ] },
{ "interfaces" : [ "java.util.function.Function", "java.util.function.Consumer" ] }
]""", "proxy-config.json");
}
@Test
void reflectionConfig() throws IOException, JSONException {
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
RuntimeHints hints = new RuntimeHints();
ReflectionHints reflectionHints = hints.reflection();
reflectionHints.registerType(StringDecoder.class, builder -> {
builder
.onReachableType(TypeReference.of(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", fieldBuilder -> {})
.withField("defaultCharset", fieldBuilder -> {
fieldBuilder.allowWrite(true);
fieldBuilder.allowUnsafeAccess(true);
})
.withConstructor(List.of(TypeReference.of(List.class), TypeReference.of(boolean.class), TypeReference.of(MimeType.class)), constructorHint ->
constructorHint.withMode(ExecutableMode.INTROSPECT))
.withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ctorBuilder -> {})
.withMethod("getDefaultCharset", Collections.emptyList(), constructorHint ->
constructorHint.withMode(ExecutableMode.INTROSPECT));
});
generator.generate(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", "allowWrite" = true, "allowUnsafeAccess" = true }
],
"methods" : [
{ "name" : "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
],
"queriedMethods" : [
{ "name" : "<init>", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
{ "name" : "getDefaultCharset" }
]
}
]""", "reflect-config.json");
}
@Test
void resourceConfig() throws IOException, JSONException {
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir);
RuntimeHints hints = new RuntimeHints();
ResourceHints resourceHints = hints.resources();
resourceHints.registerPattern("com/example/test.properties");
resourceHints.registerPattern("com/example/another.properties");
generator.generate(hints);
assertEquals("""
{
"resources": {
"includes" : [
{"pattern" : "\\\\Qcom/example/test.properties\\\\E"},
{"pattern" : "\\\\Qcom/example/another.properties\\\\E"}
]
}
}""", "resource-config.json");
}
@Test
void namespace() {
String groupId = "foo.bar";
String artifactId = "baz";
String filename = "resource-config.json";
FileNativeConfigurationGenerator generator = new FileNativeConfigurationGenerator(tempDir, groupId, artifactId);
RuntimeHints hints = new RuntimeHints();
ResourceHints resourceHints = hints.resources();
resourceHints.registerPattern("com/example/test.properties");
generator.generate(hints);
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename);
assertThat(jsonFile.toFile().exists()).isTrue();
}
private void assertEquals(String expectedString, String filename) throws IOException, JSONException {
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(filename);
String content = new String(Files.readAllBytes(jsonFile));
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.LENIENT);
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2002-2022 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 org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.aot.hint.JavaSerializationHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.env.Environment;
/**
* Tests for {@link JavaSerializationHintsSerializer}.
*
* @author Sebastien Deleuze
*/
public class JavaSerializationHintsSerializerTests {
private final JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
@Test
void empty() throws JSONException {
JavaSerializationHints hints = new JavaSerializationHints();
assertEquals("[]", hints);
}
@Test
void one() throws JSONException {
JavaSerializationHints hints = new JavaSerializationHints().registerType(TypeReference.of(String.class));
assertEquals("""
[
{ "name" : "java.lang.String" }
]""", hints);
}
@Test
void two() throws JSONException {
JavaSerializationHints hints = new JavaSerializationHints()
.registerType(TypeReference.of(String.class))
.registerType(TypeReference.of(Environment.class));
assertEquals("""
[
{ "name" : "java.lang.String" },
{ "name" : "org.springframework.core.env.Environment" }
]""", hints);
}
private void assertEquals(String expectedString, JavaSerializationHints hints) throws JSONException {
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2002-2022 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JsonUtils}.
*
* @author Sebastien Deleuze
*/
public class JsonUtilsTests {
@Test
void unescaped() {
assertThat(JsonUtils.escape("azerty")).isEqualTo("azerty");
}
@Test
void escapeDoubleQuote() {
assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
}
@Test
void escapeBackslash() {
assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
}
@Test
void escapeBackspace() {
assertThat(JsonUtils.escape("foo\bbar")).isEqualTo("foo\\bbar");
}
@Test
void escapeFormfeed() {
assertThat(JsonUtils.escape("foo\fbar")).isEqualTo("foo\\fbar");
}
@Test
void escapeNewline() {
assertThat(JsonUtils.escape("foo\nbar")).isEqualTo("foo\\nbar");
}
@Test
void escapeCarriageReturn() {
assertThat(JsonUtils.escape("foo\rbar")).isEqualTo("foo\\rbar");
}
@Test
void escapeTab() {
assertThat(JsonUtils.escape("foo\tbar")).isEqualTo("foo\\tbar");
}
@Test
void escapeUnicode() {
assertThat(JsonUtils.escape("foo\u001Fbar")).isEqualTo("foo\\u001fbar");
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2002-2022 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.function.Consumer;
import java.util.function.Function;
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;
/**
* Tests for {@link ProxyHintsSerializer}.
*
* @author Sebastien Deleuze
*/
public class ProxyHintsSerializerTests {
private final ProxyHintsSerializer serializer = new ProxyHintsSerializer();
@Test
void empty() throws JSONException {
ProxyHints hints = new ProxyHints();
assertEquals("[]", hints);
}
@Test
void one() throws JSONException {
ProxyHints hints = new ProxyHints();
hints.registerJdkProxy(Function.class);
assertEquals("""
[
{ "interfaces" : [ "java.util.function.Function" ] }
]""", hints);
}
@Test
void two() 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);
}
private void assertEquals(String expectedString, ProxyHints hints) throws JSONException {
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright 2002-2022 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.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;
/**
* Tests for {@link ReflectionHintsSerializer}.
*
* @author Sebastien Deleuze
*/
public class ReflectionHintsSerializerTests {
private final ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
@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(TypeReference.of(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", fieldBuilder -> {})
.withField("defaultCharset", fieldBuilder -> {
fieldBuilder.allowWrite(true);
fieldBuilder.allowUnsafeAccess(true);
})
.withConstructor(List.of(TypeReference.of(List.class), TypeReference.of(boolean.class), TypeReference.of(MimeType.class)), constructorHint ->
constructorHint.withMode(ExecutableMode.INTROSPECT))
.withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ctorBuilder -> {})
.withMethod("getDefaultCharset", Collections.emptyList(), constructorHint ->
constructorHint.withMode(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" : "DEFAULT_CHARSET" },
{ "name" : "defaultCharset", "allowWrite" = true, "allowUnsafeAccess" = true }
],
"methods" : [
{ "name" : "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] }
],
"queriedMethods" : [
{ "name" : "<init>", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] },
{ "name" : "getDefaultCharset" }
]
}
]""", 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);
}
private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException {
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2002-2022 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.IOException;
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;
/**
* Tests for {@link ResourceHintsSerializer}.
*
* @author Sebastien Deleuze
*/
public class ResourceHintsSerializerTests {
private final ResourceHintsSerializer serializer = new ResourceHintsSerializer();
@Test
void empty() throws IOException, 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" : "\\\\Qcom/example/test.properties\\\\E"},
{ "pattern" : "\\\\Qcom/example/another.properties\\\\E"}
]
}
}""", hints);
}
@Test
void registerPattern() throws JSONException {
ResourceHints hints = new ResourceHints();
hints.registerPattern("com/example/*.properties");
assertEquals("""
{
"resources": {
"includes" : [
{ "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}
]
}
}""", hints);
}
@Test
void registerPatternWithIncludesAndExcludes() throws JSONException {
ResourceHints hints = new ResourceHints();
hints.registerPattern("com/example/*.properties", hint -> hint.excludes("com/example/to-ignore.properties"));
hints.registerPattern("org/example/*.properties", hint -> hint.excludes("org/example/to-ignore.properties"));
assertEquals("""
{
"resources": {
"includes": [
{ "pattern" : "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
{ "pattern" : "\\\\Qorg/example/\\\\E.*\\\\Q.properties\\\\E"}
],
"excludes": [
{ "pattern" : "\\\\Qcom/example/to-ignore.properties\\\\E"},
{ "pattern" : "\\\\Qorg/example/to-ignore.properties\\\\E"}
]
}
}""", hints);
}
@Test
void registerType() throws JSONException {
ResourceHints hints = new ResourceHints();
hints.registerType(String.class);
assertEquals("""
{
"resources": {
"includes" : [
{ "pattern" : "\\\\Qjava/lang/String.class\\\\E"}
]
}
}""", hints);
}
@Test
void registerResourceBundle() throws JSONException {
ResourceHints hints = new ResourceHints();
hints.registerResourceBundle("com.example.message");
hints.registerResourceBundle("com.example.message2");
assertEquals("""
{
"bundles": [
{ "name" : "com.example.message"},
{ "name" : "com.example.message2"}
]
}""", hints);
}
private void assertEquals(String expectedString, ResourceHints hints) throws JSONException {
JSONAssert.assertEquals(expectedString, serializer.serialize(hints), JSONCompareMode.LENIENT);
}
}