Skip to content

Commit 53b9d60

Browse files
committed
Refactor ApplicationResourceLoader preferFileResolution support
Refactor `ApplicationResourceLoader` so that `preferFileResolution` is supported using a new `ResourceFilePathResolver` strategy interface. This update allows us to locate `ResourceFilePathResolver` implementations in more suitable packages that are closer to the code that they support. See gh-44535
1 parent 98b2b5f commit 53b9d60

File tree

9 files changed

+297
-97
lines changed

9 files changed

+297
-97
lines changed

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

Lines changed: 19 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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}, {@code FilteredReactiveWebContextResource} or
136-
* {@link ClassPathResource} when no resource prefix 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,28 +210,17 @@ 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-
215-
private static final String FILTERED_REACTIVE_WEB_CONTEXT_RESOURCE_CLASS_NAME = "org.springframework.boot.web.reactive.context.FilteredReactiveWebContextResource";
216-
217213
private final ResourceLoader resourceLoader;
218214

219215
private final List<ProtocolResolver> protocolResolvers;
220216

221-
private final boolean preferFileResolution;
222-
223-
private final Class<?> servletContextResourceClass;
224-
225-
private final Class<?> filteredReactiveWebContextResourceClass;
217+
private final List<ResourceFilePathResolver> filePathResolvers;
226218

227219
ProtocolResolvingResourceLoader(ResourceLoader resourceLoader, List<ProtocolResolver> protocolResolvers,
228-
boolean preferFileResolution) {
220+
List<ResourceFilePathResolver> filePathResolvers) {
229221
this.resourceLoader = resourceLoader;
230222
this.protocolResolvers = protocolResolvers;
231-
this.preferFileResolution = preferFileResolution;
232-
this.servletContextResourceClass = resolveServletContextResourceClass(resourceLoader);
233-
this.filteredReactiveWebContextResourceClass = resolveFilteredReactiveWebContextResourceClass(
234-
resourceLoader);
223+
this.filePathResolvers = filePathResolvers;
235224
}
236225

237226
@Override
@@ -250,47 +239,18 @@ public Resource getResource(String location) {
250239
}
251240
}
252241
Resource resource = this.resourceLoader.getResource(location);
253-
if (shouldUseFileResolution(location, resource)) {
254-
return new ApplicationResource(location);
255-
}
256-
return resource;
257-
}
258-
259-
private boolean shouldUseFileResolution(String location, Resource resource) {
260-
if (!this.preferFileResolution) {
261-
return false;
262-
}
263-
return isClassPathResourceByPath(location, resource) || isServletContextResource(resource)
264-
|| isFilteredReactiveWebContextResource(resource);
265-
}
266-
267-
private boolean isClassPathResourceByPath(String location, Resource resource) {
268-
return (resource instanceof ClassPathResource) && !location.startsWith(CLASSPATH_URL_PREFIX);
269-
}
270-
271-
private boolean isServletContextResource(Resource resource) {
272-
return (this.servletContextResourceClass != null) && this.servletContextResourceClass.isInstance(resource);
273-
}
274-
275-
private boolean isFilteredReactiveWebContextResource(Resource resource) {
276-
return (this.filteredReactiveWebContextResourceClass != null)
277-
&& this.filteredReactiveWebContextResourceClass.isInstance(resource);
242+
String fileSystemPath = getFileSystemPath(location, resource);
243+
return (fileSystemPath != null) ? new ApplicationResource(fileSystemPath) : resource;
278244
}
279245

