最近从一位同学那得知了一个奇怪的异常现象(和classloader相关), 问题一直没有解决, 所以趁周末有空搭建测试环境研究一下:
问题现象
该应用是服务型应用, 如果应用启动时初始化所有的spring bean, 那么会有如下的异常,反之运行时一点一点初始化spring bean则没有任何问题。(所以应用在线上没有使用fail fast模式,有一定风险)
个人分析
- 异常比较底层, 所以把抛出异常的class(DirContextURLStreamHandler)反编译看了下。如下:
- 先了解了下该class的结构和用途:该类中clBindings和threadBindings是普通的map对象, 当执行ServletContext.getResource(resourceName).openConnection()方法后DirContextURLStreamHandler的get方法都会调用(基本可认为只要加载web应用下文件就会使用). 为什么这个方法调用会和异常堆栈联系在一起? 这里简单讲解下tomcat的加载应用中资源的逻辑, 当我们使用ServletContext.getResource获取该资源URL时,你可能看到如下的结果, 因为tomcat 使用了Java命名系统接口(jndi)来命名/操作应用下的资源:
- jndi这个协议的Handler是通过以下语句设置的(WebappLoader.java中):
URL.setURLStreamHandlerFactory(new DirContextURLStreamHandlerFactory); - 而DirContextURLStreamHandlerFactory实现也比较简单,如果url的协议是jndi, 就由DirContextURLStreamHandler(这也是最终抛出异常的类)来进行处理。
从异常中发现,加载webx中的uri.xml时抛出了异常
定位问题
- 尝试在class中增加了断点。当抛出异常时, 上图中115行的result为nulll, 正常情况下result是有值的。
出现异常时clBindings中包含的key是org.jboss.web.tomcat.tc5.WebCtxLoader$ENCLoader, 而currentCL(上下文classloader)是org.jboss.mx.loading.UnifiedClassLoader3, 而导致查询不到结果. 所以我开始怀疑,是否业务代码中将上下文的classloader给改掉了。而导致这里查询不到DirContext(web目录上下文对象). - 用btrace打印了所有调用Thread.setContextClassLoader(ClassLoader classloder)的点, 并且该方法参数的class是org.jboss.mx.loading.UnifiedClassLoader3的线程堆栈. 日志打印出来后,内容很多,但是稍微过滤后(堆栈前5 frame中包含alibaba or taobao关键字),可以最终只剩下几个可疑class:
- com.taobao.hsf.route.strategy.groovy.GroovyRouteRuleParser.parse(GroovyRouteRuleParser.java:103(正常)
这里在操作ContextClassLoader逻辑是: 取出->更改->还原(finally语句块)。
- com.taobao.agoo.open.OpenServiceFactory.init(OpenServiceFactory.java:57)(有问题)
这里在操作ContextClassLoader逻辑是: 取出->更改
解决问题
- 很明显了,只要在OpenServiceFactory.java中增加"还原"的操作就可以了。增加"还原"操作后,应用启动初始化所有bean正常, 且无异常抛出。
问题还原
- OpenServiceFactory更改了ContextClassLoader,但是没有还原原有的ContextClassLoader这种写法很少见(我不确定代码这样写的原有),但这里确实导致了该应用启动时抛出的异常。
- 应用启动抛出异常 真实的原因是 OpenServiceFactory的初始化比较早,导致后面初始化home.webx.xml(还有其他的xml)出现了加载不到的异常。如果OpenServiceFactory初始化比较晚,并且应用不需要加载web下的资源文件,那么也不会有问题。
- 那为什么应用启动时,不初始化OpenServiceFactory就没有问题呢?因为该应用是服务型的应用,启动时已经初始化过了所需要的web目录下资源文件(例如: webx.xml, pipeline.xml等), 没有其他的资源文件需要加载(即使影响也肯能只影响修改了ContextClassLoader的线程), 所以不会导致这个问题出现。
classloader引起的问题很少见,问题出现后也不太容易排查和解决,这个场景相对是比较简单,只是这个异常比较"奇怪"而已。