记录⼀次debug-⾃定义SpringMVC中HandlerMethod类型转换问题。。。
摘要
在将a模块迁移到spring boot项⽬下、使⽤embeded tomcat启动项⽬后,在调⽤RESTfule接⼝时,模块中声明的⼀个SpringMVC拦截
器"cn.xxx.threadmon.web.speedctrlforuser.SpeedctrlForUserInterceptor"中抛出了ClassCastException。但是使⽤外置Tomcat启动就没有这个问题。在逐⾏debug后发现是
spring boot缺失⼀项配置导致了这个问题。
问题
在 TECHSTUDY-91 - THREAD模块接⼊服务注册/订阅服务进⾏中任务中,我为a模块定义了⼀个启动类(注解了@SpringBootApplication),并配置了对应的
application.properties。由于⽬前只需要注册到eureka上,配置⽂件中只有如下两⾏配置:
17:52:31,864 ERROR [org.apache.juli.logging.DirectJDKLog.log] (http-nio-8080-exec-2) Servlet.servic
e() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastEx java.lang.ClassCastException: org.springframework.source.ResourceHttpRequestHandler cannot be cast to org.hod.HandlerMethod
hreadmon.web.speedctrlforuser.SpeedctrlForUserInterceptor.preHandle(SpeedctrlForUserInterceptor.java:66) ~[classes/:?]
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:133) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:962) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.proce***equest(FrameworkServlet.java:970)
分析
从上⽂的异常信息可知,问题出现在SpeedctrlForUserInterceptor的第66⾏。这⾥的代码是这样的:
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws TooManyRequestsException {
User user = UserFromPrincipal(SecurityContextHolder
.getContext().getAuthentication());
if (user == null) {
return true;
}
HandlerMethod method = (HandlerMethod) handler; // 这⾥是第66⾏
/
/ 省略后续代码
}
在第66⾏,代码中做了⼀个强制类型转换。根据异常信息,在这⾥得到的handler是⼀个ResourceHttpRequestHandler,⽽不是HandlerMethod。所以会报错。
这⾥的ResourceHttpRequestHandler和HandlerMethod分别是什么呢?我们可以简单的看⼀下⼆者的Javadoc。
org.springframework.source.ResourceHttpRequestHandler
HttpRequestHandler that serves static resources in an optimized way according to the guidelines of Page Speed, YSlow, etc.
The "locations" property takes a list of Spring Resource locations from which static resources are allowed to be served by this handler. Resources could be served
from a classpath location, e.g. "classpath:/META-INF/public-web-resources/", allowing convenient packaging and serving of resources such as .js, .css, and others in
jar files.
This request handler may also be configured with a resourcesResolver and resourceTransformer chains to support arbitrary resolution and transformation of
resources being served. By default a PathResourceResolver simply finds resources based on the configured "locations". An application can configure additional
resolvers and transformers such as the VersionResourceResolver which can resolve and prepare URLs for resources with a version in the URL.
This handler also properly evaluates the Last-Modified header (if present) so that a 304 status code will be returned as appropriate, avoiding unnecessary overhead
for resources that are already cached by the client.
HandlerMethod
org.hod.HandlerMethod
Encapsulates information about a handler method consisting of a method and a bean. Provides convenient access to method parameters, the method return value,
method annotations, etc.
The class may be created with a bean instance or with a bean name (e.g. lazy-init bean, prototype bean). Use createWithResolvedBean() to obtain a HandlerMethod
instance with a bean instance resolved through the associated BeanFactory.
@Controller@RequestMapping("/rest/users")
public class UserRESTController extends AbstractController
{
@PreAuthorize("hasRole('USER_DETAIL')")
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
@ResponseBody
public User getUserByID(@PathVariable String id) throws InvalidDataException {
// 省略具体代码
}
// 省略其它⽅法
}
所以这个问题的核⼼是:为什么springMVC把⼀个⾮静态资源识别成了静态资源,并了调⽤静态资源处理器?
⽅案
这⾥尝试了好⼏种⽅案。实际上只有最后的⽅案是可⾏的。不过前⾯⼏种⽅案也记录了⼀下。
⽅案⼀:修改springMVC配置
那个接⼝怎么着也不是⼀个静态资源啊。所以我⼀开始认为是的配置有问题。于是我看了⼀下它的配置,发现确实与别的不⼀样:
<mvc:interceptors>
<!-- ⼀种配置是这样的:拦截所有请求,但过滤掉静态资源 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/js/**" />
<mvc:exclude-mapping path="/html/**" />
<mvc:exclude-mapping path="/resources/**" />
<bean class="cn.xxx.threadmon.interceptor.LoginUserInterceptor" />
</mvc:interceptor>
<!-- ⼀种配置是这样的:只拦截REST请求。 -->
<mvc:interceptor>
<mvc:mapping path="/rest/**" />
<bean class="cn.xxx.threadmon.web.speedcontrol.SpeedControlInterceptor" />
</mvc:interceptor>
<!-- 出问题的是这样的:拦截所有请求,并且不过滤静态资源 -->
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="cn.xxx.threadmon.web.speedctrlforuser.SpeedctrlForUserInterceptor" />
</mvc:interceptor>
<!-- 省略其它配置,与第⼀、第⼆种⼤同⼩异 -->
</mvc:interceptors>
于是我先后做了两次调整:把SpeedctrlForUserInterceptor的<mvc:mapping />配置改成<mvc:mapping path="/rest/**" />;把SpeedctrlForUserInterceptor的顺序调
整为第⼀位。
都没起作⽤。当然都不起作⽤。这段配置⼀直在线上正常运⾏;⽤war包发布到tomcat上也不报错。说明问题并不在这⾥。修改这段配置当然不会起作⽤。
⽅案⼆:检查内置tomcat配置
既然问题只在使⽤embeded tomcat发布时出现,那么多半是它的配置上的问题了。
于是我⼜查了⼀下,发现tomcat有⼀个defaultServlet,⽤于处理⼀些静态资源。并且我在外置tomcat的l中也确实发现了这个配置:
<!-- The default servlet for all web applications, that serves static -->
<!-- resources. It processes all requests that are not mapped to other -->
<!-- servlets with servlet mappings (defined either here or in your own -->
<!-- l file). This servlet supports the following initialization -->
<!-- parameters (default values are in square brackets): -->
<!-- 省略后⾯的注释 -->
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
难道是内置tomcat没有显式开启这个servlet导致的?我尝试着在l中增加了⼀个配置:
<!-- 这是增加的配置 -->
<mvc:default-servlet-handler/>
<!-- 官⽅提供的注释如下:Element : default-servlet-handlerConfigures a handler for serving static resources by
forwarding to the Servlet container's default Servlet. Use of this handler allows using a "/" mapping with the
DispatcherServlet while still utilizing the Servlet container to serve static resources. This handler will forward all
requests to the default Servlet. Therefore it is important that it remains last in the order of all other URL
HandlerMappings. That will be the case if you use the "annotation-driven" element or alternatively if you are setting up
your customized HandlerMapping instance be sure to set its "order" property to a value lower than that of the
DefaultServletHttpRequestHandler, which is Integer.MAX_VALUE. -->
加上配置之后,还是不起作⽤。当然不起作⽤。从注释上看,它的作⽤是增加⼀个handler,在识别出静态资源之后将请求转发给容器提供的default servlet。然⽽我遇到的问题是,springMVC在识别静态资源上出现了误判。加这个配置当然不会起作⽤。
顺带⼀提,我后来debug时发现,内置tomcat同样会注册default servlet。在这⼀点上,内置、外置没有区别。
⼆次分析:先搞清楚问题究竟在哪⼉
上⾯两个⽅案,其实都是建⽴在“推测问题原因”上的。换句话说就是“我猜我猜我猜猜”。初步分析时可以使⽤这种⽅法;但由于它对问题原因的分析很不到位,所以再怎么调整、修改也改不到点⼦上。
所以在拿出⽅案三之前,我打算祭出最后的法宝,先把病因搞清楚再开⽅⼦拿药。
这个法宝就是:开debug模式,逐⾏执⾏代码。⽽且在这个问题中,由于外置tomcat能够正常执⾏,因此,还可以⽤正常情况下的运⾏时数据来与出错情况做对⽐。
第⼀个断点
第⼀个断点打在哪⼉?分析异常信息可以发现,异常抛出位置是DispatcherServlet.doDispatch(DispatcherServlet.java:962)。这个⽅法的代码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = AsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);spring mvc和boot区别
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || Handler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = Handler()); // 这⾥是第940⾏
// Process last-modified header, if supported by the handler.
String method = Method();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, Handler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) { // 这⾥是第962⾏
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, Handler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
第962⾏执⾏了mappedHandler.applyPreHandle(processedRequest, response),⽽其中的mappedHandler来⾃第940的mappedHandler = getHandler(processedRequest);。这个getHandler⽅法的代码如下:
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
HandlerExecutionChain handler = hm.getHandler(request); // 这⾥是第1160⾏
if (handler != null) {
return handler
}
}
return null;
}
可以很清楚地看出:这段代码就是SpringMVC决定使⽤哪个Handler来处理当前Request的地⽅。因此,我把第⼀个断点打在了第1160⾏(getHandler⽅法中HandlerExecutionChain handler = hm.getHandler(request);这⼀句上)。⼀来检查内置/外置tomcat下,SpringMVC⽣成的handlerMappings是否有不同;⼆来检查两种情况下,SpringMVC分别由哪个HandlerMapping来处理request并⽣成HandlerExecutionChain。
执⾏结果的图我就不贴了。结论是这样的:两种tomcat下,handlerMappings中都有9个HandlerMapping的⽰例,并且两种情况下列表中的类、顺序都是⼀样的。但是,外置tomcat下,是下标为1的实例(RequestMappingHandlerMapping)处理了请求、并返回了⼀个HandlerMethod实例;⽽内置tomcat中,是下标为5的实例(SimpleUrlHandlerMapping)来处理请求,并返回了⼀个ResourceHttpRequestHandler实例!⽽正是这个ResourceHttpRequestHandler,在代码中强转HandlerMthod时抛出了异常。
因此,我们可以将问题聚焦为:内置tomcat情况下,为什么下标为1的实例(RequestMappingHandlerMapping)没能正确处理这个请求?
第⼆个断点
但是,虽然我们可以确定问题出现在RequestMappingHandlerMapping这个类中,但通过分析代码可以发现,getHandler⽅法的流程并没有进⼊这个类中,⽽是由它的⽗类(AbstractHandlerMethodMapping/AbstractHandlerMapping)定义的⽅法处理了。
sequenceDiagram
DispatcherServlet->>AbstractHandlerMapping: getHandler(request)
AbstractHandlerMapping->> AbstractHandlerMethodMapping: getHandlerInternal(request)
AbstractHandlerMethodMapping->>AbstractHandlerMethodMapping: lookupHandlerMehtod(lookupPath,request)
AbstractHandlerMethodMapping->>AbstractHandlerMapping: return HandlerMethod
AbstractHandlerMapping->>DispatcherServlet: return HandleExecutionChain
最关键的⽅法是AbstractHandlerMethodMapping.lookupHandlerMethod( String lookupPath, HttpServletRequest request),其代码如下:
/**
* Look up the best-matching handler method for the current request.
* If multiple matches are found, the best match is selected.
* @param lookupPath mapping lookup path within the current servlet mapping
* @param request the current request
* @return the best-matching handler method, or {@code null} if no match
* @see #handleMatch(Object, String, HttpServletRequest)
* @see #handleNoMatch(Set, String, HttpServletRequest)
*/
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
List<T> directPathMatches = MappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through
addMatchingMappings(Mappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
lookupPath + "] : " + matches);
}
Match bestMatch = (0);
if (matches.size() > 1) {
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = (1);
if (comparatorpare(bestMatch, secondBestMatch) == 0) {
Method m1 = Method();
Method m2 = Method();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(Mappings().keySet(), lookupPath, request);
}
}
SpringMVC⽤这个⽅法来将请求路径(⼊参lookupPath)匹配到已注册的handler上。于是我在这个⽅法的⼊⼝处加了个断点,在内置/外置tomcat下逐步执⾏后,发现了⽞机:外置tomcat下,directPathMatches不为空;⽽内置tomcat下,directPathMatches是⼀个EmptyList,这⼜进⼀步导致了matches是⼀个EmptyList,并使得最终的返回值是null。可以不⽤打第三个断点了。细致⼀点就能发现:内置tomcat下,lookupPath的值是"/a/rest/users",⽽外置tomcat下则是"/rest/users"。⽽⽆论使⽤内置/外置
tomcat,MappingRegistry中保存的urlPath,都是"/rest/xxxx"格式的。⽤toString()⽅法打印出来的话,基本是这样的:"/rest/dirtyUpload/clean=
[{[/rest/dirtyUpload/clean],methods=[GET]}], /{path}=[{[/{path}],methods=[GET]}], /=[{[/],methods=[GET]}], /rest/server/time=[{[/rest/server/time]}], ……"(这些mapping是c模块下的;a模块下类似,只是具体路径不同)。
context-path
为什么使⽤外置tomcat启动时,⼯程名a不会被识别为URI呢?因为当我们使⽤eclipse将a发布到tomcat中时,eclipse会⾃动向tomcat的l中写⼊⼀⾏配置:
<Context docBase="a" path="/a" reloadable="true" source="lipse.jst.jee.server:a"/></Host>
其中的path属性,就指定了这个项⽬的context-path是/a。因⽽,在将URL(protocol://host:port/context-path/URI?queryString)解析为URI时,SpringMVC能够得到正确的结果。
即使不⼿动处理l(tomcat官⽅也并不推荐⼿动处理l),⽤war包/⽂件夹⽅式发布web项⽬时,tomcat也会⾃动将路径名解析为context-path。
但是使⽤内置tomcat启动时,由于项⽬的application.properties中没有相关配置,因⽽context-path默认被指定为“/”。进⽽,在解析URL时,"protocal://host:port/"后、"? queryString"前的全部字符串都被当做了URI。
前⽂提出的两个问题(为什么springMVC把⼀个⾮静态资源识别成了静态资源,并了调⽤静态资源处理器?内置tomcat情况下,为什么下标为1的实例(RequestMappingHandlerMapping)没能正确处理这个请求?)都是这个原因导致的。
⽅案三:指定context-path
知道了真正的原因之后,⽅案就⾮常简单了:在application.properties中指定context-path即可:
迎刃⽽解。
⼩结
在trouble shooting时,⾸先,你得到⼀个对象真正的问题原因。“我猜我猜我猜猜猜”这种⽅法,可以在动⼿之初⽤来缩⼩排查范围;但是要解决问题、积累知识,还是要知其所以然。
使⽤debug逐⾏跟进这种⽅式,⼀开始我是拒绝的。因为线上环境的问题、包括测试环境的问题,基本上都是⽆法debug的。所以我⼀直推荐⽤⽇志来做trouble shooting。不过框架内的bug,这类问题⽐较bug,不⽤debug模式基本上是没法debug的。
类似spring boot这种⾃动化配置(还有⼀些约定⼤于配置的“半⾃动化配置”),确实能够节约很多开发时间、精⼒。但是,如果对其中⼀些“默认配置”、“⾃动配置”、“约定值”没有了解,很容易出问题,⽽且出了问题还不知道什么原因。所以,还是要知其所以然。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论