Skip to content

Commit 7d8f881

Browse files
committed
Merge branch '3.4.x'
Closes gh-45263
2 parents c6c2ce2 + 76f6e30 commit 7d8f881

9 files changed

+301
-54
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/io/ApplicationResourceLoader.java

+23-37
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import java.io.IOException;
2222
import java.io.UncheckedIOException;
2323
import java.nio.file.Path;
24+
import java.util.Collections;
2425
import java.util.List;
2526

26-
import org.springframework.core.io.ClassPathResource;
2727
import org.springframework.core.io.ContextResource;
2828
import org.springframework.core.io.DefaultResourceLoader;
2929
import org.springframework.core.io.FileSystemResource;
@@ -32,7 +32,6 @@
3232
import org.springframework.core.io.ResourceLoader;
3333
import org.springframework.core.io.support.SpringFactoriesLoader;
3434
import org.springframework.util.Assert;
35-
import org.springframework.util.ClassUtils;
3635
import org.springframework.util.StringUtils;
3736

3837
/**
@@ -153,9 +152,8 @@ public static ResourceLoader get(ResourceLoader resourceLoader) {
153152
* {@code spring.factories}. The factories file will be resolved using the default
154153
* class loader at the time this call is made.
155154
* @param resourceLoader the delegate resource loader
156-
* @param preferFileResolution if file based resolution is preferred over
157-
* {@code ServletContextResource} or {@link ClassPathResource} when no resource prefix
158-
* is provided.
155+
* @param preferFileResolution if file based resolution is preferred when a suitable
156+
* {@link ResourceFilePathResolver} support the resource
159157
* @return a {@link ResourceLoader} instance
160158
* @since 3.4.1
161159
*/
@@ -183,8 +181,10 @@ private static ResourceLoader get(ResourceLoader resourceLoader, SpringFactories
183181
boolean preferFileResolution) {
184182
Assert.notNull(resourceLoader, "'resourceLoader' must not be null");
185183
Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null");
186-
return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class),
187-
preferFileResolution);
184+
List<ProtocolResolver> protocolResolvers = springFactoriesLoader.load(ProtocolResolver.class);
185+
List<ResourceFilePathResolver> filePathResolvers = (preferFileResolution)
186+
? springFactoriesLoader.load(ResourceFilePathResolver.class) : Collections.emptyList();
187+
return new ProtocolResolvingResourceLoader(resourceLoader, protocolResolvers, filePathResolvers);
188188
}
189189

190190
/**
@@ -268,30 +268,22 @@ public String getPathWithinContext() {
268268
*/
269269
private static class ProtocolResolvingResourceLoader implements ResourceLoader {
270270

271-
private static final String SERVLET_CONTEXT_RESOURCE_CLASS_NAME = "org.springframework.web.context.support.ServletContextResource";
272-
273271
private final ResourceLoader resourceLoader;
274272

275273
private final List<ProtocolResolver> protocolResolvers;
276274

277-
private final boolean preferFileResolution;
278-
279-
private final Class<?> servletContextResourceClass;
275+
private final List<ResourceFilePathResolver> filePathResolvers;
280276

281277
ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
282-
boolean preferFileResolution) {
278+
List<ResourceFilePathResolver> filePathResolvers) {
283279
this.resourceLoader = resourceLoader;
284280
this.protocolResolvers = protocolResolvers;
285-
this.preferFileResolution = preferFileResolution;
286-
this.servletContextResourceClass = resolveServletContextResourceClass(
287-
resourceLoader.getClass().getClassLoader());
281+
this.filePathResolvers = filePathResolvers;
288282
}
289283

290-
private static Class<?> resolveServletContextResourceClass(ClassLoader classLoader) {
291-
if (!ClassUtils.isPresent(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader)) {
292-
return null;
293-
}
294-
return ClassUtils.resolveClassName(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader);
284+
@Override
285+
public ClassLoader getClassLoader() {
286+
return this.resourceLoader.getClassLoader();
295287
}
296288

297289
@Override
@@ -305,24 +297,18 @@ public Resource getResource(String location) {
305297
}
306298
}
307299
Resource resource = this.resourceLoader.getResource(location);
308-
if (this.preferFileResolution
309-
&& (isClassPathResourceByPath(location, resource) || isServletResource(resource))) {
310-
resource = new ApplicationResource(location);
311-
}
312-
return resource;
300+
String fileSystemPath = getFileSystemPath(location, resource);
301+
return (fileSystemPath != null) ? new ApplicationResource(fileSystemPath) : resource;
313302
}
314303

