Automatically register directories for registered resource hints

When a hint such as `graphql/*.*` is registered for resources that are
looked up via classpath scanning using a pattern such as
`classpath*:graphql/**/*.graphqls`, an appropriate pattern is in fact
registered in the generated `resource-config.json` file for GraalVM
native images; however, classpath scanning fails since GraalVM
currently does not make the `graphql` directory automatically available
as a classpath resource.

This can be very confusing and cumbersome for users since a file such
as `graphql/schema.graphqls` will not be discovered via classpath
scanning even though the file is present in the native image filesystem.

To address this, this commit automatically registers resource hints for
enclosing directories for a registered pattern.

If the GraalVM team later decides to perform automatic directory
registration, we can then remove the code introduced in conjunction
with this issue.

Closes gh-29403
This commit is contained in:
Sam Brannen 2022-10-30 18:37:02 +01:00
parent d03102edc3
commit 29f085bd1a
7 changed files with 123 additions and 37 deletions

View File

@ -149,7 +149,15 @@ class ConfigurationClassPostProcessorAotContributionTests {
.singleElement()
.satisfies(resourceHint -> assertThat(resourceHint.getIncludes())
.map(ResourcePatternHint::getPattern)
.containsOnly("org/springframework/context/testfixture/context/annotation/ImportConfiguration.class"));
.containsExactlyInAnyOrder(
"org",
"org/springframework",
"org/springframework/context",
"org/springframework/context/testfixture",
"org/springframework/context/testfixture/context",
"org/springframework/context/testfixture/context/annotation",
"org/springframework/context/testfixture/context/annotation/ImportConfiguration.class"
));
}
@SuppressWarnings("unchecked")

View File

