Skip to content

Commit 76f6e30

Browse files
committed
Merge pull request #44535 from nosan
* pr/44535: Refactor ApplicationResourceLoader `preferFileResolution` support Improve ApplicationResourceLoader `preferFileResolution` support Closes gh-44535
2 parents 80632bd + 53b9d60 commit 76f6e30

9 files changed

+302
-55
lines changed

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

+24-38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,9 @@
1616

1717
package org.springframework.boot.io;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021

21-
import org.springframework.core.io.ClassPathResource;
2222
import org.springframework.core.io.ContextResource;
2323
import org.springframework.core.io.DefaultResourceLoader;
2424
import org.springframework.core.io.FileSystemResource;
@@ -27,7 +27,6 @@
2727
import org.springframework.core.io.ResourceLoader;
2828
import org.springframework.core.io.support.SpringFactoriesLoader;
2929
import org.springframework.util.Assert;
30-
import org.springframework.util.ClassUtils;
3130
import org.springframework.util.StringUtils;
3231

3332
/**
@@ -131,9 +130,8 @@ public static ResourceLoader get(ResourceLoader resourceLoader) {
131130
* {@code spring.factories}. The factories file will be resolved using the default
132131
* class loader at the time this call is made.
133132
* @param resourceLoader the delegate resource loader
134-
* @param preferFileResolution if file based resolution is preferred over
135-
* {@code ServletContextResource} or {@link ClassPathResource} when no resource prefix
136-
* is provided.
133+
* @param preferFileResolution if file based resolution is preferred when a suitable
134+
* {@link ResourceFilePathResolver} support the resource
137135
* @return a {@link ResourceLoader} instance
138136
* @since 3.4.1
139137
*/
@@ -161,8 +159,10 @@ private static ResourceLoader get(ResourceLoader resourceLoader, SpringFactories
161159
boolean preferFileResolution) {
162160
Assert.notNull(resourceLoader, "'resourceLoader' must not be null");
163161
Assert.notNull(springFactoriesLoader, "'springFactoriesLoader' must not be null");
164-
return new ProtocolResolvingResourceLoader(resourceLoader, springFactoriesLoader.load(ProtocolResolver.class),
165-
preferFileResolution);
162+
List<ProtocolResolver> protocolResolvers = springFactoriesLoader.load(ProtocolResolver.class);
163+
List<ResourceFilePathResolver> filePathResolvers = (preferFileResolution)
164+
? springFactoriesLoader.load(ResourceFilePathResolver.class) : Collections.emptyList();
165+
return new ProtocolResolvingResourceLoader(resourceLoader, protocolResolvers, filePathResolvers);
166166
}
167167

168168
/**
@@ -210,30 +210,22 @@ public String getPathWithinContext() {
210210
*/
211211
private static class ProtocolResolvingResourceLoader implements ResourceLoader {
212212

213-
private static final String SERVLET_CONTEXT_RESOURCE_CLASS_NAME = "org.springframework.web.context.support.ServletContextResource";
214-
215213
private final ResourceLoader resourceLoader;
216214

217215
private final List<ProtocolResolver> protocolResolvers;
218216

219-
private final boolean preferFileResolution;
220-
221-
private Class<?> servletContextResourceClass;
217+
private final List<ResourceFilePathResolver> filePathResolvers;
222218

223219
ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
224-
boolean preferFileResolution) {
220+
List<ResourceFilePathResolver> filePathResolvers) {
225221
this.resourceLoader = resourceLoader;
226222
this.protocolResolvers = protocolResolvers;
227-
this.preferFileResolution = preferFileResolution;
228-
this.servletContextResourceClass = resolveServletContextResourceClass(
229-
resourceLoader.getClass().getClassLoader());
223+
this.filePathResolvers = filePathResolvers;
230224
}
231225

