在将a模块迁移到spring boot项目下、使用embeded tomcat启动项目后,在调用RESTfule接口时,模块中声明的一个SpringMVC拦截器"cn.xxx.thread.common.web.speedctrlforuser.SpeedctrlForUserInterceptor"中抛出了ClassCastException。但是使用外置Tomcat启动就没有这个问题。在逐行debug后发现是spring boot缺失一项配置导致了这个问题。
在 TECHSTUDY-91 - THREAD模块接入服务注册/订阅服务 进行中 任务中,我为a模块定义了一个启动类(注解了@SpringBootApplication),并配置了对应的application.properties。由于目前只需要注册到eureka上,配置文件中只有如下两行配置:
applictaion.properties
spring.application.name=a
eureka.client.serviceUrl.defaultZone=http://10.255.33.207:8080/eureka,http://10.255.33.208:8080/eureka,http://10.255.33.209:8080/eureka
在其它配置(如maven依赖关系、xml配置文件引入等)都整理好之后,用eclipse将a模块发布到tomcat上(即打成war包后发布),调用auth模块接口(如http://localhost:8080/a/rest/users/admin),一切正常。
但是,在使用启动类将模块发布到内置tomcat上(相当于打成jar包后发布),再调用上述auth模块的接口,会出现以下异常:
17:52:31,864 ERROR [org.apache.juli.logging.DirectJDKLog.log] (http-nio-8080-exec-2) Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod] with root cause^|TraceId.-http-nio-8080-exec-2
java.lang.ClassCastException: org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod
at cn.xxx.thread.common.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.processRequest(FrameworkServlet.java:970)
从上文的异常信息可知,问题出现在SpeedctrlForUserInterceptor的第66行。这里的代码是这样的:
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws TooManyRequestsException {
User user = SecurityUtils.getUserFromPrincipal(SecurityContextHolder
.getContext().getAuthentication());
if (user == null) {
return true;
}
HandlerMethod method = (HandlerMethod) handler; // 这里是第66行
// 省略后续代码
}
在第66行,代码中做了一个强制类型转换。根据异常信息,在这里得到的handler是一个ResourceHttpRequestHandler,而不是HandlerMethod。所以会报错。
这里的ResourceHttpRequestHandler和HandlerMethod分别是什么呢?我们可以简单的看一下二者的Javadoc。
org.springframework.web.servlet.resource.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.springframework.web.method.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.
简单的说,ResourceHttpRequestHandler是用来处理静态资源的;而HandlerMethod则是springMVC中用@Controller声明的一个bean及对应的处理方法。以http://localhost:8080/a/rest/users/admin这个接口为例,它对应的HandlerMethod应该指向这个类的这个方法:
@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把一个非静态资源识别成了静态资源,并了调用静态资源处理器?
这里尝试了好几种方案。实际上只有最后的方案是可行的。不过前面几种方案也记录了一下。
那个接口怎么着也不是一个静态资源啊。所以我一开始认为是拦截器的配置有问题。于是我看了一下它的配置,发现确实与别的拦截器不一样:
<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.thread.common.interceptor.LoginUserInterceptor" />
mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/rest/**" />
<bean class="cn.xxx.thread.common.web.speedcontrol.SpeedControlInterceptor" />
mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean class="cn.xxx.thread.common.web.speedctrlforuser.SpeedctrlForUserInterceptor" />
mvc:interceptor>
mvc:interceptors>
于是我先后做了两次调整:把SpeedctrlForUserInterceptor拦截器的
都没起作用。当然都不起作用。这段配置一直在线上正常运行;用war包发布到tomcat上也不报错。说明问题并不在这里。修改这段配置当然不会起作用。
既然问题只在使用embeded tomcat发布时出现,那么多半是它的配置上的问题了。
于是我又查了一下,发现tomcat有一个defaultServlet,用于处理一些静态资源。并且我在外置tomcat的web.xml中也确实发现了这个配置:
<servlet>
<servlet-name>defaultservlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServletservlet-class>
<init-param>
<param-name>debugparam-name>
<param-value>0param-value>
init-param>
<init-param>
<param-name>listingsparam-name>
<param-value>falseparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>defaultservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
难道是内置tomcat没有显式开启这个servlet导致的?我尝试着在spring-servlet-common.xml中增加了一个配置:
<mvc:default-servlet-handler/>
加上配置之后,还是不起作用。当然不起作用。从注释上看,它的作用是增加一个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 = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 这里是第940行
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
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, mappedHandler.getHandler());
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()) {
logger.trace(
"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 matches = new ArrayList();
List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
lookupPath + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().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模块下类似,只是具体路径不同)。
为什么使用外置tomcat启动时,工程名a不会被识别为URI呢?因为当我们使用eclipse将a发布到tomcat中时,eclipse会自动向tomcat的server.xml中写入一行配置:
<Context docBase="a" path="/a" reloadable="true" source="org.eclipse.jst.jee.server:a"/>Host>
其中的path属性,就指定了这个项目的context-path是/a。因而,在将URL(protocol://host:port/context-path/URI?queryString)解析为URI时,SpringMVC能够得到正确的结果。
即使不手动处理server.xml(tomcat官方也并不推荐手动处理server.xml),用war包/文件夹方式发布web项目时,tomcat也会自动将路径名解析为context-path。
但是使用内置tomcat启动时,由于项目的application.properties中没有相关配置,因而context-path默认被指定为“/”。进而,在解析URL时,"protocal://host:port/"后、"?queryString"前的全部字符串都被当做了URI。
前文提出的两个问题(为什么springMVC把一个非静态资源识别成了静态资源,并了调用静态资源处理器?内置tomcat情况下,为什么下标为1的实例(RequestMappingHandlerMapping)没能正确处理这个请求?)都是这个原因导致的。
知道了真正的原因之后,方案就非常简单了:在application.properties中指定context-path即可:
server.contextPath=/a
spring.application.name=a
eureka.client.serviceUrl.defaultZone=http://10.255.33.207:8080/eureka,http://10.255.33.208:8080/eureka,http://10.255.33.209:8080/eureka
迎刃而解。
在trouble shooting时,首先,你得找到一个对象真正的问题原因。“我猜我猜我猜猜猜”这种方法,可以在动手之初用来缩小排查范围;但是要解决问题、积累知识,还是要知其所以然。
使用debug逐行跟进这种方式,一开始我是拒绝的。因为线上环境的问题、包括测试环境的问题,基本上都是无法debug的。所以我一直推荐用日志来做trouble shooting。不过框架内的bug,这类问题比较bug,不用debug模式基本上是没法debug的。
类似spring boot这种自动化配置(还有一些约定大于配置的“半自动化配置”),确实能够节约很多开发时间、精力。但是,如果对其中一些“默认配置”、“自动配置”、“约定值”没有了解,很容易出问题,而且出了问题还不知道什么原因。所以,还是要知其所以然。
本文转自 斯然在天边 51CTO博客,原文链接:http://blog.51cto.com/winters1224/2049425,如需转载请自行联系原作者