一。问题描述
springboot版本升级到2.6.13 自定义的拦截器居然出错了
主要业务代码如下
主要根据请求获取 对应的HandlerMethod
private HandlerMethod getHandlerMethod(HttpServletrequest request) {
Map<String, HandlerMapping> requestMappings = BeanFactoryUtils.beansOfTypeIncludingAncestors(context,
HandlerMapping.class, true, false);
try {
for (HandlerMapping handlerMapping : requestMappings.values()) {
HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
if (handlerExecutionChain != null && handlerExecutionChain.getHandler() instanceof HandlerMethod) {
return (HandlerMethod) handlerExecutionChain.getHandler();
}
}
} catch (Exception e) {
logger.error("获取URL{}对应的处理方法出现异常:{}", request.getServletPath(), e);
}
return null;
}
异常如下
java.lang.IllegalArgumentException: Expected parsed RequestPath in request attribute "org.springframework.web.util.ServletRequestPathUtils.PATH".
at org.springframework.util.Assert.notNull(Assert.java:219)
at org.springframework.web.util.ServletRequestPathUtils.getParsedRequestPath(ServletRequestPathUtils.java:77)
at org.springframework.web.servlet.handler.AbstractHandlerMapping.initLookupPath(AbstractHandlerMapping.java:574)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:380)
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:125)
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:67)
at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:498)
at ***.style.***mon.permission.LoginCheckFilter.getHandlerMethod(LoginCheckFilter.java:84)
at ***.style.***mon.permission.LoginCheckFilter.doFilter(LoginCheckFilter.java:62)
二。 错误分析
主要是因为 请求 路径解析失败 可以从如下代码看到
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
public static RequestPath getParsedRequestPath(ServletRequest request) {
RequestPath path = (RequestPath) request.getAttribute(PATH_ATTRIBUTE);
Assert.notNull(path, () -> "Expected parsed RequestPath in request attribute \"" + PATH_ATTRIBUTE + "\".");
return path;
}
直接从请求中设置的值获取 对应的path 如果获取失败就会看到错误提示。
三。解决
- 修改路径解析的方式
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
正文结束
四。 源码分析
先下报错位置的代码
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
public static RequestPath getParsedRequestPath(ServletRequest request) {
RequestPath path = (RequestPath) request.getAttribute(PATH_ATTRIBUTE);
Assert.notNull(path, () -> "Expected parsed RequestPath in request attribute \"" + PATH_ATTRIBUTE + "\".");
return path;
}
在usesPathPatterns 方法中进行判断 是否存在 首先结论肯定是存在,报错误日志已经提示调用了下面的方法
那我们还是来验证一下
org.springframework.web.servlet.handler.AbstractHandlerMapping#usesPathPatterns
@Override
public boolean usesPathPatterns() {
return getPatternParser() != null;
}
@Nullable
public PathPatternParser getPatternParser() {
return this.patternParser;
}
然后看下 patternParser 这个成员变量在哪里被赋值
其中只有一个 set方法 来设置patternParser
public void setPatternParser(PathPatternParser patternParser) {
this.patternParser = patternParser;
}
再来看下被调用的地方 被调用的地址有点多 为了避免找错,我们直接断点看下
调用的链路如下
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#setPatternParser
@Override
public void setPatternParser(PathPatternParser patternParser) {
Assert.state(this.mappingRegistry.getRegistrations().isEmpty(),
"PathPatternParser must be set before the initialization of " +
"request mappings through InitializingBean#afterPropertiesSet.");
super.setPatternParser(patternParser);
}
org.springframework.web.servlet.config.annotation.WebMv***onfigurationSupport#requestMappingHandlerMapping
@Bean
@SuppressWarnings("deprecation")
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mv***ontentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mv***onversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
mapping.setContentNegotiationManager(contentNegotiationManager);
mapping.setCorsConfigurations(getCorsConfigurations());
//重点在这里
PathMatchConfigurer pathConfig = getPathMatchConfigurer();
if (pathConfig.getPatternParser() != null) {
//调用位置
mapping.setPatternParser(pathConfig.getPatternParser());
}
//省略部分代码
return mapping;
}
调用路径整体的流程如上
但是我们再来看下 具体 细节
org.springframework.web.servlet.config.annotation.WebMv***onfigurationSupport#getPathMatchConfigurer
protected PathMatchConfigurer getPathMatchConfigurer() {
if (this.pathMatchConfigurer == null) {
//为空 则new一个
this.pathMatchConfigurer = new PathMatchConfigurer();
//使用钩子方法进行配置
configurePathMatch(this.pathMatchConfigurer);
}
return this.pathMatchConfigurer;
}
configurePathMatch 具体的钩子方法在
org.springframework.web.servlet.config.annotation.DelegatingWebMv***onfiguration#configurePathMatch
@Override
protected void configurePathMatch(PathMatchConfigurer configurer) {
this.configurers.configurePathMatch(configurer);
}
具体的调用又在
org.springframework.web.servlet.config.annotation.WebMv***onfigurer***posite#configurePathMatch
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
for (WebMv***onfigurer delegate : this.delegates) {
delegate.configurePathMatch(configurer);
}
}
最后遍历delegates 调用 configurePathMatch方法 进行配置 这里只有一个 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
我们来看下 WebMvcAutoConfigurationAdapter 的 configurePathMatch 方法
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configurePathMatch
@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMv***onfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMv***onfigurer, ServletContextAware {
private static final Log logger = LogFactory.getLog(WebMv***onfigurer.class);
//省略部分方法以及成员变量
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
if (this.mvcProperties.getPathmatch()
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
configurer.setPatternParser(pathPatternParser);
}
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
this.dispatcherServletPath.ifAvailable((dispatcherPath) -> {
String servletUrlMapping = dispatcherPath.getServletUrlMapping();
if (servletUrlMapping.equals("/") && singleDispatcherServlet()) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
});
}
//省略部分代码
}
configurePathMatch 方法中首先判断 配置属性 matchingStrategy 是否是 PATH_PATTERN_PARSER 如果未配置情况下 都是这个PATH_PATTERN_PARSER 所以 会进行设置setPatternParser 且设置值为 PathPatternParser
所以在org.springframework.web.servlet.config.annotation.WebMv***onfigurationSupport#requestMappingHandlerMapping
这里 RequestMappingHandlerMapping 所设置的处理类PatternParser 对应的实例为 PathPatternParser
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mv***ontentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mv***onversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
mapping.setContentNegotiationManager(contentNegotiationManager);
mapping.setCorsConfigurations(getCorsConfigurations());
PathMatchConfigurer pathConfig = getPathMatchConfigurer();
if (pathConfig.getPatternParser() != null) {
mapping.setPatternParser(pathConfig.getPatternParser());
}
//省略部分代码
return mapping;
}
RequestMappingHandlerMapping
是 Spring MVC 中的一个关键组件,用于映射请求到相应的处理方法(controller 方法)。其中的 patternParser
用于解析请求映射中的 URL 模式,以确定哪个处理方法应该处理特定的请求。
那后续将会使用 PathPatternParser 来处理请求路径
所以在不做配置的情况 默认都是使用的PathPatternParser 来解析请求路径
那解决的办法就出来了
- 更换解析器 使用 ANT_PATH_MATCHER 具体配置方法如上
路径解析器修改后
新的调用就成了如下逻辑
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
//这个逻辑将不会走 因为patternParser 未设置为空
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
//走的这段逻辑
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
org.springframework.web.util.UrlPathHelper#resolveAndCacheLookupPath
public String resolveAndCacheLookupPath(HttpServletRequest request) {
String lookupPath = getLookupPathForRequest(request);
request.setAttribute(PATH_ATTRIBUTE, lookupPath);
return lookupPath;
}
public String getLookupPathForRequest(HttpServletRequest request) {
String pathWithinApp = getPathWithinApplication(request);
// Always use full path within current servlet context?
if (this.alwaysUseFullPath || skipServletPathDetermination(request)) {
return pathWithinApp;
}
// Else, use path within current servlet mapping if applicable
String rest = getPathWithinServletMapping(request, pathWithinApp);
if (StringUtils.hasLength(rest)) {
return rest;
}
else {
return pathWithinApp;
}
}
首先获取应用内的路径
public String getPathWithinApplication(HttpServletRequest request) {
String contextPath = getContextPath(request);
String requestUri = getRequestUri(request);
String path = getRemainingPath(requestUri, contextPath, true);
if (path != null) {
// Normal case: URI contains context path.
return (StringUtils.hasText(path) ? path : "/");
}
else {
return requestUri;
}
}
其次获取servlet 映射路径
protected String getPathWithinServletMapping(HttpServletRequest request,
String pathWithinApp) {
String servletPath = getServletPath(request);
String sanitizedPathWithinApp = getSanitizedPath(pathWithinApp);
String path;
// If the app container sanitized the servletPath, check against the sanitized version
if (servletPath.contains(sanitizedPathWithinApp)) {
path = getRemainingPath(sanitizedPathWithinApp, servletPath, false);
}
else {
path = getRemainingPath(pathWithinApp, servletPath, false);
}
if (path != null) {
// Normal case: URI contains servlet path.
return path;
}
else {
// Special case: URI is different from servlet path.
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
// Use path info if available. Indicates index page within a servlet mapping?
// e.g. with index page: URI="/", servletPath="/index.html"
return pathInfo;
}
if (!this.urlDecode) {
// No path info... (not mapped by prefix, nor by extension, nor "/*")
// For the default servlet mapping (i.e. "/"), urlDecode=false can
// cause issues since getServletPath() returns a decoded path.
// If decoding pathWithinApp yields a match just use pathWithinApp.
path = getRemainingPath(decodeInternal(request, pathWithinApp), servletPath, false);
if (path != null) {
return pathWithinApp;
}
}
// Otherwise, use the full servlet path.
return servletPath;
}
}
这样路径就被解析了 而不是另一种方式 直接从请求头获取 (如下)
public static RequestPath getParsedRequestPath(ServletRequest request) {
RequestPath path = (RequestPath) request.getAttribute(PATH_ATTRIBUTE);
Assert.notNull(path, () -> "Expected parsed RequestPath in request attribute \"" + PATH_ATTRIBUTE + "\".");
return path;
}
good day