Skip to content

Commit a4e3af5

Browse files
committed
Revise HandlerMappingIntrospector caching
Expose methods to set and reset cache to use from a Filter instead of a method to create such a Filter. Also use cached results only if they match by dispatcher type and requestURI. See gh-31588
1 parent e71117d commit a4e3af5

File tree

2 files changed

+251
-107
lines changed

2 files changed

+251
-107
lines changed

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

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

30-
import jakarta.servlet.Filter;
30+
import jakarta.servlet.DispatcherType;
3131
import jakarta.servlet.ServletException;
3232
import jakarta.servlet.ServletRequest;
3333
import jakarta.servlet.http.HttpServletRequest;
@@ -68,25 +68,27 @@
6868
* request.
6969
* </ul>
7070
*
71-
* <p><strong>Note:</strong> This is primarily an SPI to allow Spring Security
71+
* <p>Note that this is primarily an SPI to allow Spring Security
7272
* to align its pattern matching with the same pattern matching that would be
7373
* used in Spring MVC for a given request, in order to avoid security issues.
7474
* Use of this introspector should be avoided for other purposes because it
7575
* incurs the overhead of resolving the handler for a request.
7676
*
77+
* <p>Alternative security filter solutions that also rely on
78+
* {@link HandlerMappingIntrospector} should consider adding an additional
79+
* {@link jakarta.servlet.Filter} that invokes
80+
* {@link #setCache(HttpServletRequest)} and {@link #resetCache(ServletRequest, CachedResult)}
81+
* before and after delegating to the rest of the chain. Such a Filter should
82+
* process all dispatcher types and should be ordered ahead of security filters.
83+
*
7784
* @author Rossen Stoyanchev
7885
* @since 4.3.1
7986
*/
8087
public class HandlerMappingIntrospector
8188
implements CorsConfigurationSource, ApplicationContextAware, InitializingBean {
8289

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+
private static final String CACHED_RESULT_ATTRIBUTE =
91+
HandlerMappingIntrospector.class.getName() + ".CachedResult";
9092

9193

9294
@Nullable
@@ -166,55 +168,43 @@ public List<HandlerMapping> getHandlerMappings() {
166168

167169

168170
/**
169-
* Return Filter that performs lookups, caches the results in request attributes,
170-
* and clears the attributes after the filter chain returns.
171+
* Perform a lookup and save the {@link CachedResult} as a request attribute.
172+
* This method can be invoked from a filter before subsequent calls to
173+
* {@link #getMatchableHandlerMapping(HttpServletRequest)} and
174+
* {@link #getCorsConfiguration(HttpServletRequest)} to avoid repeated lookups.
175+
* @param request the current request
176+
* @return the previous {@link CachedResult}, if there is one from a parent dispatch
177+
* @throws ServletException thrown the lookup fails for any reason
171178
* @since 6.0.14
172179
*/
173-
public Filter createCacheFilter() {
174-
return (request, response, chain) -> {
175-
MatchableHandlerMapping previousMapping = getCachedMapping(request);
176-
CorsConfiguration previousCorsConfig = getCachedCorsConfiguration(request);
180+
@Nullable
181+
public CachedResult setCache(HttpServletRequest request) throws ServletException {
182+
CachedResult previous = getAttribute(request);
183+
if (previous == null || !previous.matches(request)) {
177184
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;
185+
HttpServletRequest wrapped = new AttributesPreservingRequest(request);
186+
CachedResult cachedResult = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> {
187+
MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrapped);
188+
CorsConfiguration corsConfig = getCorsConfiguration(wrapped, executionChain);
189+
return new CachedResult(request, matchableMapping, corsConfig);
184190
});
185-
chain.doFilter(request, response);
191+
request.setAttribute(CACHED_RESULT_ATTRIBUTE,
192+
cachedResult != null ? cachedResult : new CachedResult(request, null, null));
186193
}
187-
catch (Exception ex) {
194+
catch (Throwable ex) {
188195
throw new ServletException("HandlerMapping introspection failed", ex);
189196
}
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);
197+
}
198+
return previous;
204199
}
205200

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-
}
201+
/**
202+
* Restore a previous {@link CachedResult}. This method can be invoked from
203+
* a filter after delegating to the rest of the chain.
204+
* @since 6.0.14
205+
*/
206+
public void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) {
207+
request.setAttribute(CACHED_RESULT_ATTRIBUTE, cachedResult);
218208
}
219209

