一個用戶在使用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() 是否須要進行掃描註釋信息。