diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebResourcesRuntimeHintsRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebResourcesRuntimeHintsRegistrar.java new file mode 100644 index 0000000000..f2d7367fb3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebResourcesRuntimeHintsRegistrar.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-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.boot.autoconfigure.web; + +import java.util.List; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +/** + * {@link RuntimeHintsRegistrar} for default locations of web resources. + * + * @author Stephane Nicoll + * @since 3.0 + */ +public class WebResourcesRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + private static final List DEFAULT_LOCATIONS = List.of("META-INF/resources/", "resources/", "static/", + "public/"); + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + ClassLoader classLoaderToUse = (classLoader != null) ? classLoader : getClass().getClassLoader(); + String[] locations = DEFAULT_LOCATIONS.stream() + .filter((candidate) -> classLoaderToUse.getResource(candidate) != null) + .map((location) -> location + "*").toArray(String[]::new); + if (locations.length > 0) { + hints.resources().registerPattern((hint) -> hint.includes(locations)); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index a18b3d0865..e3574e4a75 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceCh import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.WebResourcesRuntimeHintsRegistrar; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format; @@ -50,6 +51,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.format.FormatterRegistry; @@ -104,6 +106,7 @@ import org.springframework.web.server.session.WebSessionManager; @ConditionalOnClass(WebFluxConfigurer.class) @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) +@ImportRuntimeHints(WebResourcesRuntimeHintsRegistrar.class) public class WebFluxAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index be1e13d18e..ae3871bbfd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -50,6 +50,7 @@ import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceCh import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain.Strategy; +import org.springframework.boot.autoconfigure.web.WebResourcesRuntimeHintsRegistrar; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format; @@ -64,6 +65,7 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; @@ -143,6 +145,7 @@ import org.springframework.web.util.pattern.PathPatternParser; @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) +@ImportRuntimeHints(WebResourcesRuntimeHintsRegistrar.class) public class WebMvcAutoConfiguration { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebResourcesRuntimeHintsRegistrarTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebResourcesRuntimeHintsRegistrarTests.java new file mode 100644 index 0000000000..8efeb56b5f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebResourcesRuntimeHintsRegistrarTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-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.boot.autoconfigure.web; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.ResourcePatternHint; +import org.springframework.aot.hint.ResourcePatternHints; +import org.springframework.aot.hint.RuntimeHints; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebResourcesRuntimeHintsRegistrar}. + * + * @author Stephane Nicoll + */ +class WebResourcesRuntimeHintsRegistrarTests { + + @Test + void registerHintsWithAllLocations() { + RuntimeHints hints = register( + new TestClassLoader(List.of("META-INF/resources/", "resources/", "static/", "public/"))); + assertThat(hints.resources().resourcePatterns()).singleElement() + .satisfies(include("META-INF/resources/*", "resources/*", "static/*", "public/*")); + } + + @Test + void registerHintsWithOnlyStaticLocations() { + RuntimeHints hints = register(new TestClassLoader(List.of("static/"))); + assertThat(hints.resources().resourcePatterns()).singleElement().satisfies(include("static/*")); + } + + @Test + void registerHintsWithNoLocation() { + RuntimeHints hints = register(new TestClassLoader(List.of())); + assertThat(hints.resources().resourcePatterns()).isEmpty(); + } + + RuntimeHints register(ClassLoader classLoader) { + RuntimeHints hints = new RuntimeHints(); + WebResourcesRuntimeHintsRegistrar registrar = new WebResourcesRuntimeHintsRegistrar(); + registrar.registerHints(hints, classLoader); + return hints; + } + + private Consumer include(String... patterns) { + return (hint) -> { + assertThat(hint.getIncludes()).map(ResourcePatternHint::getPattern).containsExactly(patterns); + assertThat(hint.getExcludes()).isEmpty(); + }; + } + + private static class TestClassLoader extends URLClassLoader { + + private final List availableResources; + + TestClassLoader(List availableResources) { + super(new URL[0], TestClassLoader.class.getClassLoader()); + this.availableResources = availableResources; + } + + @Override + public URL getResource(String name) { + return (this.availableResources.contains(name)) ? super.getResource("web/custom-resource.txt") : null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/web/custom-resource.txt b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/web/custom-resource.txt new file mode 100644 index 0000000000..e69de29bb2