Skip to content

Commit 77e0100

Browse files
committed
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
1 parent bb9cf7c commit 77e0100

15 files changed

+1280
-0
lines changed

spring-core/spring-core.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dependencies {
6767
testImplementation("org.xmlunit:xmlunit-matchers")
6868
testImplementation("io.projectreactor:reactor-test")
6969
testImplementation("io.projectreactor.tools:blockhound")
70+
testImplementation("org.skyscreamer:jsonassert")
7071
testFixturesImplementation("com.google.code.findbugs:jsr305")
7172
testFixturesImplementation("org.junit.platform:junit-platform-launcher")
7273
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.nativex;
18+
19+
import java.io.File;
20+
import java.io.FileWriter;
21+
import java.io.IOException;
22+
import java.nio.file.Path;
23+
24+
import org.springframework.aot.hint.JavaSerializationHints;
25+
import org.springframework.aot.hint.ProxyHints;
26+
import org.springframework.aot.hint.ReflectionHints;
27+
import org.springframework.aot.hint.ResourceHints;
28+
import org.springframework.aot.hint.RuntimeHints;
29+
import org.springframework.lang.Nullable;
30+
31+
/**
32+
* Generate the GraalVM native configuration files from runtime hints.
33+
*
34+
* @author Sebastien Deleuze
35+
* @since 6.0
36+
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
37+
*/
38+
public class FileNativeConfigurationGenerator implements NativeConfigurationGenerator {
39+
40+
private final Path basePath;
41+
42+
private final String groupId;
43+
44+
private final String artifactId;
45+
46+
public FileNativeConfigurationGenerator(Path basePath) {
47+
this(basePath, null, null);
48+
}
49+
50+
public FileNativeConfigurationGenerator(Path basePath, @Nullable String groupId, @Nullable String artifactId) {
51+
this.basePath = basePath;
52+
if ((groupId == null && artifactId != null) || (groupId != null && artifactId == null)) {
53+
throw new IllegalArgumentException("groupId and artifactId must be both null or both non-null");
54+
}
55+
this.groupId = groupId;
56+
this.artifactId = artifactId;
57+
}
58+
59+
@Override
60+
public void generate(RuntimeHints hints) {
61+
try {
62+
if (hints.javaSerialization().types().findAny().isPresent()) {
63+
generateFile(hints.javaSerialization());
64+
}
65+
if (hints.proxies().jdkProxies().findAny().isPresent()) {
66+
generateFile(hints.proxies());
67+
}
68+
if (hints.reflection().typeHints().findAny().isPresent()) {
69+
generateFile(hints.reflection());
70+
}
71+
if (hints.resources().resourcePatterns().findAny().isPresent() ||
72+
hints.resources().resourceBundles().findAny().isPresent()) {
73+
generateFile(hints.resources());
74+
}
75+
}
76+
catch (IOException ex) {
77+
throw new IllegalStateException("Unexpected I/O error while writing the native configuration", ex);
78+
}
79+
}
80+
81+
/**
82+
* Generate the Java serialization native configuration file.
83+
*/
84+
private void generateFile(JavaSerializationHints hints) throws IOException {
85+
JavaSerializationHintsSerializer serializer = new JavaSerializationHintsSerializer();
86+
File file = createIfNecessary("serialization-config.json");
87+
FileWriter writer = new FileWriter(file);
88+
writer.write(serializer.serialize(hints));
89+
writer.close();
90+
}
91+
92+
/**
93+
* Generate the proxy native configuration file.
94+
*/
95+
private void generateFile(ProxyHints hints) throws IOException {
96+
ProxyHintsSerializer serializer = new ProxyHintsSerializer();
97+
File file = createIfNecessary("proxy-config.json");
98+
FileWriter writer = new FileWriter(file);
99+
writer.write(serializer.serialize(hints));
100+
writer.close();
101+
}
102+
103+
/**
104+
* Generate the reflection native configuration file.
105+
*/
106+
private void generateFile(ReflectionHints hints) throws IOException {
107+
ReflectionHintsSerializer serializer = new ReflectionHintsSerializer();
108+
File file = createIfNecessary("reflect-config.json");
109+
FileWriter writer = new FileWriter(file);
110+
writer.write(serializer.serialize(hints));
111+
writer.close();
112+
}
113+
114+
/**
115+
* Generate the resource native configuration file.
116+
*/
117+
private void generateFile(ResourceHints hints) throws IOException {
118+
ResourceHintsSerializer serializer = new ResourceHintsSerializer();
119+
File file = createIfNecessary("resource-config.json");
120+
FileWriter writer = new FileWriter(file);
121+
writer.write(serializer.serialize(hints));
122+
writer.close();
123+
}
124+
125+
private File createIfNecessary(String filename) throws IOException {
126+
Path outputDirectory = this.basePath.resolve("META-INF").resolve("native-image");
127+
if (this.groupId != null && this.artifactId != null) {
128+
outputDirectory = outputDirectory.resolve(this.groupId).resolve(this.artifactId);
129+
}
130+
outputDirectory.toFile().mkdirs();
131+
File file = outputDirectory.resolve(filename).toFile();
132+
file.createNewFile();
133+
return file;
134+
}
135+
136+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.nativex;
18+
19+
import java.util.Iterator;
20+
21+
import org.springframework.aot.hint.JavaSerializationHints;
22+
import org.springframework.aot.hint.TypeReference;
23+
24+
/**
25+
* Serialize a {@link JavaSerializationHints} to the JSON file expected by GraalVM {@code native-image} compiler,
26+
* typically named {@code serialization-config.json}.
27+
*
28+
* @author Sebastien Deleuze
29+
* @since 6.0
30+
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
31+
*/
32+
class JavaSerializationHintsSerializer {
33+
34+
public String serialize(JavaSerializationHints hints) {
35+
StringBuilder builder = new StringBuilder();
36+
builder.append("[\n");
37+
Iterator<TypeReference> typeIterator = hints.types().iterator();
38+
while (typeIterator.hasNext()) {
39+
TypeReference type = typeIterator.next();
40+
String name = JsonUtils.escape(type.getCanonicalName());
41+
builder.append("{ \"name\": \"").append(name).append("\" }");
42+
if (typeIterator.hasNext()) {
43+
builder.append(",\n");
44+
}
45+
}
46+
builder.append("\n]\n");
47+
return builder.toString();
48+
}
49+
50+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.nativex;
18+
19+
/**
20+
* Utility class for JSON.
21+
*
22+
* @author Sebastien Deleuze
23+
*/
24+
abstract class JsonUtils {
25+
26+
/**
27+
* Escape a JSON String.
28+
*/
29+
static String escape(String input) {
30+
StringBuilder builder = new StringBuilder();
31+
input.chars().forEach(c -> {
32+
switch (c) {
33+
case '"':
34+
builder.append("\\\"");
35+
break;
36+
case '\\':
37+
builder.append("\\\\");
38+
break;
39+
case '/':
40+
builder.append("\\/");
41+
break;
42+
case '\b':
43+
builder.append("\\b");
44+
break;
45+
case '\f':
46+
builder.append("\\f");
47+
break;
48+
case '\n':
49+
builder.append("\\n");
50+
break;
51+
case '\r':
52+
builder.append("\\r");
53+
break;
54+
case '\t':
55+
builder.append("\\t");
56+
break;
57+
default:
58+
if (c <= 0x1F) {
59+
builder.append(String.format("\\u%04x", c));
60+
}
61+
else {
62+
builder.append((char) c);
63+
}
64+
break;
65+
}
66+
});
67+
return builder.toString();
68+
}
69+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.nativex;
18+
19+
import org.springframework.aot.hint.RuntimeHints;
20+
21+
/**
22+
* Generate GraalVM native configuration.
23+
*
24+
* @author Sebastien Deleuze
25+
* @since 6.0
26+
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
27+
*/
28+
public interface NativeConfigurationGenerator {
29+
30+
/**
31+
* Generate the GraalVM native configuration from the provided hints.
32+
* @param hints the hints to serialize
33+
*/
34+
void generate(RuntimeHints hints);
35+
36+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.nativex;
18+
19+
import java.util.Iterator;
20+
21+
import org.springframework.aot.hint.JdkProxyHint;
22+
import org.springframework.aot.hint.ProxyHints;
23+
import org.springframework.aot.hint.TypeReference;
24+
25+
/**
26+
* Serialize {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON file expected by GraalVM
27+
* {@code native-image} compiler, typically named {@code proxy-config.json}.
28+
*
29+
* @author Sebastien Deleuze
30+
* @since 6.0
31+
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/DynamicProxy/">Dynamic Proxy in Native Image</a>
32+
* @see <a href="https://www.graalvm.org/22.0/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
33+
*/
34+
class ProxyHintsSerializer {
35+
36+
public String serialize(ProxyHints hints) {
37+
StringBuilder builder = new StringBuilder();
38+
builder.append("[\n");
39+
Iterator<JdkProxyHint> hintIterator = hints.jdkProxies().iterator();
40+
while (hintIterator.hasNext()) {
41+
builder.append("{ \"interfaces\": [ ");
42+
JdkProxyHint hint = hintIterator.next();
43+
Iterator<TypeReference> interfaceIterator = hint.getProxiedInterfaces().iterator();
44+
while (interfaceIterator.hasNext()) {
45+
String name = JsonUtils.escape(interfaceIterator.next().getCanonicalName());
46+
builder.append("\"").append(name).append("\"");
47+
if (interfaceIterator.hasNext()) {
48+
builder.append(", ");
49+
}
50+
}
51+
builder.append(" ] }");
52+
if (hintIterator.hasNext()) {
53+
builder.append(",\n");
54+
}
55+
}
56+
builder.append("\n]\n");
57+
return builder.toString();
58+
}
59+
60+
}

0 commit comments

Comments
 (0)