280-
private static Class<?> resolveServletContextResourceClass(ResourceLoader resourceLoader) {
281-
return resolveClassName(SERVLET_CONTEXT_RESOURCE_CLASS_NAME, resourceLoader.getClass().getClassLoader());
282-
}
283-
284-
private static Class<?> resolveFilteredReactiveWebContextResourceClass(ResourceLoader resourceLoader) {
285-
return resolveClassName(FILTERED_REACTIVE_WEB_CONTEXT_RESOURCE_CLASS_NAME,
286-
resourceLoader.getClass().getClassLoader());
287-
}
288-
289-
private static Class<?> resolveClassName(String clazz, ClassLoader classLoader) {
290-
if (!ClassUtils.isPresent(clazz, classLoader)) {
291-
return null;
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+
}
292252
}
293-
return ClassUtils.resolveClassName(clazz, classLoader);
253+
return null;
294254
}
295255

296256
}
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+
}
Lines changed: 42 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,17 @@
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;
3130
import org.springframework.boot.testsupport.classpath.resources.WithResource;
32-
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
3331
import org.springframework.core.io.ByteArrayResource;
3432
import org.springframework.core.io.ClassPathResource;
3533
import org.springframework.core.io.DefaultResourceLoader;
3634
import org.springframework.core.io.FileSystemResource;
3735
import org.springframework.core.io.Resource;
3836
import org.springframework.core.io.ResourceLoader;
3937
import org.springframework.core.io.support.SpringFactoriesLoader;
40-
import org.springframework.mock.web.MockServletContext;
41-
import org.springframework.util.ClassUtils;
42-
import org.springframework.web.context.support.ServletContextResource;
43-
import org.springframework.web.context.support.ServletContextResourceLoader;
4438

4539
import static org.assertj.core.api.Assertions.assertThat;
4640
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -198,38 +192,6 @@ void getResourceWithPreferFileResolutionWhenExplicitClassPathPrefix() {
198192
assertThat(resource).isInstanceOf(ClassPathResource.class);
199193
}
200194

201-
@Test
202-
void getResourceWithPreferFileResolutionWhenPathWithServletContextResource() throws Exception {
203-
ServletContext servletContext = new MockServletContext();
204-
ServletContextResourceLoader servletContextResourceLoader = new ServletContextResourceLoader(servletContext);
205-
ResourceLoader loader = ApplicationResourceLoader.get(servletContextResourceLoader, true);
206-
Resource resource = loader.getResource("src/main/resources/a-file");
207-
assertThat(resource).isInstanceOf(FileSystemResource.class);
208-
assertThat(resource.getFile().getAbsoluteFile())
209-
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
210-
ResourceLoader regularLoader = ApplicationResourceLoader.get(servletContextResourceLoader, false);
211-
assertThat(regularLoader.getResource("src/main/resources/a-file")).isInstanceOf(ServletContextResource.class);
212-
}
213-
214-
@Test
215-
void getResourceWhenFilteredReactiveWebContextResourceWithPreferFileResolution() throws Exception {
216-
ResourceLoader resourceLoader = ApplicationResourceLoader
217-
.get(new AnnotationConfigReactiveWebApplicationContext(), true);
218-
Resource resource = resourceLoader.getResource("src/main/resources/a-file");
219-
assertThat(resource).isInstanceOf(FileSystemResource.class);
220-
assertThat(resource.getFile().getAbsoluteFile())
221-
.isEqualTo(new File("src/main/resources/a-file").getAbsoluteFile());
222-
}
223-
224-
@Test
225-
void getResourceWhenFilteredReactiveWebContextResource() {
226-
ResourceLoader resourceLoader = ApplicationResourceLoader
227-
.get(new AnnotationConfigReactiveWebApplicationContext(), false);
228-
Resource resource = resourceLoader.getResource("src/main/resources/a-file");
229-
assertThat(resource).isInstanceOf(ClassUtils.resolveClassName(
230-
"org.springframework.boot.web.reactive.context.FilteredReactiveWebContextResource", null));
231-
}
232-
233195
@Test
234196
void getClassLoaderReturnsDelegateClassLoader() {
235197
ClassLoader classLoader = new TestClassLoader(this::useTestProtocolResolversFactories);

0 commit comments

Comments
 (0)