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 fde0b85c6b6..40a778ace1d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -16,9 +16,9 @@ package org.springframework.boot.io; +import java.util.Collections; 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; @@ -27,7 +27,6 @@ import org.springframework.core.io.Resource; 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; /** @@ -131,9 +130,8 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { * {@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. + * @param preferFileResolution if file based resolution is preferred when a suitable + * {@link ResourceFilePathResolver} support the resource * @return a {@link ResourceLoader} instance * @since 3.4.1 */ @@ -161,8 +159,10 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { 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), - preferFileResolution); + List protocolResolvers = springFactoriesLoader.load(ProtocolResolver.class); + List filePathResolvers = (preferFileResolution) + ? springFactoriesLoader.load(ResourceFilePathResolver.class) : Collections.emptyList(); + return new ProtocolResolvingResourceLoader(resourceLoader, protocolResolvers, filePathResolvers); } /** @@ -210,30 +210,22 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { */ 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; - private final boolean preferFileResolution; - - private Class servletContextResourceClass; + private final List filePathResolvers; ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List protocolResolvers, - boolean preferFileResolution) { + List filePathResolvers) { this.resourceLoader = resourceLoader; this.protocolResolvers = protocolResolvers; - this.preferFileResolution = preferFileResolution; - this.servletContextResourceClass = resolveServletContextResourceClass( - resourceLoader.getClass().getClassLoader()); + this.filePathResolvers = filePathResolvers; } - 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 + public ClassLoader getClassLoader() { + return this.resourceLoader.getClassLoader(); } @Override @@ -247,24 +239,18 @@ public class ApplicationResourceLoader extends DefaultResourceLoader { } } Resource resource = this.resourceLoader.getResource(location); - if (this.preferFileResolution - && (isClassPathResourceByPath(location, resource) || isServletResource(resource))) { - resource = new ApplicationResource(location); + String fileSystemPath = getFileSystemPath(location, resource); + return (fileSystemPath != null) ? new ApplicationResource(fileSystemPath) : resource; + } + + private String getFileSystemPath(String location, Resource resource) { + for (ResourceFilePathResolver filePathResolver : this.filePathResolvers) { + String filePath = filePathResolver.resolveFilePath(location, resource); + if (filePath != null) { + return filePath; + } } - 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 - public ClassLoader getClassLoader() { - return this.resourceLoader.getClassLoader(); + return null; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ClassPathResourceFilePathResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ClassPathResourceFilePathResolver.java new file mode 100644 index 00000000000..aca69aaccb9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ClassPathResourceFilePathResolver.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2025 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.io; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * {@link ResourceFilePathResolver} for {@link ClassPathResource}. + * + * @author Phillip Webb + */ +class ClassPathResourceFilePathResolver implements ResourceFilePathResolver { + + @Override + public String resolveFilePath(String location, Resource resource) { + return (resource instanceof ClassPathResource && !isClassPathUrl(location)) ? location : null; + } + + private boolean isClassPathUrl(String location) { + return location.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ResourceFilePathResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ResourceFilePathResolver.java new file mode 100644 index 00000000000..b3c9fe8b6ad --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ResourceFilePathResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2025 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.io; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +/** + * Strategy interface registered in {@code spring.factories} and used by + * {@link ApplicationResourceLoader} to determine the file path of loaded resource when it + * can also be represented as a {@link FileSystemResource}. + * + * @author Phillip Webb + * @since 3.4.5 + */ +public interface ResourceFilePathResolver { + + /** + * Return the {@code path} of the given resource if it can also be represented as a + * {@link FileSystemResource}. + * @param location the location used to create the resource + * @param resource the resource to check + * @return the file path of the resource or {@code null} if the it is not possible to + * represent the resource as a {@link FileSystemResource}. + */ + String resolveFilePath(String location, Resource resource); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServletContextResourceFilePathResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServletContextResourceFilePathResolver.java new file mode 100644 index 00000000000..9c0e571d545 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServletContextResourceFilePathResolver.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2025 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.web.context; + +import org.springframework.boot.io.ResourceFilePathResolver; +import org.springframework.core.io.Resource; +import org.springframework.util.ClassUtils; +import org.springframework.web.context.support.ServletContextResource; + +/** + * {@link ResourceFilePathResolver} for {@link ServletContextResource}. + * + * @author Phillip Webb + */ +class ServletContextResourceFilePathResolver implements ResourceFilePathResolver { + + private static final String RESOURCE_CLASS_NAME = "org.springframework.web.context.support.ServletContextResource"; + + private final Class resourceClass; + + ServletContextResourceFilePathResolver() { + ClassLoader classLoader = getClass().getClassLoader(); + this.resourceClass = ClassUtils.isPresent(RESOURCE_CLASS_NAME, classLoader) + ? ClassUtils.resolveClassName(RESOURCE_CLASS_NAME, classLoader) : null; + } + + @Override + public String resolveFilePath(String location, Resource resource) { + return (this.resourceClass != null && this.resourceClass.isInstance(resource)) ? location : null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/FilteredReactiveWebContextResourceFilePathResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/FilteredReactiveWebContextResourceFilePathResolver.java new file mode 100644 index 00000000000..9876bc0aad9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/FilteredReactiveWebContextResourceFilePathResolver.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2025 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.web.reactive.context; + +import org.springframework.boot.io.ResourceFilePathResolver; +import org.springframework.core.io.Resource; + +/** + * {@link ResourceFilePathResolver} for {@link FilteredReactiveWebContextResource}. + * + * @author Dmytro Nosan + */ +class FilteredReactiveWebContextResourceFilePathResolver implements ResourceFilePathResolver { + + @Override + public String resolveFilePath(String location, Resource resource) { + return (resource instanceof FilteredReactiveWebContextResource) ? location : null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index b1feda37ed3..6d99bfabd27 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -105,3 +105,9 @@ org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitiali # Resource Locator Protocol Resolvers org.springframework.core.io.ProtocolResolver=\ org.springframework.boot.io.Base64ProtocolResolver + +# Resource File Path Resolvers +org.springframework.boot.io.ResourceFilePathResolver=\ +org.springframework.boot.io.ClassPathResourceFilePathResolver,\ +org.springframework.boot.web.context.ServletContextResourceFilePathResolver,\ +org.springframework.boot.web.reactive.context.FilteredReactiveWebContextResourceFilePathResolver 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 6da80ecfa92..deacda3a9cb 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,7 +24,6 @@ import java.util.Base64; import java.util.Enumeration; import java.util.function.UnaryOperator; -import jakarta.servlet.ServletContext; import org.junit.jupiter.api.Test; import org.springframework.boot.testsupport.classpath.resources.ResourcePath; @@ -36,9 +35,6 @@ 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; @@ -196,19 +192,6 @@ class ApplicationResourceLoaderTests { 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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/context/ServletContextResourceFilePathResolverIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/context/ServletContextResourceFilePathResolverIntegrationTests.java new file mode 100644 index 00000000000..e4be8322744 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/context/ServletContextResourceFilePathResolverIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2025 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.web.context; + +import java.io.File; + +import jakarta.servlet.ServletContext; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +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; + +/** + * Integration tests for {@link ServletContextResourceFilePathResolver}. + * + * @author Phillip Webb + */ +class ServletContextResourceFilePathResolverIntegrationTests { + + @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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/FilteredReactiveWebContextResourceFilePathResolverIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/FilteredReactiveWebContextResourceFilePathResolverIntegrationTests.java new file mode 100644 index 00000000000..1a70d388467 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/FilteredReactiveWebContextResourceFilePathResolverIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2025 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.web.reactive.context; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link FilteredReactiveWebContextResourceFilePathResolver}. + * + * @author Dmytro Nosan + */ +class FilteredReactiveWebContextResourceFilePathResolverIntegrationTests { + + @Test + void getResourceWhenFilteredReactiveWebContextResourceWithPreferFileResolution() throws Exception { + ResourceLoader resourceLoader = ApplicationResourceLoader + .get(new AnnotationConfigReactiveWebApplicationContext(), true); + Resource resource = resourceLoader.getResource("src/main/resources/a-file"); + assertThat(resource).isInstanceOf(FileSystemResource.class); + assertThat(resource.getFile().getAbsoluteFile()) + .isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile()); + } + + @Test + void getResourceWhenFilteredReactiveWebContextResource() { + ResourceLoader resourceLoader = ApplicationResourceLoader + .get(new AnnotationConfigReactiveWebApplicationContext(), false); + Resource resource = resourceLoader.getResource("src/main/resources/a-file"); + assertThat(resource).isInstanceOf(ClassUtils.resolveClassName( + "org.springframework.boot.web.reactive.context.FilteredReactiveWebContextResource", null)); + } + +}