diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/RuntimeHintsUtils.java b/spring-core/src/main/java/org/springframework/aot/hint/support/RuntimeHintsUtils.java index 9668f1d891a..fa6cc03b91a 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/RuntimeHintsUtils.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/RuntimeHintsUtils.java @@ -19,11 +19,14 @@ package org.springframework.aot.hint.support; import java.util.function.Consumer; import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeHint.Builder; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; /** * Utility methods for runtime hints support code. @@ -92,4 +95,18 @@ public abstract class RuntimeHintsUtils { } } + /** + * Determine if the supplied resource is a {@link ClassPathResource} that + * {@linkplain Resource#exists() exists} and register the resource for run-time + * availability accordingly. + * @param hints the {@link RuntimeHints} instance to use + * @param resource the resource to register + * @see ResourceHints#registerPattern(String) + */ + public static void registerResourceIfNecessary(RuntimeHints hints, Resource resource) { + if (resource instanceof ClassPathResource classPathResource && classPathResource.exists()) { + hints.resources().registerPattern(classPathResource.getPath()); + } + } + } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/RuntimeHintsUtilsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/RuntimeHintsUtilsTests.java index ca4f576e6ea..0b8b0248584 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/RuntimeHintsUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/RuntimeHintsUtilsTests.java @@ -20,6 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.function.Consumer; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.JdkProxyHint; @@ -28,8 +29,11 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DescriptiveResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource; /** * Tests for {@link RuntimeHintsUtils}. @@ -41,6 +45,39 @@ class RuntimeHintsUtilsTests { private final RuntimeHints hints = new RuntimeHints(); + @Test + void registerResourceIfNecessaryWithUnsupportedResourceType() { + DescriptiveResource resource = new DescriptiveResource("bogus"); + RuntimeHintsUtils.registerResourceIfNecessary(this.hints, resource); + assertThat(this.hints.resources().resourcePatterns()).isEmpty(); + } + + @Test + void registerResourceIfNecessaryWithNonexistentClassPathResource() { + ClassPathResource resource = new ClassPathResource("bogus", getClass()); + RuntimeHintsUtils.registerResourceIfNecessary(this.hints, resource); + assertThat(this.hints.resources().resourcePatterns()).isEmpty(); + } + + @Test + void registerResourceIfNecessaryWithExistingClassPathResource() { + String path = "org/springframework/aot/hint/support"; + ClassPathResource resource = new ClassPathResource(path); + RuntimeHintsUtils.registerResourceIfNecessary(this.hints, resource); + assertThat(resource().forResource(path)).accepts(this.hints); + } + + @Disabled("Disabled since ClassPathResource.getPath() does not honor its contract for relative resources") + @Test + void registerResourceIfNecessaryWithExistingRelativeClassPathResource() { + String path = "org/springframework/aot/hint/support"; + ClassPathResource resource = new ClassPathResource("support", RuntimeHints.class); + RuntimeHintsUtils.registerResourceIfNecessary(this.hints, resource); + // This unfortunately fails since ClassPathResource.getPath() returns + // "support" instead of "org/springframework/aot/hint/support". + assertThat(resource().forResource(path)).accepts(this.hints); + } + @Test @SuppressWarnings("deprecation") void registerSynthesizedAnnotation() { diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java index 6204cb2e52b..54dd5bd4af5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/hint/StandardTestRuntimeHints.java @@ -20,7 +20,9 @@ import java.util.Arrays; import java.util.List; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.support.RuntimeHintsUtils; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfilesResolver; import org.springframework.test.context.ContextLoader; @@ -50,12 +52,12 @@ class StandardTestRuntimeHints implements TestRuntimeHintsRegistrar { public void registerHints(MergedContextConfiguration mergedConfig, List> testClasses, RuntimeHints runtimeHints, ClassLoader classLoader) { - registerHintsForMergedContextConfiguration(runtimeHints, mergedConfig); + registerHintsForMergedContextConfiguration(runtimeHints, classLoader, mergedConfig); testClasses.forEach(testClass -> registerHintsForActiveProfilesResolvers(runtimeHints, testClass)); } private void registerHintsForMergedContextConfiguration( - RuntimeHints runtimeHints, MergedContextConfiguration mergedConfig) { + RuntimeHints runtimeHints, ClassLoader classLoader, MergedContextConfiguration mergedConfig) { // @ContextConfiguration(loader = ...) ContextLoader contextLoader = mergedConfig.getContextLoader(); @@ -68,14 +70,14 @@ class StandardTestRuntimeHints implements TestRuntimeHintsRegistrar { .forEach(clazz -> registerDeclaredConstructors(runtimeHints, clazz)); // @ContextConfiguration(locations = ...) - registerClasspathResources(runtimeHints, mergedConfig.getLocations()); + registerClasspathResources(mergedConfig.getLocations(), runtimeHints, classLoader); // @TestPropertySource(locations = ... ) - registerClasspathResources(runtimeHints, mergedConfig.getPropertySourceLocations()); + registerClasspathResources(mergedConfig.getPropertySourceLocations(), runtimeHints, classLoader); // @WebAppConfiguration(value = ...) if (mergedConfig instanceof WebMergedContextConfiguration webConfig) { - registerClasspathResourceDirectoryStructure(runtimeHints, webConfig.getResourceBasePath()); + registerClasspathResourceDirectoryStructure(webConfig.getResourceBasePath(), runtimeHints); } } @@ -94,16 +96,20 @@ class StandardTestRuntimeHints implements TestRuntimeHintsRegistrar { runtimeHints.reflection().registerType(type, INVOKE_DECLARED_CONSTRUCTORS); } - private void registerClasspathResources(RuntimeHints runtimeHints, String... locations) { - Arrays.stream(locations) - .filter(location -> location.startsWith(CLASSPATH_URL_PREFIX)) - .map(this::cleanClasspathResource) - .forEach(runtimeHints.resources()::registerPattern); + private void registerClasspathResources(String[] paths, RuntimeHints runtimeHints, ClassLoader classLoader) { + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader); + Arrays.stream(paths) + .filter(path -> path.startsWith(CLASSPATH_URL_PREFIX)) + .map(resourceLoader::getResource) + .forEach(resource -> RuntimeHintsUtils.registerResourceIfNecessary(runtimeHints, resource)); } - private void registerClasspathResourceDirectoryStructure(RuntimeHints runtimeHints, String directory) { + private void registerClasspathResourceDirectoryStructure(String directory, RuntimeHints runtimeHints) { if (directory.startsWith(CLASSPATH_URL_PREFIX)) { - String pattern = cleanClasspathResource(directory); + String pattern = directory.substring(CLASSPATH_URL_PREFIX.length()); + if (pattern.startsWith(SLASH)) { + pattern = pattern.substring(1); + } if (!pattern.endsWith(SLASH)) { pattern += SLASH; } @@ -112,12 +118,4 @@ class StandardTestRuntimeHints implements TestRuntimeHintsRegistrar { } } - private String cleanClasspathResource(String location) { - location = location.substring(CLASSPATH_URL_PREFIX.length()); - if (location.startsWith(SLASH)) { - location = location.substring(1); - } - return location; - } - } diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index 1f4192a6354..581f9dd978a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -28,10 +28,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.support.RuntimeHintsUtils; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.lang.NonNull; @@ -148,10 +150,10 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen @Override public void processAheadOfTime(Class testClass, RuntimeHints runtimeHints, ClassLoader classLoader) { getSqlAnnotationsFor(testClass).forEach(sql -> - registerClasspathResources(runtimeHints, getScripts(sql, testClass, null, true))); + registerClasspathResources(getScripts(sql, testClass, null, true), runtimeHints, classLoader)); getSqlMethods(testClass).forEach(testMethod -> getSqlAnnotationsFor(testMethod).forEach(sql -> - registerClasspathResources(runtimeHints, getScripts(sql, testClass, testMethod, false)))); + registerClasspathResources(getScripts(sql, testClass, testMethod, false), runtimeHints, classLoader))); } /** @@ -390,19 +392,12 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen return Arrays.stream(ReflectionUtils.getUniqueDeclaredMethods(testClass, sqlMethodFilter)); } - private void registerClasspathResources(RuntimeHints runtimeHints, String... locations) { - Arrays.stream(locations) - .filter(location -> location.startsWith(CLASSPATH_URL_PREFIX)) - .map(this::cleanClasspathResource) - .forEach(runtimeHints.resources()::registerPattern); - } - - private String cleanClasspathResource(String location) { - location = location.substring(CLASSPATH_URL_PREFIX.length()); - if (location.startsWith(SLASH)) { - location = location.substring(1); - } - return location; + private void registerClasspathResources(String[] paths, RuntimeHints runtimeHints, ClassLoader classLoader) { + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(classLoader); + Arrays.stream(paths) + .filter(path -> path.startsWith(CLASSPATH_URL_PREFIX)) + .map(resourceLoader::getResource) + .forEach(resource -> RuntimeHintsUtils.registerResourceIfNecessary(runtimeHints, resource)); } }