diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java index 46af4615a0e8..c3e8bf469774 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java @@ -43,7 +43,7 @@ public class SslAutoConfiguration { private final SslProperties sslProperties; SslAutoConfiguration(ResourceLoader resourceLoader, SslProperties sslProperties) { - this.resourceLoader = ApplicationResourceLoader.get(resourceLoader); + this.resourceLoader = ApplicationResourceLoader.get(resourceLoader, true); this.sslProperties = sslProperties; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java index 87fd89f85477..c810f298abb9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java @@ -119,6 +119,24 @@ void sslBundlesCreatedWithCustomSslBundle() { }); } + @Test + void sslBundleWithoutClassPathPrefix() { + List propertyValues = new ArrayList<>(); + String location = "src/test/resources/org/springframework/boot/autoconfigure/ssl/"; + propertyValues.add("spring.ssl.bundle.pem.test.key.alias=alias1"); + propertyValues.add("spring.ssl.bundle.pem.test.key.password=secret1"); + propertyValues.add("spring.ssl.bundle.pem.test.keystore.certificate=" + location + "rsa-cert.pem"); + propertyValues.add("spring.ssl.bundle.pem.test.keystore.keystore.private-key=" + location + "rsa-key.pem"); + propertyValues.add("spring.ssl.bundle.pem.test.truststore.certificate=" + location + "rsa-cert.pem"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasSingleBean(SslBundles.class); + SslBundles bundles = context.getBean(SslBundles.class); + SslBundle bundle = bundles.getBundle("test"); + assertThat(bundle.getStores().getKeyStore().getCertificate("alias1")).isNotNull(); + assertThat(bundle.getStores().getTrustStore().getCertificate("ssl")).isNotNull(); + }); + } + @Configuration @EnableConfigurationProperties(CustomSslProperties.class) public static class CustomSslBundleConfiguration { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java index 819b082a9923..de160fdaa3ff 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java @@ -18,6 +18,7 @@ import java.util.List; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ContextResource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.FileSystemResource; @@ -26,6 +27,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -120,8 +122,25 @@ public static ResourceLoader get(ClassLoader classLoader, SpringFactoriesLoader * @since 3.4.0 */ public static ResourceLoader get(ResourceLoader resourceLoader) { + return get(resourceLoader, false); + } + + /** + * Return a {@link ResourceLoader} delegating to the given resource loader and + * supporting additional {@link ProtocolResolver ProtocolResolvers} registered in + * {@code spring.factories}. The factories file will be resolved using the default + * class loader at the time this call is made. + * @param resourceLoader the delegate resource loader + * @param preferFileResolution if file based resolution is preferred over + * {@code ServletContextResource} or {@link ClassPathResource} when no resource prefix + * is provided. + * @return a {@link ResourceLoader} instance + * @since 3.4.1 + */ + public static ResourceLoader get(ResourceLoader resourceLoader, boolean preferFileResolution) { Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); - return get(resourceLoader, SpringFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader())); + return get(resourceLoader, SpringFactoriesLoader.forDefaultResourceLocation(resourceLoader.getClassLoader()), + preferFileResolution); } /** @@ -135,9 +154,15 @@ public static ResourceLoader get(ResourceLoader resourceLoader) { * @since 3.4.0 */ public static ResourceLoader get(ResourceLoader resourceLoader, SpringFactoriesLoader springFactoriesLoader) { + return get(resourceLoader, springFactoriesLoader, false); + } + + private static ResourceLoader get(ResourceLoader resourceLoader, SpringFactoriesLoader springFactoriesLoader, + boolean preferFileResolution) { Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null"); - return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class)); + return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class), + preferFileResolution); } /** @@ -185,13 +210,30 @@ public String getPathWithinContext() { */ private static class ProtocolResolvingResourceLoader implements ResourceLoader { + private static final String SERVLET_CONTEXT_RESOURCE_CLASS_NAME = "org.springframework.web.context.support.ServletContextResource"; + private final ResourceLoader resourceLoader; private final List protocolResolvers; - ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List protocolResolvers) { + private final boolean preferFileResolution; + + private Class servletContextResourceClass; + + ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List protocolResolvers, + boolean preferFileResolution) { this.resourceLoader = resourceLoader; this.protocolResolvers = protocolResolvers; + this.preferFileResolution = preferFileResolution; + this.servletContextResourceClass = resolveServletContextResourceClass( + resourceLoader.getClass().getClassLoader()); + } + + private static Class resolveServletContextResourceClass(ClassLoader classLoader) { + if (!ClassUtils.isPresent(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader)) { + return null; + } + return ClassUtils.resolveClassName(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader); } @Override @@ -204,7 +246,20 @@ public Resource getResource(String location) { } } } - return this.resourceLoader.getResource(location); + Resource resource = this.resourceLoader.getResource(location); + if (this.preferFileResolution + && (isClassPathResourceByPath(location, resource) || isServletResource(resource))) { + resource = new ApplicationResource(location); + } + return resource; + } + + private boolean isClassPathResourceByPath(String location, Resource resource) { + return (resource instanceof ClassPathResource) && !location.startsWith(CLASSPATH_URL_PREFIX); + } + + private boolean isServletResource(Resource resource) { + return this.servletContextResourceClass != null && this.servletContextResourceClass.isInstance(resource); } @Override diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java index 0384f7cfcc14..124147329512 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java @@ -24,13 +24,19 @@ import java.util.Enumeration; import java.util.function.UnaryOperator; +import jakarta.servlet.ServletContext; import org.junit.jupiter.api.Test; import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.support.ServletContextResource; +import org.springframework.web.context.support.ServletContextResourceLoader; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -152,6 +158,48 @@ void getResourceWhenPathIsNull() { .withMessage("Location must not be null"); } + @Test + void getResourceWithPreferFileResolutionWhenFullPathWithClassPathResource() throws Exception { + File file = new File("src/main/resources/a-file"); + ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(), true); + Resource resource = loader.getResource(file.getAbsolutePath()); + assertThat(resource).isInstanceOf(FileSystemResource.class); + assertThat(resource.getFile().getAbsoluteFile()).isEqualTo(file.getAbsoluteFile()); + ResourceLoader regularLoader = ApplicationResourceLoader.get(new DefaultResourceLoader(), false); + assertThat(regularLoader.getResource(file.getAbsolutePath())).isInstanceOf(ClassPathResource.class); + } + + @Test + void getResourceWithPreferFileResolutionWhenRelativePathWithClassPathResource() throws Exception { + ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(), true); + Resource resource = loader.getResource("src/main/resources/a-file"); + assertThat(resource).isInstanceOf(FileSystemResource.class); + assertThat(resource.getFile().getAbsoluteFile()) + .isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile()); + ResourceLoader regularLoader = ApplicationResourceLoader.get(new DefaultResourceLoader(), false); + assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ClassPathResource.class); + } + + @Test + void getResourceWithPreferFileResolutionWhenExplicitClassPathPrefix() { + ResourceLoader loader = ApplicationResourceLoader.get(new DefaultResourceLoader(), true); + Resource resource = loader.getResource("classpath:a-file"); + assertThat(resource).isInstanceOf(ClassPathResource.class); + } + + @Test + void getResourceWithPreferFileResolutionWhenPathWithServletContextResource() throws Exception { + ServletContext servletContext = new MockServletContext(); + ServletContextResourceLoader servletContextResourceLoader = new ServletContextResourceLoader(servletContext); + ResourceLoader loader = ApplicationResourceLoader.get(servletContextResourceLoader, true); + Resource resource = loader.getResource("src/main/resources/a-file"); + assertThat(resource).isInstanceOf(FileSystemResource.class); + assertThat(resource.getFile().getAbsoluteFile()) + .isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile()); + ResourceLoader regularLoader = ApplicationResourceLoader.get(servletContextResourceLoader, false); + assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ServletContextResource.class); + } + @Test void getClassLoaderReturnsDelegateClassLoader() { ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);