220210
/**
@@ -228,9 +218,9 @@ private static void setCache(
228218
*/
229219
@Nullable
230220
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
231-
MatchableHandlerMapping cachedMapping = getCachedMapping(request);
232-
if (cachedMapping != null) {
233-
return cachedMapping;
221+
CachedResult cachedResult = getCachedResultFor(request);
222+
if (cachedResult != null) {
223+
return cachedResult.getHandlerMapping();
234224
}
235225
HttpServletRequest requestToUse = new AttributesPreservingRequest(request);
236226
return doWithHandlerMapping(requestToUse, false,
@@ -255,9 +245,9 @@ private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping map
255245
@Override
256246
@Nullable
257247
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
258-
CorsConfiguration cachedCorsConfiguration = getCachedCorsConfiguration(request);
259-
if (cachedCorsConfiguration != null) {
260-
return (cachedCorsConfiguration != NO_CORS_CONFIG ? cachedCorsConfiguration : null);
248+
CachedResult cachedResult = getCachedResultFor(request);
249+
if (cachedResult != null) {
250+
return cachedResult.getCorsConfig();
261251
}
262252
try {
263253
boolean ignoreException = true;
@@ -322,6 +312,68 @@ private <T> T doWithHandlerMapping(
322312
return null;
323313
}
324314

315+
/**
316+
* Return a {@link CachedResult} that matches the given request.
317+
*/
318+
@Nullable
319+
private CachedResult getCachedResultFor(HttpServletRequest request) {
320+
CachedResult result = getAttribute(request);
321+
return (result != null && result.matches(request) ? result : null);
322+
}
323+
324+
@Nullable
325+
private static CachedResult getAttribute(HttpServletRequest request) {
326+
return (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
327+
}
328+
329+
330+
/**
331+
* Container for a {@link MatchableHandlerMapping} and {@link CorsConfiguration}
332+
* for a given request identified by dispatcher type and requestURI.
333+
* @since 6.0.14
334+
*/
335+
public final static class CachedResult {
336+
337+
private final DispatcherType dispatcherType;
338+
339+
private final String requestURI;
340+
341+
@Nullable
342+
private final MatchableHandlerMapping handlerMapping;
343+
344+
@Nullable
345+
private final CorsConfiguration corsConfig;
346+
347+
private CachedResult(HttpServletRequest request,
348+
@Nullable MatchableHandlerMapping mapping, @Nullable CorsConfiguration config) {
349+
350+
this.dispatcherType = request.getDispatcherType();
351+
this.requestURI = request.getRequestURI();
352+
this.handlerMapping = mapping;
353+
this.corsConfig = config;
354+
}
355+
356+
public boolean matches(HttpServletRequest request) {
357+
return (this.dispatcherType.equals(request.getDispatcherType()) &&
358+
this.requestURI.matches(request.getRequestURI()));
359+
}
360+
361+
@Nullable
362+
public MatchableHandlerMapping getHandlerMapping() {
363+
return this.handlerMapping;
364+
}
365+
366+
@Nullable
367+
public CorsConfiguration getCorsConfig() {
368+
return this.corsConfig;
369+
}
370+
371+
@Override
372+
public String toString() {
373+
return "CacheValue " + this.dispatcherType + " '" + this.requestURI + "'";
374+
}
375+
}
376+
325377

326378
/**
327379
* Request wrapper that buffers request attributes in order protect the

0 commit comments

Comments
 (0)