315-
private boolean isClassPathResourceByPath(String location, Resource resource) {
316-
return (resource instanceof ClassPathResource) && !location.startsWith(CLASSPATH_URL_PREFIX);
317-
}
318-
319-
private boolean isServletResource(Resource resource) {
320-
return this.servletContextResourceClass != null && this.servletContextResourceClass.isInstance(resource);
321-
}
322-
323-
@Override
324-
public ClassLoader getClassLoader() {
325-
return this.resourceLoader.getClassLoader();
304+
private String getFileSystemPath(String location, Resource resource) {
305+
for (ResourceFilePathResolver filePathResolver : this.filePathResolvers) {
306+
String filePath = filePathResolver.resolveFilePath(location, resource);
307+
if (filePath != null) {
308+
return filePath;
309+
}
310+
}
311+
return null;
326312
}
327313

328314
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.io;
18+
19+
import org.springframework.core.io.ClassPathResource;
20+
import org.springframework.core.io.Resource;
21+
import org.springframework.core.io.ResourceLoader;
22+
23+
/**
24+
* {@link ResourceFilePathResolver} for {@link ClassPathResource}.
25+
*
26+
* @author Phillip Webb
27+
*/
28+
class ClassPathResourceFilePathResolver implements ResourceFilePathResolver {
29+
30+
@Override
31+
public String resolveFilePath(String location, Resource resource) {
32+
return (resource instanceof ClassPathResource && !isClassPathUrl(location)) ? location : null;
33+
}
34+
35+
private boolean isClassPathUrl(String location) {
36+
return location.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.io;
18+
19+
import org.springframework.core.io.FileSystemResource;
20+
import org.springframework.core.io.Resource;
21+
22+
/**
23+
* Strategy interface registered in {@code spring.factories} and used by
24+
* {@link ApplicationResourceLoader} to determine the file path of loaded resource when it
25+
* can also be represented as a {@link FileSystemResource}.
26+
*
27+
* @author Phillip Webb
28+
* @since 3.4.5
29+
*/
30+
public interface ResourceFilePathResolver {
31+
32+
/**
33+
* Return the {@code path} of the given resource if it can also be represented as a
34+
* {@link FileSystemResource}.
35+
* @param location the location used to create the resource
36+
* @param resource the resource to check
37+
* @return the file path of the resource or {@code null} if the it is not possible to
38+
* represent the resource as a {@link FileSystemResource}.
39+
*/
40+
String resolveFilePath(String location, Resource resource);
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.context;
18+
19+
import org.springframework.boot.io.ResourceFilePathResolver;
20+
import org.springframework.core.io.Resource;
21+
import org.springframework.util.ClassUtils;
22+
import org.springframework.web.context.support.ServletContextResource;
23+
24+
/**
25+
* {@link ResourceFilePathResolver} for {@link ServletContextResource}.
26+
*
27+
* @author Phillip Webb
28+
*/
29+
class ServletContextResourceFilePathResolver implements ResourceFilePathResolver {
30+
31+
private static final String RESOURCE_CLASS_NAME = "org.springframework.web.context.support.ServletContextResource";
32+
33+
private final Class<?> resourceClass;
34+
35+
ServletContextResourceFilePathResolver() {
36+
ClassLoader classLoader = getClass().getClassLoader();
37+
this.resourceClass = ClassUtils.isPresent(RESOURCE_CLASS_NAME, classLoader)
38+
? ClassUtils.resolveClassName(RESOURCE_CLASS_NAME, classLoader) : null;
39+
}
40+
41+
@Override
42+
public String resolveFilePath(String location, Resource resource) {
43+
return (this.resourceClass != null && this.resourceClass.isInstance(resource)) ? location : null;
44+
}
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.reactive.context;
18+
19+
import org.springframework.boot.io.ResourceFilePathResolver;
20+
import org.springframework.core.io.Resource;
21+
22+
/**
23+
* {@link ResourceFilePathResolver} for {@link FilteredReactiveWebContextResource}.
24+
*
25+
* @author Dmytro Nosan
26+
*/
27+
class FilteredReactiveWebContextResourceFilePathResolver implements ResourceFilePathResolver {
28+
29+
@Override
30+
public String resolveFilePath(String location, Resource resource) {
31+
return (resource instanceof FilteredReactiveWebContextResource) ? location : null;
32+
}
33+
34+
}

spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories

+6
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,9 @@ org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitiali
107107
# Resource Locator Protocol Resolvers
108108
org.springframework.core.io.ProtocolResolver=\
109109
org.springframework.boot.io.Base64ProtocolResolver
110+
111+
# Resource File Path Resolvers
112+
org.springframework.boot.io.ResourceFilePathResolver=\
113+
org.springframework.boot.io.ClassPathResourceFilePathResolver,\
114+
org.springframework.boot.web.context.ServletContextResourceFilePathResolver,\
115+
org.springframework.boot.web.reactive.context.FilteredReactiveWebContextResourceFilePathResolver

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/io/ApplicationResourceLoaderTests.java

-17
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import java.util.Enumeration;
2626
import java.util.function.UnaryOperator;
2727

28-
import jakarta.servlet.ServletContext;
2928
import org.junit.jupiter.api.Test;
3029

3130
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
@@ -37,9 +36,6 @@
3736
import org.springframework.core.io.Resource;
3837
import org.springframework.core.io.ResourceLoader;
3938
import org.springframework.core.io.support.SpringFactoriesLoader;
40-
import org.springframework.mock.web.MockServletContext;
41-
import org.springframework.web.context.support.ServletContextResource;
42-
import org.springframework.web.context.support.ServletContextResourceLoader;
4339

4440
import static org.assertj.core.api.Assertions.assertThat;
4541
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -315,19 +311,6 @@ void getResourceWithPreferFileResolutionWhenExplicitClassPathPrefix() {
315311
assertThat(resource).isInstanceOf(ClassPathResource.class);
316312
}
317313

318-
@Test
319-
void getResourceWithPreferFileResolutionWhenPathWithServletContextResource() throws Exception {
320-
ServletContext servletContext = new MockServletContext();
321-
ServletContextResourceLoader servletContextResourceLoader = new ServletContextResourceLoader(servletContext);
322-
ResourceLoader loader = ApplicationResourceLoader.get(servletContextResourceLoader, true);
323-
Resource resource = loader.getResource("src/main/resources/a-file");
324-
assertThat(resource).isInstanceOf(FileSystemResource.class);
325-
assertThat(resource.getFile().getAbsoluteFile())
326-
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
327-
ResourceLoader regularLoader = ApplicationResourceLoader.get(servletContextResourceLoader, false);
328-
assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ServletContextResource.class);
329-
}
330-
331314
@Test
332315
void getClassLoaderReturnsDelegateClassLoader() {
333316
ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.context;
18+
19+
import java.io.File;
20+
21+
import jakarta.servlet.ServletContext;
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.boot.io.ApplicationResourceLoader;
25+
import org.springframework.core.io.FileSystemResource;
26+
import org.springframework.core.io.Resource;
27+
import org.springframework.core.io.ResourceLoader;
28+
import org.springframework.mock.web.MockServletContext;
29+
import org.springframework.web.context.support.ServletContextResource;
30+
import org.springframework.web.context.support.ServletContextResourceLoader;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* Integration tests for {@link ServletContextResourceFilePathResolver}.
36+
*
37+
* @author Phillip Webb
38+
*/
39+
class ServletContextResourceFilePathResolverIntegrationTests {
40+
41+
@Test
42+
void getResourceWithPreferFileResolutionWhenPathWithServletContextResource() throws Exception {
43+
ServletContext servletContext = new MockServletContext();
44+
ServletContextResourceLoader servletContextResourceLoader = new ServletContextResourceLoader(servletContext);
45+
ResourceLoader loader = ApplicationResourceLoader.get(servletContextResourceLoader, true);
46+
Resource resource = loader.getResource("src/main/resources/a-file");
47+
assertThat(resource).isInstanceOf(FileSystemResource.class);
48+
assertThat(resource.getFile().getAbsoluteFile())
49+
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
50+
ResourceLoader regularLoader = ApplicationResourceLoader.get(servletContextResourceLoader, false);
51+
assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ServletContextResource.class);
52+
}
53+
54+
}

0 commit comments

Comments
 (0)