@ -31,6 +31,7 @@ import org.springframework.lang.Nullable;
*
* @author Stephane Nicoll
* @author Brian Clozel
* @author Sam Brannen
* @since 6.0
*/
public final class ResourcePatternHints {
@ -81,12 +82,57 @@ public final class ResourcePatternHints {
* @return {@code this}, to facilitate method chaining
*/
public Builder includes(@Nullable TypeReference reachableType, String... includes) {
List<ResourcePatternHint> newIncludes = Arrays.stream(includes)
.map(include -> new ResourcePatternHint(include, reachableType)).toList();
this.includes.addAll(newIncludes);
Arrays.stream(includes)
.map(this::expandToIncludeDirectories)
.flatMap(List::stream)
.map(include -> new ResourcePatternHint(include, reachableType))
.forEach(this.includes::add);
return this;
}
/**
* Expand the supplied include pattern into multiple patterns that include
* all parent directories for the ultimate resource or resources.
* <p>This is necessary to support classpath scanning within a GraalVM
* native image.
* @see <a href="https://github.com/spring-projects/spring-framework/issues/29403">gh-29403</a>
*/
private List<String> expandToIncludeDirectories(String includePattern) {
// Root resource or no explicit subdirectories?
if (!includePattern.contains("/")) {
if (includePattern.contains("*")) {
// If it's a root pattern, include the root directory as well as the pattern
return List.of("/", includePattern);
}
else {
// Include only the root resource
return List.of(includePattern);
}
}
List<String> includePatterns = new ArrayList<>();
// Ensure the original pattern is always included
includePatterns.add(includePattern);
StringBuilder path = new StringBuilder();
for (String pathElement : includePattern.split("/")) {
if (pathElement.isEmpty()) {
// Skip empty path elements
continue;
}
if (pathElement.contains("*")) {
// Stop at the first encountered wildcard, since we cannot reliably reason
// any further about the directory structure below this path element.
break;
}
if (!path.isEmpty()) {
path.append("/");
}
path.append(pathElement);
includePatterns.add(path.toString());
}
return includePatterns;
}
/**
* Include resources matching the specified patterns.
* @param includes the include patterns (see {@link ResourcePatternHint} documentation)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* 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.
@ -46,21 +46,23 @@ class ResourceHintsTests {
void registerType() {
this.resourceHints.registerType(String.class);
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("java/lang/String.class"));
patternOf("java", "java/lang", "java/lang/String.class"));
}
@Test
void registerTypeWithNestedType() {
this.resourceHints.registerType(TypeReference.of(Nested.class));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org/springframework/aot/hint/ResourceHintsTests$Nested.class"));
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint",
"org/springframework/aot/hint/ResourceHintsTests$Nested.class"));
}
@Test
void registerTypeWithInnerNestedType() {
this.resourceHints.registerType(TypeReference.of(Inner.class));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org/springframework/aot/hint/ResourceHintsTests$Nested$Inner.class"));
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint",
"org/springframework/aot/hint/ResourceHintsTests$Nested$Inner.class"));
}
@Test
@ -68,16 +70,16 @@ class ResourceHintsTests {
this.resourceHints.registerType(String.class);
this.resourceHints.registerType(TypeReference.of(String.class));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("java/lang/String.class"));
patternOf("java", "java/lang", "java/lang/String.class"));
}
@Test
void registerExactMatch() {
void registerExactMatches() {
this.resourceHints.registerPattern("com/example/test.properties");
this.resourceHints.registerPattern("com/example/another.properties");
assertThat(this.resourceHints.resourcePatternHints())
.anySatisfy(patternOf("com/example/test.properties"))
.anySatisfy(patternOf("com/example/another.properties"))
.anySatisfy(patternOf("com", "com/example", "com/example/test.properties"))
.anySatisfy(patternOf("com", "com/example", "com/example/another.properties"))
.hasSize(2);
}
@ -88,11 +90,18 @@ class ResourceHintsTests {
patternOf("/"));
}
@Test
void registerRootPattern() {
this.resourceHints.registerPattern("*.properties");
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("/", "*.properties"));
}
@Test
void registerPattern() {
this.resourceHints.registerPattern("com/example/*.properties");
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("com/example/*.properties"));
patternOf("com", "com/example", "com/example/*.properties"));
}
@Test
@ -100,7 +109,7 @@ class ResourceHintsTests {
this.resourceHints.registerPattern(resourceHint ->
resourceHint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties"));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(
List.of("com/example/*.properties"),
List.of("com", "com/example", "com/example/*.properties"),
List.of("com/example/to-ignore.properties")));
}
@ -109,7 +118,7 @@ class ResourceHintsTests {
this.resourceHints.registerPatternIfPresent(null, "META-INF/",
resourceHint -> resourceHint.includes("com/example/*.properties"));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("com/example/*.properties"));
patternOf("com", "com/example", "com/example/*.properties"));
}
@Test
@ -142,7 +151,8 @@ class ResourceHintsTests {
String path = "org/springframework/aot/hint/support";
ClassPathResource resource = new ClassPathResource(path);
this.resourceHints.registerResource(resource);
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(path));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint", path));
}
@Test
@ -150,7 +160,8 @@ class ResourceHintsTests {
String path = "org/springframework/aot/hint/support";
ClassPathResource resource = new ClassPathResource("support", RuntimeHints.class);
this.resourceHints.registerResource(resource);
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf(path));
assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(
patternOf("org", "org/springframework", "org/springframework/aot", "org/springframework/aot/hint", path));
}
@Test
@ -179,7 +190,7 @@ class ResourceHintsTests {
private Consumer<ResourcePatternHints> patternOf(List<String> includes, List<String> excludes) {
return pattern -> {
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(includes);
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyInAnyOrderElementsOf(includes);
assertThat(pattern.getExcludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(excludes);
};
}

View File

@ -31,6 +31,7 @@ class RuntimeHintsTests {
private final RuntimeHints hints = new RuntimeHints();
@Test
void reflectionHintWithClass() {
this.hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
@ -47,7 +48,8 @@ class RuntimeHintsTests {
void resourceHintWithClass() {
this.hints.resources().registerType(String.class);
assertThat(this.hints.resources().resourcePatternHints()).singleElement().satisfies(resourceHint -> {
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern).containsExactly("java/lang/String.class");
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern)
.containsExactlyInAnyOrder("java", "java/lang", "java/lang/String.class");
assertThat(resourceHint.getExcludes()).isEmpty();
});
}

View File

@ -37,6 +37,7 @@ class FilePatternResourceHintsRegistrarTests {
private final ResourceHints hints = new ResourceHints();
@Test
void createWithInvalidName() {
assertThatIllegalArgumentException().isThrownBy(() -> new FilePatternResourceHintsRegistrar(
@ -56,7 +57,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt"));
.satisfies(includes("/", "test*.txt"));
}
@Test
@ -64,7 +65,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test", "another"), List.of(""), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt", "another*.txt"));
.satisfies(includes("/" , "test*.txt", "another*.txt"));
}
@Test
@ -72,7 +73,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("", "META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt", "META-INF/test*.txt"));
.satisfies(includes("/", "test*.txt", "META-INF", "META-INF/test*.txt"));
}
@Test
@ -80,7 +81,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of(""), List.of(".txt", ".conf"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt", "test*.conf"));
.satisfies(includes("/", "test*.txt", "test*.conf"));
}
@Test
@ -88,7 +89,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("META-INF/test*.txt"));
.satisfies(includes("META-INF", "META-INF/test*.txt"));
}
@Test
@ -96,7 +97,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("/"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("test*.txt"));
.satisfies(includes("/", "test*.txt"));
}
@Test
@ -104,7 +105,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("META-INF/test*.txt"));
.satisfies(includes("META-INF", "META-INF/test*.txt"));
}
@Test
@ -112,7 +113,7 @@ class FilePatternResourceHintsRegistrarTests {
new FilePatternResourceHintsRegistrar(List.of("test"), List.of("classpath:/META-INF"), List.of(".txt"))
.registerHints(this.hints, null);
assertThat(this.hints.resourcePatternHints()).singleElement()
.satisfies(includes("META-INF/test*.txt"));
.satisfies(includes("META-INF", "META-INF/test*.txt"));
}
@Test

View File

@ -49,12 +49,14 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Sebastien Deleuze
* @author Janne Valkealahti
* @author Sam Brannen
*/
public class FileNativeConfigurationWriterTests {
class FileNativeConfigurationWriterTests {
@TempDir
static Path tempDir;
@Test
void emptyConfig() {
Path empty = tempDir.resolve("empty");
@ -174,6 +176,8 @@ public class FileNativeConfigurationWriterTests {
"resources": {
"includes": [
{"pattern": "\\\\Qcom/example/test.properties\\\\E"},
{"pattern": "\\\\Qcom\\\\E"},
{"pattern": "\\\\Qcom/example\\\\E"},
{"pattern": "\\\\Qcom/example/another.properties\\\\E"}
]
}
@ -191,12 +195,12 @@ public class FileNativeConfigurationWriterTests {
resourceHints.registerPattern("com/example/test.properties");
generator.write(hints);
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(groupId).resolve(artifactId).resolve(filename);
assertThat(jsonFile.toFile().exists()).isTrue();
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);
String content = new String(Files.readAllBytes(jsonFile));
String content = Files.readString(jsonFile);
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE);
}

View File

@ -32,7 +32,7 @@ import org.springframework.aot.hint.TypeReference;
* @author Sebastien Deleuze
* @author Brian Clozel
*/
public class ResourceHintsWriterTests {
class ResourceHintsWriterTests {
@Test
void empty() throws JSONException {
@ -50,6 +50,8 @@ public class ResourceHintsWriterTests {
"resources": {
"includes": [
{ "pattern": "\\\\Qcom/example/test.properties\\\\E"},
{ "pattern": "\\\\Qcom\\\\E"},
{ "pattern": "\\\\Qcom/example\\\\E"},
{ "pattern": "\\\\Qcom/example/another.properties\\\\E"}
]
}
@ -64,7 +66,8 @@ public class ResourceHintsWriterTests {
{
"resources": {
"includes": [
{ "pattern": ".*\\\\Q.properties\\\\E"}
{ "pattern": ".*\\\\Q.properties\\\\E"},
{ "pattern": "\\\\Q\\/\\\\E"}
]
}
}""", hints);
@ -78,7 +81,9 @@ public class ResourceHintsWriterTests {
{
"resources": {
"includes": [
{ "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}
{ "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
{ "pattern": "\\\\Qcom\\\\E"},
{ "pattern": "\\\\Qcom/example\\\\E"}
]
}
}""", hints);
@ -92,7 +97,8 @@ public class ResourceHintsWriterTests {
{
"resources": {
"includes": [
{ "pattern": "\\\\Qstatic/\\\\E.*"}
{ "pattern": "\\\\Qstatic/\\\\E.*"},
{ "pattern": "\\\\Qstatic\\\\E"}
]
}
}""", hints);
@ -108,7 +114,11 @@ public class ResourceHintsWriterTests {
"resources": {
"includes": [
{ "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
{ "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"}
{ "pattern": "\\\\Qcom\\\\E"},
{ "pattern": "\\\\Qcom/example\\\\E"},
{ "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"},
{ "pattern": "\\\\Qorg\\\\E"},
{ "pattern": "\\\\Qorg/other\\\\E"}
],
"excludes": [
{ "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"},
@ -126,7 +136,9 @@ public class ResourceHintsWriterTests {
{
"resources": {
"includes": [
{ "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"}
{ "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"},
{ "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom\\\\E"},
{ "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example\\\\E"}
]
}
}""", hints);
@ -140,7 +152,9 @@ public class ResourceHintsWriterTests {
{
"resources": {
"includes": [
{ "pattern": "\\\\Qjava/lang/String.class\\\\E"}
{ "pattern": "\\\\Qjava/lang/String.class\\\\E" },
{ "pattern": "\\\\Qjava\\\\E" },
{ "pattern": "\\\\Qjava/lang\\\\E" }
]
}
}""", hints);