一个用户在使用tomcat7054版本启动的时候遇到的错误:java
Caused by: java.lang.IllegalStateException: Unable to complete the scan for annotations for web application [/test] due to a StackOverflowError. Possible root causes include a too low setting for -Xss and illegal cyclic inheritance dependencies. The class hierarchy being processed was [org.jaxen.util.AncestorAxisIterator-> org.jaxen.util.AncestorOrSelfAxisIterator-> org.jaxen.util.AncestorAxisIterator] at org.apache.catalina.startup.ContextConfig.checkHandlesTypes(ContextConfig.java:2112) at org.apache.catalina.startup.ContextConfig.processAnnotationsStream(ContextConfig.java:2059) at org.apache.catalina.startup.ContextConfig.processAnnotationsJar(ContextConfig.java:1934) at org.apache.catalina.startup.ContextConfig.processAnnotationsUrl(ContextConfig.java:1900) at org.apache.catalina.startup.ContextConfig.processAnnotations(ContextConfig.java:1885) at org.apache.catalina.startup.ContextConfig.webConfig(ContextConfig.java:1317) at org.apache.catalina.startup.ContextConfig.configureStart(ContextConfig.java:876) at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:374) at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117) at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90) at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5355) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)web
这是在tomcat解析servlet3注释时进行类扫描的过程,发现了两个类的继承关系存在循环继承的状况而致使了栈溢出。排查了一下,是由于应用所依赖的 dom4j-1.1.jar 里存在 AncestorAxisIterator 和子类 AncestorOrSelfAxisIterator:apache
% javap org.jaxen.util.AncestorAxisIterator Compiled from "AncestorAxisIterator.java" public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.StackedIterator { protected org.jaxen.util.AncestorAxisIterator(); public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator); protected java.util.Iterator createIterator(java.lang.Object); } % javap org.jaxen.util.AncestorOrSelfAxisIterator Compiled from "AncestorOrSelfAxisIterator.java" public class org.jaxen.util.AncestorOrSelfAxisIterator extends org.jaxen.util.AncestorAxisIterator { public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator); protected java.util.Iterator createIterator(java.lang.Object); }
同时应用所依赖的 sourceforge.jaxen-1.1.jar 里面也存在这两个同名类,但继承关系正好相反:tomcat
% javap org.jaxen.util.AncestorAxisIterator Compiled from "AncestorAxisIterator.java" public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.AncestorOrSelfAxisIterator { public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator); } % javap org.jaxen.util.AncestorOrSelfAxisIterator Compiled from "AncestorOrSelfAxisIterator.java" public class org.jaxen.util.AncestorOrSelfAxisIterator implements java.util.Iterator { public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator); public boolean hasNext(); public java.lang.Object next(); public void remove(); }
简单的说,在第1个jar里存在 B继承自A,在第2个jar里存在同名的A和B,但倒是A继承自B。其实也能运行的,只是可能出现类加载时可能加载的不必定是你想要的那个,但tomcat作类型检查的时候把这个当成了一个环。app
在ContextConfig.processAnnotationsStream方法里,每次解析以后要对类型作一次检测,而后才获取注释信息:less
ClassParser parser = new ClassParser(is, null); JavaClass clazz = parser.parse(); checkHandlesTypes(clazz); ... AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries(); ...
再看这个用来检测类型的checkHandlesTypes方法里面:dom
populateJavaClassCache(className, javaClass); JavaClassCacheEntry entry = javaClassCache.get(className); if (entry.getSciSet() == null) { try { populateSCIsForCacheEntry(entry); // 这里 } catch (StackOverflowError soe) { throw new IllegalStateException(sm.getString( "contextConfig.annotationsStackOverflow",context.getName(), classHierarchyToString(className, entry))); } }
每次新解析出来的类(tomcat里定义了JavaClass来描述),会被populateJavaClassCache放入cache,这个cache内部是个Map,因此对于key相同的会存在把之前的值覆盖了的状况,这个“环形继承”的现象就比较好解释了。设计
Map里的key是String类型即类名,value是JavaClassCacheEntry类型封装了JavaClass及其父类和接口信息。咱们假设第一个jar里B继承自A,它们被放入cache的时候键值对是这样的:code
"A" -> [JavaClass-A, 父类Object,父接口]" "B" -> [JavaClass-B, 父类A,父接口]orm
而后当解析到第2个jar里的A的时候,覆盖了以前A的键值对,变成了:
"A" -> [JavaClass-A, 父类B,父接口] "B" -> [JavaClass-B, 父类A,父接口]
这2个的继承关系在这个cache里被描述成了环状,而后在接下来的populateSCIsForCacheEntry方法里找父类的时候就绕不出来了,最终致使了栈溢出。
这个算是cache设计不太合理,没有考虑到不一样jar下面有相同的类的状况。问题确认以后,让应用方去修正本身的依赖就能够了,但应用方说以前在7026的时候,是能够正常启动的。这就有意思了,接着一番排查以后,发如今7026版本里,ContextConfig.webConfig的时候先判断了一下web.xml里的版本信息,若是版本>=3才会去扫描类里的servlet3注释信息。
// Parse context level web.xml InputSource contextWebXml = getContextWebXmlSource(); parseWebXml(contextWebXml, webXml, false); if (webXml.getMajorVersion() >= 3) { // 扫描jar里的web-fragment.xml 和 servlet3注释信息 ... }
而在7054版本里是没有这个判断的。搜了一下,发现是在7029这个版本里去掉的这个判断。在7029的changelog里:
As per section 1.6.2 of the Servlet 3.0 specification and clarification from the Servlet Expert Group, the servlet specification version declared in web.xml no longer controls if >Tomcat scans for annotations. Annotation scanning is now always performed – regardless of the version declared in web.xml – unless metadata complete is set to true.
以前对servlet3规范理解不够清晰;之因此改,是由于在web.xml里定义的servlet版本,再也不控制tomcat是否去扫描每一个类里的注释信息。也就是说无论web.xml里声明的servlet版本是什么,都会进行注释扫描,除非metadata-complete属性设置为true(默认是false)。
因此在7029版本以后改成了判断 webXml.isMetadataComplete() 是否须要进行扫描注释信息。