Skip to content

Commit 44a3700

Browse files
committed
HandlerMappingIntrospector exposes Filter for caching
Closes gh-31588
1 parent 53fe5fa commit 44a3700

File tree

2 files changed

+165
-10
lines changed

2 files changed

+165
-10
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
import java.util.function.BiFunction;
2828
import java.util.stream.Collectors;
2929

30+
import jakarta.servlet.Filter;
31+
import jakarta.servlet.ServletException;
32+
import jakarta.servlet.ServletRequest;
3033
import jakarta.servlet.http.HttpServletRequest;
3134
import jakarta.servlet.http.HttpServletRequestWrapper;
3235

@@ -77,6 +80,15 @@
7780
public class HandlerMappingIntrospector
7881
implements CorsConfigurationSource, ApplicationContextAware, InitializingBean {
7982

83+
static final String MAPPING_ATTRIBUTE =
84+
HandlerMappingIntrospector.class.getName() + ".HandlerMapping";
85+
86+
static final String CORS_CONFIG_ATTRIBUTE =
87+
HandlerMappingIntrospector.class.getName() + ".CorsConfig";
88+
89+
private static final CorsConfiguration NO_CORS_CONFIG = new CorsConfiguration();
90+
91+
8092
@Nullable
8193
private ApplicationContext applicationContext;
8294

@@ -153,6 +165,58 @@ public List<HandlerMapping> getHandlerMappings() {
153165
}
154166

155167

168+
/**
169+
* Return Filter that performs lookups, caches the results in request attributes,
170+
* and clears the attributes after the filter chain returns.
171+
* @since 6.0.14
172+
*/
173+
public Filter createCacheFilter() {
174+
return (request, response, chain) -> {
175+
MatchableHandlerMapping previousMapping = getCachedMapping(request);
176+
CorsConfiguration previousCorsConfig = getCachedCorsConfiguration(request);
177+
try {
178+
HttpServletRequest wrappedRequest = new AttributesPreservingRequest((HttpServletRequest) request);
179+
doWithHandlerMapping(wrappedRequest, false, (mapping, executionChain) -> {
180+
MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrappedRequest);
181+
CorsConfiguration corsConfig = getCorsConfiguration(wrappedRequest, executionChain);
182+
setCache(request, matchableMapping, corsConfig);
183+
return null;
184+
});
185+
chain.doFilter(request, response);
186+
}
187+
catch (Exception ex) {
188+
throw new ServletException("HandlerMapping introspection failed", ex);
189+
}
190+
finally {
191+
setCache(request, previousMapping, previousCorsConfig);
192+
}
193+
};
194+
}
195+
196+
@Nullable
197+
private static MatchableHandlerMapping getCachedMapping(ServletRequest request) {
198+
return (MatchableHandlerMapping) request.getAttribute(MAPPING_ATTRIBUTE);
199+
}
200+
201+
@Nullable
202+
private static CorsConfiguration getCachedCorsConfiguration(ServletRequest request) {
203+
return (CorsConfiguration) request.getAttribute(CORS_CONFIG_ATTRIBUTE);
204+
}
205+
206+
private static void setCache(
207+
ServletRequest request, @Nullable MatchableHandlerMapping mapping,
208+
@Nullable CorsConfiguration corsConfig) {
209+
210+
if (mapping != null) {
211+
request.setAttribute(MAPPING_ATTRIBUTE, mapping);
212+
request.setAttribute(CORS_CONFIG_ATTRIBUTE, (corsConfig != null ? corsConfig : NO_CORS_CONFIG));
213+
}
214+
else {
215+
request.removeAttribute(MAPPING_ATTRIBUTE);
216+
request.removeAttribute(CORS_CONFIG_ATTRIBUTE);
217+
}
218+
}
219+
156220
/**
157221
* Find the {@link HandlerMapping} that would handle the given request and
158222
* return a {@link MatchableHandlerMapping} to use for path matching.
@@ -164,6 +228,10 @@ public List<HandlerMapping> getHandlerMappings() {
164228
*/
165229
@Nullable
166230
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
231+
MatchableHandlerMapping cachedMapping = getCachedMapping(request);
232+
if (cachedMapping != null) {
233+
return cachedMapping;
234+
}
167235
HttpServletRequest requestToUse = new AttributesPreservingRequest(request);
168236
return doWithHandlerMapping(requestToUse, false,
169237
(mapping, executionChain) -> createMatchableHandlerMapping(mapping, requestToUse));
@@ -187,6 +255,10 @@ private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping map
187255
@Override
188256
@Nullable
189257
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
258+
CorsConfiguration cachedCorsConfiguration = getCachedCorsConfiguration(request);
259+
if (cachedCorsConfiguration != null) {
260+
return (cachedCorsConfiguration != NO_CORS_CONFIG ? cachedCorsConfiguration : null);
261+
}
190262
try {
191263
boolean ignoreException = true;
192264
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request);

spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616

1717
package org.springframework.web.servlet.handler;
1818

19+
import java.io.IOException;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.List;
2324