232-
private static Class<?> resolveServletContextResourceClass(ClassLoader classLoader) {
233-
if (!ClassUtils.isPresent(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader)) {
234-
return null;
235-
}
236-
return ClassUtils.resolveClassName(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, classLoader);
226+
@Override
227+
public ClassLoader getClassLoader() {
228+
return this.resourceLoader.getClassLoader();
237229
}
238230

239231
@Override
@@ -247,24 +239,18 @@ public Resource getResource(String location) {
247239
}
248240
}
249241
Resource resource = this.resourceLoader.getResource(location);
250-
if (this.preferFileResolution
251-
&& (isClassPathResourceByPath(location, resource) || isServletResource(resource))) {
252-
resource = new ApplicationResource(location);
253-
}
254-
return resource;
242+
String fileSystemPath = getFileSystemPath(location, resource);
243+
return (fileSystemPath != null) ? new ApplicationResource(fileSystemPath) : resource;
255244
}
256245

257-
private boolean isClassPathResourceByPath(String location, Resource resource) {
258-
return (resource instanceof ClassPathResource) && !location.startsWith(CLASSPATH_URL_PREFIX);
259-
}
260-
261-
private boolean isServletResource(Resource resource) {
262-
return this.servletContextResourceClass != null && this.servletContextResourceClass.isInstance(resource);
263-
}
264-
265-
@Override
266-
public ClassLoader getClassLoader() {
267-
return this.resourceLoader.getClassLoader();
246+
private String getFileSystemPath(String location, Resource resource) {
247+
for (ResourceFilePathResolver filePathResolver : this.filePathResolvers) {
248+
String filePath = filePathResolver.resolveFilePath(location, resource);
249+
if (filePath != null) {
250+
return filePath;
251+
}
252+
}
253+
return null;
268254
}
269255

270256
}
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
@@ -105,3 +105,9 @@ org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitiali
105105
# Resource Locator Protocol Resolvers
106106
org.springframework.core.io.ProtocolResolver=\
107107
org.springframework.boot.io.Base64ProtocolResolver
108+
109+
# Resource File Path Resolvers
110+
org.springframework.boot.io.ResourceFilePathResolver=\
111+
org.springframework.boot.io.ClassPathResourceFilePathResolver,\
112+
org.springframework.boot.web.context.ServletContextResourceFilePathResolver,\
113+
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
@@ -24,7 +24,6 @@
2424
import java.util.Enumeration;
2525
import java.util.function.UnaryOperator;
2626

27-
import jakarta.servlet.ServletContext;
2827
import org.junit.jupiter.api.Test;
2928

3029
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
@@ -36,9 +35,6 @@
3635
import org.springframework.core.io.Resource;
3736
import org.springframework.core.io.ResourceLoader;
3837
import org.springframework.core.io.support.SpringFactoriesLoader;
39-
import org.springframework.mock.web.MockServletContext;
40-
import org.springframework.web.context.support.ServletContextResource;
41-
import org.springframework.web.context.support.ServletContextResourceLoader;
4238

4339
import static org.assertj.core.api.Assertions.assertThat;
4440
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -196,19 +192,6 @@ void getResourceWithPreferFileResolutionWhenExplicitClassPathPrefix() {
196192
assertThat(resource).isInstanceOf(ClassPathResource.class);
197193
}
198194

199-
@Test
200-
void getResourceWithPreferFileResolutionWhenPathWithServletContextResource() throws Exception {
201-
ServletContext servletContext = new MockServletContext();
202-
ServletContextResourceLoader servletContextResourceLoader = new ServletContextResourceLoader(servletContext);
203-
ResourceLoader loader = ApplicationResourceLoader.get(servletContextResourceLoader, true);
204-
Resource resource = loader.getResource("src/main/resources/a-file");
205-
assertThat(resource).isInstanceOf(FileSystemResource.class);
206-
assertThat(resource.getFile().getAbsoluteFile())
207-
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
208-
ResourceLoader regularLoader = ApplicationResourceLoader.get(servletContextResourceLoader, false);
209-
assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ServletContextResource.class);
210-
}
211-
212195
@Test
213196
void getClassLoaderReturnsDelegateClassLoader() {
214197
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)