Affects: spring-web 6.0.7, probably any other version too, as the root cause is mvcMatchers
and HandlerMappingIntrospector
.
When autorization configuration in HttpSecurity
contains many request matchers and org.springframework.web
logger is set to debug
, then logs are spammed with message about selected handler mapper on each request (i.e. "Mapped to package.Controller#handler()"). The more requestMatchers
configured (or even individual URLs in the same request matcher), the more log lines with the same mapping message appears.
Example (spring-boot 3.0.5, spring-security 6.0.2):
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/error").permitAll()
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/livez", "/readyz").permitAll()
.requestMatchers("/actuator/**").authenticated()
.requestMatchers("/management-api/**").hasRole("ADMIN")
.requestMatchers("/api/v2/config/**", "/api/v2/preferences/**").permitAll()
.anyRequest().permitAll()
)
.build();
}
@RestController
public static class MyController {
@GetMapping("/test")
public String test() {
return "OK";
}
}
}
Then curl -v "http://localhost:8080/test"
will result with these logs:
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : GET "/test", parameters={}
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.302+02:00 DEBUG 7304 --- [nio-8080-exec-6] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
2023-04-17T21:56:51.302+02:00 DEBUG 7304 --- [nio-8080-exec-6] m.m.a.RequestResponseBodyMethodProcessor : Writing ["OK"]
2023-04-17T21:56:51.302+02:00 DEBUG 7304 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : Completed 200 OK
While the last Mapped to
log message (that is after DispatcherServlet
log) is useful, other 10 are confusing and "spamming". And also totally useless. Why would I want to know what controller is matched to the current request every time when MvcRequestMatcher
checks current request's url against each configured URL? If the log would display something like "/readyz is not matched against /test" (even for each matcher) it would be useful, but resolving current request to a controller's method is just a side effect of the request matching algorithm and the result of such resolving should not be shown on the debug level, IMHO. Especially for each request matcher in the scope of the same request.
The root cause is probably the usage of MvcRequestMatcher
by default and its delegation to HandlerMappingIntrospector
that calls handlerMapping.getHandler(request)
. AbstractHandlerMapping.getHandler(HttpServletRequest)
then logs that message.
Changing requestMatchers(String)
to requestMatchers(AntPathRequestMatcher)
solves the problem.
E.g. changing
.requestMatchers("/livez", "/readyz").permitAll()
with
.requestMatchers(antMatcher("/livez"), antMatcher("/readyz")).permitAll()
makes 2 less Mapped to
messages to be logged (9 instead of 11).
To be honest, I don't know how to solve this. Probably AbstractHandlerMapping.getHandler(HttpServletRequest)
should not log on debug
level? And the caller of this method (e.g. DispatcherServlet
) should log the resolved mapping instead, if it's appropriate. This way if the resolving is done not for the purpose of request dispatching but for e.g. comparison with security rule, the result of resolving will not be logged. HandlerMappingIntrospector
could also log the result, if needed, but probably on a trace
level.
This problem probably exists for a long time, but previously most popular request matcher was antMatcher
. Now the mvcMatcher
is the default if spring-webmvc is on classpath (like 99% of the time) so the problem just came to the surface.
Comment From: simonbasle
Briefly looking at it, it appears that piece of code uses a logger
instance (from ApplicationObjectSupport
) while the class has a second logger this.mappingsLogger
which is "hidden" using LogDelegateFactory.getHiddenLog
.
To quote getHiddenLog
's javadoc:
Create a "hidden" logger with a category name prefixed with "_", thus precluding it from being enabled together with other log categories from the same package. This is useful for specialized output that is either too verbose or otherwise optional or unnecessary to see all the time.
I think that would fit the bill, @rstoyanchev any insight?
(if that's the case, this issue would be ideal-for-contribution
too)
Comment From: rstoyanchev
Indeed this is due to the use of HandlerMappingIntrospector
to help mvcRequestMatcher
make the same pattern matching decisions as the HandlerMapping
that matches the request. However, I am surprised that it happens so many times. @rwinch is this expected? Conceptually, Spring Security should not have to do this more than once per request. So, if anything the goal should be to ensure that, rather than change the logging.
Comment From: xak2000
@rstoyanchev
So, if anything the goal should be to ensure that, rather than change the logging.
Maybe both?
Even if resolving will be done only once by Spring Security, the log will look like:
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] o.s.web.servlet.DispatcherServlet : GET "/test", parameters={}
2023-04-17T21:56:51.301+02:00 DEBUG 7304 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.DemoApplication$MyController#test()
The first Mapped to
is still excessive.
At the moment the code for logging this looks like this:
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
logger.debug("Mapped to " + executionChain.getHandler());
}
Probably, we could use mappingsLogger
instead of logger
if TRACE
level is disabled, but DEBUG
level is enabled, like @simonbasle suggested?
For example there is a similar pattern in the AbstractDetectingUrlHandlerMapping.detectHandlers()
:
if (mappingsLogger.isDebugEnabled()) {
mappingsLogger.debug(formatMappingName() + " " + getHandlerMap());
}
else if ((logger.isDebugEnabled() && !getHandlerMap().isEmpty()) || logger.isTraceEnabled()) {
logger.debug("Detected " + getHandlerMap().size() + " mappings in " + formatMappingName());
}
But this will make even second Mapped to
message to be hidden on DEBUG level of org.springframework.web
logger, that is also not optimal, as it's really useful to see what controller's method is selected to handle the request right after GET "/test", parameters={}
log message. By replacing logger
with mappingsLogger
this message will appear only if _org.springframework.web.servlet.HandlerMapping
logger is explicitly configured on DEBUG level or org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
is configured on TRACE level. And both variants will still enable both Mapped to
messages (the garbage one and the desired one). :(
Comment From: rstoyanchev
Logging is now suppressed with a request attribute as the trigger.
Comment From: xak2000
Wow, very creative solution! Thanks! 👍