25+
import jakarta.servlet.Filter;
26+
import jakarta.servlet.ServletException;
27+
import jakarta.servlet.http.HttpServlet;
2428
import jakarta.servlet.http.HttpServletRequest;
29+
import jakarta.servlet.http.HttpServletResponse;
2530
import org.junit.jupiter.api.Test;
2631
import org.junit.jupiter.params.ParameterizedTest;
2732
import org.junit.jupiter.params.provider.ValueSource;
@@ -44,7 +49,9 @@
4449
import org.springframework.web.servlet.function.ServerResponse;
4550
import org.springframework.web.servlet.function.support.RouterFunctionMapping;
4651
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
52+
import org.springframework.web.testfixture.servlet.MockFilterChain;
4753
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
54+
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
4855
import org.springframework.web.util.ServletRequestPathUtils;
4956
import org.springframework.web.util.pattern.PathPattern;
5057
import org.springframework.web.util.pattern.PathPatternParser;
@@ -137,7 +144,7 @@ void getMatchable(boolean usePathPatterns) throws Exception {
137144
@Test
138145
void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() {
139146
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
140-
cxt.registerSingleton("mapping", TestHandlerMapping.class);
147+
cxt.registerBean("mapping", HandlerMapping.class, () -> request -> new HandlerExecutionChain(new Object()));
141148
cxt.refresh();
142149

143150
MockHttpServletRequest request = new MockHttpServletRequest();
@@ -193,6 +200,69 @@ void getCorsConfigurationActual() {
193200
assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST"));
194201
}
195202

203+
@Test
204+
void cacheFilter() throws Exception {
205+
testCacheFilter(new MockHttpServletRequest());
206+
}
207+
208+
@Test
209+
void cacheFilterRestoresPreviousValues() throws Exception {
210+
TestMatchableHandlerMapping previousMapping = new TestMatchableHandlerMapping();
211+
CorsConfiguration previousCorsConfig = new CorsConfiguration();
212+
213+
MockHttpServletRequest request = new MockHttpServletRequest();
214+
request.setAttribute(HandlerMappingIntrospector.MAPPING_ATTRIBUTE, previousMapping);
215+
request.setAttribute(HandlerMappingIntrospector.CORS_CONFIG_ATTRIBUTE, previousCorsConfig);
216+
217+
testCacheFilter(request);
218+
219+
assertThat(previousMapping.getInvocationCount()).isEqualTo(0);
220+
assertThat(request.getAttribute(HandlerMappingIntrospector.MAPPING_ATTRIBUTE)).isSameAs(previousMapping);
221+
assertThat(request.getAttribute(HandlerMappingIntrospector.CORS_CONFIG_ATTRIBUTE)).isSameAs(previousCorsConfig);
222+
}
223+
224+
private void testCacheFilter(MockHttpServletRequest request) throws IOException, ServletException {
225+
TestMatchableHandlerMapping mapping = new TestMatchableHandlerMapping();
226+
StaticWebApplicationContext context = new StaticWebApplicationContext();
227+
context.registerBean(TestMatchableHandlerMapping.class, () -> mapping);
228+
context.refresh();
229+
230+
HandlerMappingIntrospector introspector = initIntrospector(context);
231+
MockHttpServletResponse response = new MockHttpServletResponse();
232+
233+
Filter filter = (req, res, chain) -> {
234+
try {
235+
for (int i = 0; i < 10; i++) {
236+
introspector.getMatchableHandlerMapping((HttpServletRequest) req);
237+
introspector.getCorsConfiguration((HttpServletRequest) req);
238+
}
239+
}
240+
catch (Exception ex) {
241+
throw new IllegalStateException(ex);
242+
}
243+
chain.doFilter(req, res);
244+
};
245+
246+
HttpServlet servlet = new HttpServlet() {
247+
248+
@Override
249+
protected void service(HttpServletRequest req, HttpServletResponse res) {
250+
try {
251+
res.getWriter().print("Success");
252+
}
253+
catch (Exception ex) {
254+
throw new IllegalStateException(ex);
255+
}
256+
}
257+
};
258+
259+
new MockFilterChain(servlet, introspector.createCacheFilter(), filter)
260+
.doFilter(request, response);
261+
262+
assertThat(response.getContentAsString()).isEqualTo("Success");
263+
assertThat(mapping.getInvocationCount()).isEqualTo(1);
264+
}
265+
196266
private HandlerMappingIntrospector initIntrospector(WebApplicationContext context) {
197267
HandlerMappingIntrospector introspector = new HandlerMappingIntrospector();
198268
introspector.setApplicationContext(context);
@@ -201,15 +271,6 @@ private HandlerMappingIntrospector initIntrospector(WebApplicationContext contex
201271
}
202272

203273

204-
private static class TestHandlerMapping implements HandlerMapping {
205-
206-
@Override
207-
public HandlerExecutionChain getHandler(HttpServletRequest request) {
208-
return new HandlerExecutionChain(new Object());
209-
}
210-
}
211-
212-
213274
@Configuration
214275
static class TestConfig {
215276

@@ -248,6 +309,7 @@ void handle() {
248309
}
249310
}
250311

312+
251313
private static class TestPathPatternParser extends PathPatternParser {
252314

253315
private final List<String> parsedPatterns = new ArrayList<>();
@@ -264,4 +326,25 @@ public PathPattern parse(String pathPattern) throws PatternParseException {
264326
}
265327
}
266328

329+
330+
private static class TestMatchableHandlerMapping implements MatchableHandlerMapping {
331+
332+
private int invocationCount;
333+
334+
public int getInvocationCount() {
335+
return this.invocationCount;
336+
}
337+
338+
@Override
339+
public HandlerExecutionChain getHandler(HttpServletRequest request) {
340+
this.invocationCount++;
341+
return new HandlerExecutionChain(new Object());
342+
}
343+
344+
@Override
345+
public RequestMatchResult match(HttpServletRequest request, String pattern) {
346+
throw new UnsupportedOperationException();
347+
}
348+
}
349+
267350
}

0 commit comments

Comments
 (0)