Merge branch '3.4.x'

Closes gh-45263
This commit is contained in:
Phillip Webb 2025-04-22 22:53:42 -07:00
commit 7d8f881c15
9 changed files with 302 additions and 55 deletions

View File

@ -21,9 +21,9 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
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;
@ -32,7 +32,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;
/**
@ -153,9 +152,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
*/
@ -183,8 +181,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<ProtocolResolver> protocolResolvers = springFactoriesLoader.load(ProtocolResolver.class);
List<ResourceFilePathResolver> filePathResolvers = (preferFileResolution)
? springFactoriesLoader.load(ResourceFilePathResolver.class) : Collections.emptyList();
return new ProtocolResolvingResourceLoader(resourceLoader, protocolResolvers, filePathResolvers);
}
/**
@ -268,30 +268,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<ProtocolResolver> protocolResolvers;
private final boolean preferFileResolution;
private final Class<?> servletContextResourceClass;
private final List<ResourceFilePathResolver> filePathResolvers;
ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
boolean preferFileResolution) {
List<ResourceFilePathResolver> 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
@ -305,24 +297,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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -107,3 +107,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

View File

@ -25,7 +25,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;
@ -37,9 +36,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;
@ -315,19 +311,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);

View File

@ -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);
}
}

View File

@ -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));
}
}