Skip to content

Commit 9f7c8ae

Browse files
committed
Fix ResourceUrlProvider handler auto-detection
Prior to this commit, `ResourceUrlProvider` would listen and consider all `ContextRefreshedEvent` and use the given context to detect `SimpleUrlHandlerMapping`. This could lead to situations where a `ResourceUrlProvider` uses another application context than its own (in a parent/child context setup) and detect the wrong set of handlers. Because `ResourceUrlProvider` locks itself once the auto-detection is done, we need to ensure that it considers only events sent by its application context. Fixes gh-26562
1 parent 7926560 commit 9f7c8ae

File tree

4 files changed

+109
-24
lines changed

4 files changed

+109
-24
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceUrlProvider.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -26,12 +26,15 @@
2626
import org.apache.commons.logging.LogFactory;
2727
import reactor.core.publisher.Mono;
2828

29+
import org.springframework.beans.BeansException;
2930
import org.springframework.context.ApplicationContext;
31+
import org.springframework.context.ApplicationContextAware;
3032
import org.springframework.context.ApplicationListener;
3133
import org.springframework.context.event.ContextRefreshedEvent;
3234
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
3335
import org.springframework.http.server.PathContainer;
3436
import org.springframework.http.server.reactive.ServerHttpRequest;
37+
import org.springframework.lang.Nullable;
3538
import org.springframework.util.StringUtils;
3639
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
3740
import org.springframework.web.server.ServerWebExchange;
@@ -47,15 +50,23 @@
4750
* {@code ResourceHttpRequestHandler}s to make its decisions.
4851
*
4952
* @author Rossen Stoyanchev
53+
* @author Brian Clozel
5054
* @since 5.0
5155
*/
52-
public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> {
56+
public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware {
5357

5458
private static final Log logger = LogFactory.getLog(ResourceUrlProvider.class);
5559

5660

5761
private final Map<PathPattern, ResourceWebHandler> handlerMap = new LinkedHashMap<>();
5862

63+
@Nullable
64+
private ApplicationContext applicationContext;
65+
66+
@Override
67+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
68+
this.applicationContext = applicationContext;
69+
}
5970

6071
/**
6172
* Return a read-only view of the resource handler mappings either manually
@@ -83,8 +94,8 @@ public void registerHandlers(Map<String, ResourceWebHandler> handlerMap) {
8394

8495
@Override
8596
public void onApplicationEvent(ContextRefreshedEvent event) {
86-
if (this.handlerMap.isEmpty()) {
87-
detectResourceHandlers(event.getApplicationContext());
97+
if (this.applicationContext == event.getApplicationContext() && this.handlerMap.isEmpty()) {
98+
detectResourceHandlers(this.applicationContext);
8899
}
89100
}
90101

spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceUrlProviderTests.java

+38-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -44,6 +44,7 @@
4444
* Unit tests for {@link ResourceUrlProvider}.
4545
*
4646
* @author Rossen Stoyanchev
47+
* @author Brian Clozel
4748
*/
4849
public class ResourceUrlProviderTests {
4950

@@ -62,7 +63,7 @@ public class ResourceUrlProviderTests {
6263

6364

6465
@BeforeEach
65-
public void setup() throws Exception {
66+
void setup() throws Exception {
6667
this.locations.add(new ClassPathResource("test/", getClass()));
6768
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
6869
this.handler.setLocations(this.locations);
@@ -73,15 +74,15 @@ public void setup() throws Exception {
7374

7475

7576
@Test
76-
public void getStaticResourceUrl() {
77+
void getStaticResourceUrl() {
7778
String expected = "/resources/foo.css";
7879
String actual = this.urlProvider.getForUriString(expected, this.exchange).block(TIMEOUT);
7980

8081
assertThat(actual).isEqualTo(expected);
8182
}
8283

8384
@Test // SPR-13374
84-
public void getStaticResourceUrlRequestWithQueryOrHash() {
85+
void getStaticResourceUrlRequestWithQueryOrHash() {
8586

8687
String url = "/resources/foo.css?foo=bar&url=https://example.org";
8788
String resolvedUrl = this.urlProvider.getForUriString(url, this.exchange).block(TIMEOUT);
@@ -93,7 +94,7 @@ public void getStaticResourceUrlRequestWithQueryOrHash() {
9394
}
9495

9596
@Test
96-
public void getVersionedResourceUrl() {
97+
void getVersionedResourceUrl() {
9798
VersionResourceResolver versionResolver = new VersionResourceResolver();
9899
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
99100
List<ResourceResolver> resolvers = new ArrayList<>();
@@ -108,7 +109,7 @@ public void getVersionedResourceUrl() {
108109
}
109110

110111
@Test // SPR-12647
111-
public void bestPatternMatch() {
112+
void bestPatternMatch() {
112113
ResourceWebHandler otherHandler = new ResourceWebHandler();
113114
otherHandler.setLocations(this.locations);
114115

@@ -129,7 +130,7 @@ public void bestPatternMatch() {
129130

130131
@Test // SPR-12592
131132
@SuppressWarnings("resource")
132-
public void initializeOnce() {
133+
void initializeOnce() {
133134
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
134135
context.setServletContext(new MockServletContext());
135136
context.register(HandlerMappingConfiguration.class);
@@ -139,6 +140,26 @@ public void initializeOnce() {
139140
.hasKeySatisfying(pathPatternStringOf("/resources/**"));
140141
}
141142

143+
@Test
144+
void initializeOnCurrentContext() {
145+
AnnotationConfigWebApplicationContext parentContext = new AnnotationConfigWebApplicationContext();
146+
parentContext.setServletContext(new MockServletContext());
147+
parentContext.register(ParentHandlerMappingConfiguration.class);
148+
149+
AnnotationConfigWebApplicationContext childContext = new AnnotationConfigWebApplicationContext();
150+
childContext.setParent(parentContext);
151+
childContext.setServletContext(new MockServletContext());
152+
childContext.register(HandlerMappingConfiguration.class);
153+
154+
parentContext.refresh();
155+
childContext.refresh();
156+
157+
ResourceUrlProvider parentUrlProvider = parentContext.getBean(ResourceUrlProvider.class);
158+
assertThat(parentUrlProvider.getHandlerMap()).isEmpty();
159+
ResourceUrlProvider childUrlProvider = childContext.getBean(ResourceUrlProvider.class);
160+
assertThat(childUrlProvider.getHandlerMap()).hasKeySatisfying(pathPatternStringOf("/resources/**"));
161+
}
162+
142163

143164
private Condition<PathPattern> pathPatternStringOf(String expected) {
144165
return new Condition<PathPattern>(
@@ -161,4 +182,14 @@ public ResourceUrlProvider resourceUrlProvider() {
161182
}
162183
}
163184

185+
@Configuration
186+
@SuppressWarnings({"unused", "WeakerAccess"})
187+
static class ParentHandlerMappingConfiguration {
188+
189+
@Bean
190+
public ResourceUrlProvider resourceUrlProvider() {
191+
return new ResourceUrlProvider();
192+
}
193+
}
194+
164195
}

spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -27,7 +27,9 @@
2727
import org.apache.commons.logging.Log;
2828
import org.apache.commons.logging.LogFactory;
2929

30+
import org.springframework.beans.BeansException;
3031
import org.springframework.context.ApplicationContext;
32+
import org.springframework.context.ApplicationContextAware;
3133
import org.springframework.context.ApplicationListener;
3234
import org.springframework.context.event.ContextRefreshedEvent;
3335
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@@ -48,12 +50,16 @@
4850
* {@code ResourceHttpRequestHandler}s to make its decisions.
4951
*
5052
* @author Rossen Stoyanchev
53+
* @author Brian Clozel
5154
* @since 4.1
5255
*/
53-
public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> {
56+
public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware {
5457

5558
protected final Log logger = LogFactory.getLog(getClass());
5659

60+
@Nullable
61+
private ApplicationContext applicationContext;
62+
5763
private UrlPathHelper urlPathHelper = UrlPathHelper.defaultInstance;
5864

5965
private PathMatcher pathMatcher = new AntPathMatcher();
@@ -62,6 +68,10 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
6268

6369
private boolean autodetect = true;
6470

71+
@Override
72+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
73+
this.applicationContext = applicationContext;
74+
}
6575

6676
/**
6777
* Configure a {@code UrlPathHelper} to use in
@@ -127,9 +137,9 @@ public boolean isAutodetect() {
127137

128138
@Override
129139
public void onApplicationEvent(ContextRefreshedEvent event) {
130-
if (isAutodetect()) {
140+
if (event.getApplicationContext() == this.applicationContext && isAutodetect()) {
131141
this.handlerMap.clear();
132-
detectResourceHandlers(event.getApplicationContext());
142+
detectResourceHandlers(this.applicationContext);
133143
if (!this.handlerMap.isEmpty()) {
134144
this.autodetect = false;
135145
}

spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderTests.java

+42-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 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.
@@ -44,6 +44,7 @@
4444
*
4545
* @author Jeremy Grelle
4646
* @author Rossen Stoyanchev
47+
* @author Brian Clozel
4748
*/
4849
public class ResourceUrlProviderTests {
4950

@@ -57,7 +58,7 @@ public class ResourceUrlProviderTests {
5758

5859

5960
@BeforeEach
60-
public void setUp() throws Exception {
61+
void setUp() throws Exception {
6162
this.locations.add(new ClassPathResource("test/", getClass()));
6263
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
6364
this.handler.setServletContext(new MockServletContext());
@@ -69,13 +70,13 @@ public void setUp() throws Exception {
6970

7071

7172
@Test
72-
public void getStaticResourceUrl() {
73+
void getStaticResourceUrl() {
7374
String url = this.urlProvider.getForLookupPath("/resources/foo.css");
7475
assertThat(url).isEqualTo("/resources/foo.css");
7576
}
7677

7778
@Test // SPR-13374
78-
public void getStaticResourceUrlRequestWithQueryOrHash() {
79+
void getStaticResourceUrlRequestWithQueryOrHash() {
7980
MockHttpServletRequest request = new MockHttpServletRequest();
8081
request.setContextPath("/");
8182
request.setRequestURI("/");
@@ -90,7 +91,7 @@ public void getStaticResourceUrlRequestWithQueryOrHash() {
9091
}
9192

9293
@Test // SPR-16526
93-
public void getStaticResourceWithMissingContextPath() {
94+
void getStaticResourceWithMissingContextPath() {
9495
MockHttpServletRequest request = new MockHttpServletRequest();
9596
request.setContextPath("/contextpath-longer-than-request-path");
9697
request.setRequestURI("/contextpath-longer-than-request-path/style.css");
@@ -100,7 +101,7 @@ public void getStaticResourceWithMissingContextPath() {
100101
}
101102

102103
@Test
103-
public void getFingerprintedResourceUrl() {
104+
void getFingerprintedResourceUrl() {
104105
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
105106
versionStrategyMap.put("/**", new ContentVersionStrategy());
106107
VersionResourceResolver versionResolver = new VersionResourceResolver();
@@ -116,7 +117,7 @@ public void getFingerprintedResourceUrl() {
116117
}
117118

118119
@Test // SPR-12647
119-
public void bestPatternMatch() throws Exception {
120+
void bestPatternMatch() throws Exception {
120121
ResourceHttpRequestHandler otherHandler = new ResourceHttpRequestHandler();
121122
otherHandler.setLocations(this.locations);
122123
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
@@ -138,7 +139,7 @@ public void bestPatternMatch() throws Exception {
138139

139140
@Test // SPR-12592
140141
@SuppressWarnings("resource")
141-
public void initializeOnce() throws Exception {
142+
void initializeOnce() throws Exception {
142143
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
143144
context.setServletContext(new MockServletContext());
144145
context.register(HandlerMappingConfiguration.class);
@@ -149,8 +150,30 @@ public void initializeOnce() throws Exception {
149150
assertThat(urlProviderBean.isAutodetect()).isFalse();
150151
}
151152

153+
@Test
154+
void initializeOnCurrentContext() {
155+
AnnotationConfigWebApplicationContext parentContext = new AnnotationConfigWebApplicationContext();
156+
parentContext.setServletContext(new MockServletContext());
157+
parentContext.register(ParentHandlerMappingConfiguration.class);
158+
159+
AnnotationConfigWebApplicationContext childContext = new AnnotationConfigWebApplicationContext();
160+
childContext.setParent(parentContext);
161+
childContext.setServletContext(new MockServletContext());
162+
childContext.register(HandlerMappingConfiguration.class);
163+
164+
parentContext.refresh();
165+
childContext.refresh();
166+
167+
ResourceUrlProvider parentUrlProvider = parentContext.getBean(ResourceUrlProvider.class);
168+
assertThat(parentUrlProvider.getHandlerMap()).isEmpty();
169+
assertThat(parentUrlProvider.isAutodetect()).isTrue();
170+
ResourceUrlProvider childUrlProvider = childContext.getBean(ResourceUrlProvider.class);
171+
assertThat(childUrlProvider.getHandlerMap()).containsOnlyKeys("/resources/**");
172+
assertThat(childUrlProvider.isAutodetect()).isFalse();
173+
}
174+
152175
@Test // SPR-16296
153-
public void getForLookupPathShouldNotFailIfPathContainsDoubleSlashes() {
176+
void getForLookupPathShouldNotFailIfPathContainsDoubleSlashes() {
154177
// given
155178
ResourceResolver mockResourceResolver = mock(ResourceResolver.class);
156179
given(mockResourceResolver.resolveUrlPath(any(), any(), any())).willReturn("some-path");
@@ -185,4 +208,14 @@ public ResourceUrlProvider resourceUrlProvider() {
185208
}
186209
}
187210

211+
@Configuration
212+
@SuppressWarnings({"unused", "WeakerAccess"})
213+
static class ParentHandlerMappingConfiguration {
214+
215+
@Bean
216+
public ResourceUrlProvider resourceUrlProvider() {
217+
return new ResourceUrlProvider();
218+
}
219+
}
220+
188221
}

0 commit comments

Comments
 (0)