24
24
import java .util .List ;
25
25
import java .util .Map ;
26
26
import java .util .Properties ;
27
+ import java .util .concurrent .atomic .AtomicInteger ;
27
28
import java .util .function .BiFunction ;
28
29
import java .util .stream .Collectors ;
29
30
30
31
import jakarta .servlet .DispatcherType ;
32
+ import jakarta .servlet .Filter ;
31
33
import jakarta .servlet .ServletException ;
32
34
import jakarta .servlet .ServletRequest ;
33
35
import jakarta .servlet .http .HttpServletRequest ;
34
36
import jakarta .servlet .http .HttpServletRequestWrapper ;
37
+ import org .apache .commons .logging .Log ;
38
+ import org .apache .commons .logging .LogFactory ;
35
39
36
40
import org .springframework .beans .factory .BeanFactoryUtils ;
37
41
import org .springframework .beans .factory .InitializingBean ;
87
91
public class HandlerMappingIntrospector
88
92
implements CorsConfigurationSource , ApplicationContextAware , InitializingBean {
89
93
94
+ private static final Log logger = LogFactory .getLog (HandlerMappingIntrospector .class .getName ());
95
+
90
96
private static final String CACHED_RESULT_ATTRIBUTE =
91
97
HandlerMappingIntrospector .class .getName () + ".CachedResult" ;
92
98
@@ -99,6 +105,8 @@ public class HandlerMappingIntrospector
99
105
100
106
private Map <HandlerMapping , PathPatternMatchableHandlerMapping > pathPatternMappings = Collections .emptyMap ();
101
107
108
+ private final CacheResultLogHelper cacheLogHelper = new CacheResultLogHelper ();
109
+
102
110
103
111
@ Override
104
112
public void setApplicationContext (ApplicationContext applicationContext ) {
@@ -167,6 +175,36 @@ public List<HandlerMapping> getHandlerMappings() {
167
175
}
168
176
169
177
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
+
170
208
/**
171
209
* Perform a lookup and save the {@link CachedResult} as a request attribute.
172
210
* This method can be invoked from a filter before subsequent calls to
@@ -178,18 +216,18 @@ public List<HandlerMapping> getHandlerMappings() {
178
216
* @since 6.0.14
179
217
*/
180
218
@ 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 );
183
221
if (previous == null || !previous .matches (request )) {
184
222
try {
185
223
HttpServletRequest wrapped = new AttributesPreservingRequest (request );
186
- CachedResult cachedResult = doWithHandlerMapping (wrapped , false , (mapping , executionChain ) -> {
224
+ CachedResult result = doWithHandlerMapping (wrapped , false , (mapping , executionChain ) -> {
187
225
MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping (mapping , wrapped );
188
226
CorsConfiguration corsConfig = getCorsConfiguration (wrapped , executionChain );
189
227
return new CachedResult (request , matchableMapping , corsConfig );
190
228
});
191
229
request .setAttribute (CACHED_RESULT_ATTRIBUTE ,
192
- cachedResult != null ? cachedResult : new CachedResult (request , null , null ));
230
+ ( result != null ? result : new CachedResult (request , null , null ) ));
193
231
}
194
232
catch (Throwable ex ) {
195
233
throw new ServletException ("HandlerMapping introspection failed" , ex );
@@ -203,7 +241,7 @@ public CachedResult setCache(HttpServletRequest request) throws ServletException
203
241
* a filter after delegating to the rest of the chain.
204
242
* @since 6.0.14
205
243
*/
206
- public void resetCache (ServletRequest request , @ Nullable CachedResult cachedResult ) {
244
+ private void resetCache (ServletRequest request , @ Nullable CachedResult cachedResult ) {
207
245
request .setAttribute (CACHED_RESULT_ATTRIBUTE , cachedResult );
208
246
}
209
247
@@ -218,10 +256,11 @@ public void resetCache(ServletRequest request, @Nullable CachedResult cachedResu
218
256
*/
219
257
@ Nullable
220
258
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 ();
224
262
}
263
+ this .cacheLogHelper .logHandlerMappingCacheMiss (request );
225
264
HttpServletRequest requestToUse = new AttributesPreservingRequest (request );
226
265
return doWithHandlerMapping (requestToUse , false ,
227
266
(mapping , executionChain ) -> createMatchableHandlerMapping (mapping , requestToUse ));
@@ -245,10 +284,11 @@ private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping map
245
284
@ Override
246
285
@ Nullable
247
286
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 ();
251
290
}
291
+ this .cacheLogHelper .logCorsConfigCacheMiss (request );
252
292
try {
253
293
boolean ignoreException = true ;
254
294
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest (request );
@@ -312,28 +352,14 @@ private <T> T doWithHandlerMapping(
312
352
return null ;
313
353
}
314
354
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
355
330
356
/**
331
357
* 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.
333
359
* @since 6.0.14
334
360
*/
335
361
@ SuppressWarnings ("serial" )
336
- public static final class CachedResult {
362
+ private static final class CachedResult {
337
363
338
364
private final DispatcherType dispatcherType ;
339
365
@@ -371,7 +397,53 @@ public CorsConfiguration getCorsConfig() {
371
397
372
398
@ Override
373
399
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." ;
375
447
}
376
448
}
377
449
0 commit comments