Skip to content

Commit 19e8ed1

Browse files
committed
Cache Filter for HandlerMappingIntrospector and log warnings
See gh-31588
1 parent 4464251 commit 19e8ed1

File tree

2 files changed

+103
-58
lines changed

2 files changed

+103
-58
lines changed

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

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Properties;
27+
import java.util.concurrent.atomic.AtomicInteger;
2728
import java.util.function.BiFunction;
2829
import java.util.stream.Collectors;
2930

3031
import jakarta.servlet.DispatcherType;
32+
import jakarta.servlet.Filter;
3133
import jakarta.servlet.ServletException;
3234
import jakarta.servlet.ServletRequest;
3335
import jakarta.servlet.http.HttpServletRequest;
3436
import jakarta.servlet.http.HttpServletRequestWrapper;
37+
import org.apache.commons.logging.Log;
38+
import org.apache.commons.logging.LogFactory;
3539

3640
import org.springframework.beans.factory.BeanFactoryUtils;
3741
import org.springframework.beans.factory.InitializingBean;
@@ -87,6 +91,8 @@
8791
public class HandlerMappingIntrospector
8892
implements CorsConfigurationSource, ApplicationContextAware, InitializingBean {
8993

94+
private static final Log logger = LogFactory.getLog(HandlerMappingIntrospector.class.getName());
95+
9096
private static final String CACHED_RESULT_ATTRIBUTE =
9197
HandlerMappingIntrospector.class.getName() + ".CachedResult";
9298

@@ -99,6 +105,8 @@ public class HandlerMappingIntrospector
99105

100106
private Map<HandlerMapping, PathPatternMatchableHandlerMapping> pathPatternMappings = Collections.emptyMap();
101107

108+
private final CacheResultLogHelper cacheLogHelper = new CacheResultLogHelper();
109+
102110

103111
@Override
104112
public void setApplicationContext(ApplicationContext applicationContext) {
@@ -167,6 +175,36 @@ public List<HandlerMapping> getHandlerMappings() {
167175
}
168176

169177

178+
/**
179+
* {@link Filter} that looks up the {@code MatchableHandlerMapping} and
180+
* {@link CorsConfiguration} for the request proactively before delegating
181+
* to the rest of the chain, caching the result in a request attribute, and
182+
* restoring it after the chain returns.
183+
* <p><strong>Note:</strong> Applications that rely on Spring Security do
184+
* not use this component directly and should not deploy the filter instead
185+
* allowing Spring Security to do it. Other custom security layers used in
186+
* place of Spring Security that also rely on {@code HandlerMappingIntrospector}
187+
* should deploy this filter ahead of other filters where lookups are
188+
* performed, and should also make sure the filter is configured to handle
189+
* all dispatcher types.
190+
* @return the Filter instance to use
191+
* @since 6.0.14
192+
*/
193+
public Filter createCacheFilter() {
194+
return (request, response, chain) -> {
195+
HandlerMappingIntrospector.CachedResult previous = setCache((HttpServletRequest) request);
196+
try {
197+
chain.doFilter(request, response);
198+
}
199+
catch (Exception ex) {
200+
throw new ServletException("HandlerMapping introspection failed", ex);
201+
}
202+
finally {
203+
resetCache(request, previous);
204+
}
205+
};
206+
}
207+
170208
/**
171209
* Perform a lookup and save the {@link CachedResult} as a request attribute.
172210
* This method can be invoked from a filter before subsequent calls to
@@ -178,18 +216,18 @@ public List<HandlerMapping> getHandlerMappings() {
178216
* @since 6.0.14
179217
*/
180218
@Nullable
181-
public CachedResult setCache(HttpServletRequest request) throws ServletException {
182-
CachedResult previous = getAttribute(request);
219+
private CachedResult setCache(HttpServletRequest request) throws ServletException {
220+
CachedResult previous = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
183221
if (previous == null || !previous.matches(request)) {
184222
try {
185223
HttpServletRequest wrapped = new AttributesPreservingRequest(request);
186-
CachedResult cachedResult = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> {
224+
CachedResult result = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> {
187225
MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrapped);
188226
CorsConfiguration corsConfig = getCorsConfiguration(wrapped, executionChain);
189227
return new CachedResult(request, matchableMapping, corsConfig);
190228
});
191229
request.setAttribute(CACHED_RESULT_ATTRIBUTE,
192-
cachedResult != null ? cachedResult : new CachedResult(request, null, null));
230+
(result != null ? result : new CachedResult(request, null, null)));
193231
}
194232
catch (Throwable ex) {
195233
throw new ServletException("HandlerMapping introspection failed", ex);
@@ -203,7 +241,7 @@ public CachedResult setCache(HttpServletRequest request) throws ServletException
203241
* a filter after delegating to the rest of the chain.
204242
* @since 6.0.14
205243
*/
206-
public void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) {
244+
private void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) {
207245
request.setAttribute(CACHED_RESULT_ATTRIBUTE, cachedResult);
208246
}
209247

@@ -218,10 +256,11 @@ public void resetCache(ServletRequest request, @Nullable CachedResult cachedResu
218256
*/
219257
@Nullable
220258
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
221-
CachedResult cachedResult = getCachedResultFor(request);
222-
if (cachedResult != null) {
223-
return cachedResult.getHandlerMapping();
259+
CachedResult result = CachedResult.forRequest(request);
260+
if (result != null) {
261+
return result.getHandlerMapping();
224262
}
263+
this.cacheLogHelper.logHandlerMappingCacheMiss(request);
225264
HttpServletRequest requestToUse = new AttributesPreservingRequest(request);
226265
return doWithHandlerMapping(requestToUse, false,
227266
(mapping, executionChain) -> createMatchableHandlerMapping(mapping, requestToUse));
@@ -245,10 +284,11 @@ private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping map
245284
@Override
246285
@Nullable
247286
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
248-
CachedResult cachedResult = getCachedResultFor(request);
249-
if (cachedResult != null) {
250-
return cachedResult.getCorsConfig();
287+
CachedResult result = CachedResult.forRequest(request);
288+
if (result != null) {
289+
return result.getCorsConfig();
251290
}
291+
this.cacheLogHelper.logCorsConfigCacheMiss(request);
252292
try {
253293
boolean ignoreException = true;
254294
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request);
@@ -312,28 +352,14 @@ private <T> T doWithHandlerMapping(
312352
return null;
313353
}
314354

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-
329355

330356
/**
331357
* Container for a {@link MatchableHandlerMapping} and {@link CorsConfiguration}
332-
* for a given request identified by dispatcher type and requestURI.
358+
* for a given request matched by dispatcher type and requestURI.
333359
* @since 6.0.14
334360
*/
335361
@SuppressWarnings("serial")
336-
public static final class CachedResult {
362+
private static final class CachedResult {
337363

338364
private final DispatcherType dispatcherType;
339365

@@ -371,7 +397,53 @@ public CorsConfiguration getCorsConfig() {
371397

372398
@Override
373399
public String toString() {
374-
return "CacheValue " + this.dispatcherType + " '" + this.requestURI + "'";
400+
return "CachedResult for " + this.dispatcherType + " dispatch to '" + this.requestURI + "'";
401+
}
402+
403+
404+
/**
405+
* Return a {@link CachedResult} that matches the given request.
406+
*/
407+
@Nullable
408+
public static CachedResult forRequest(HttpServletRequest request) {
409+
CachedResult result = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
410+
return (result != null && result.matches(request) ? result : null);
411+
}
412+
413+
}
414+
415+
416+
private static class CacheResultLogHelper {
417+
418+
private final Map<String, AtomicInteger> counters =
419+
Map.of("MatchableHandlerMapping", new AtomicInteger(), "CorsConfiguration", new AtomicInteger());
420+
421+
public void logHandlerMappingCacheMiss(HttpServletRequest request) {
422+
logCacheMiss("MatchableHandlerMapping", request);
423+
}
424+
425+
public void logCorsConfigCacheMiss(HttpServletRequest request) {
426+
logCacheMiss("CorsConfiguration", request);
427+
}
428+
429+
private void logCacheMiss(String label, HttpServletRequest request) {
430+
AtomicInteger counter = this.counters.get(label);
431+
Assert.notNull(counter, "Expected '" + label + "' counter.");
432+
433+
String message = getLogMessage(label, request);
434+
435+
if (logger.isWarnEnabled() && counter.getAndIncrement() == 0) {
436+
logger.warn(message + " This is logged once only at WARN level, and every time at TRACE.");
437+
}
438+
else if (logger.isTraceEnabled()) {
439+
logger.trace("No CachedResult, performing " + label + " lookup instead.");
440+
}
441+
}
442+
443+
private static String getLogMessage(String label, HttpServletRequest request) {
444+
return "Cache miss for " + request.getDispatcherType() + " dispatch to '" + request.getRequestURI() + "' " +
445+
"(previous " + request.getAttribute(CACHED_RESULT_ATTRIBUTE) + "). " +
446+
"Performing " + label + " lookup.";
375447
}
376448
}
377449

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

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
import org.springframework.web.servlet.function.RouterFunctions;
5353
import org.springframework.web.servlet.function.ServerResponse;
5454
import org.springframework.web.servlet.function.support.RouterFunctionMapping;
55-
import org.springframework.web.servlet.handler.HandlerMappingIntrospector.CachedResult;
5655
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
5756
import org.springframework.web.testfixture.servlet.MockFilterChain;
5857
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
@@ -217,7 +216,7 @@ void cacheFilter() throws Exception {
217216
MockHttpServletResponse response = new MockHttpServletResponse();
218217

219218
MockFilterChain filterChain = new MockFilterChain(
220-
new TestServlet(), new CacheResultFilter(introspector), new AuthFilter(introspector, corsConfig));
219+
new TestServlet(), introspector.createCacheFilter(), new AuthFilter(introspector, corsConfig));
221220

222221
filterChain.doFilter(request, response);
223222

@@ -241,10 +240,10 @@ void cacheFilterWithNestedDispatch() throws Exception {
241240

242241
MockFilterChain filterChain = new MockFilterChain(
243242
new TestServlet(),
244-
new CacheResultFilter(introspector),
243+
introspector.createCacheFilter(),
245244
new AuthFilter(introspector, corsConfig1),
246245
(req, res, chain) -> chain.doFilter(new MockHttpServletRequest("GET", "/2"), res),
247-
new CacheResultFilter(introspector),
246+
introspector.createCacheFilter(),
248247
new AuthFilter(introspector, corsConfig2));
249248

250249
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -372,32 +371,6 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
372371
}
373372

374373

375-
private static class CacheResultFilter implements Filter {
376-
377-
private final HandlerMappingIntrospector introspector;
378-
379-
private CacheResultFilter(HandlerMappingIntrospector introspector) {
380-
this.introspector = introspector;
381-
}
382-
383-
@Override
384-
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
385-
throws ServletException {
386-
387-
CachedResult previousValue = this.introspector.setCache((HttpServletRequest) req);
388-
try {
389-
chain.doFilter(req, res);
390-
}
391-
catch (Exception ex) {
392-
throw new ServletException("HandlerMapping introspection failed", ex);
393-
}
394-
finally {
395-
this.introspector.resetCache(req, previousValue);
396-
}
397-
}
398-
}
399-
400-
401374
private static class AuthFilter implements Filter {
402375

403376
private final HandlerMappingIntrospector introspector;

0 commit comments

Comments
 (0)