一個用戶在使用tomcat7054版本啟動的時候遇到的錯誤:
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)
這是在tomcat解析servlet3注釋時進(jìn)行類掃描的過程航揉,發(fā)現(xiàn)了兩個類的繼承關(guān)系存在循環(huán)繼承的情況而導(dǎo)致了棧溢出奕筐。排查了一下,是因?yàn)閼?yīng)用所依賴的 dom4j-1.1.jar 里存在 AncestorAxisIterator 和子類 AncestorOrSelfAxisIterator:
% 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);
}
同時應(yīng)用所依賴的 sourceforge.jaxen-1.1.jar 里面也存在這兩個同名類,但繼承關(guān)系正好相反:
% 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偎窘。其實(shí)也能運(yùn)行的乌助,只是可能出現(xiàn)類加載時可能加載的不一定是你想要的那個,但tomcat做類型檢查的時候把這個當(dāng)成了一個環(huán)陌知。
在ContextConfig.processAnnotationsStream方法里他托,每次解析之后要對類型做一次檢測,然后才獲取注釋信息:
ClassParser parser = new ClassParser(is, null);
JavaClass clazz = parser.parse();
checkHandlesTypes(clazz);
...
AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
...
再看這個用來檢測類型的checkHandlesTypes方法里面:
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內(nèi)部是個Map,所以對于key相同的會存在把以前的值覆蓋了的情況沿盅,這個“環(huán)形繼承”的現(xiàn)象就比較好解釋了把篓。
Map里的key是String類型即類名,value是JavaClassCacheEntry類型封裝了JavaClass及其父類和接口信息腰涧。我們假設(shè)第一個jar里B繼承自A纸俭,它們被放入cache的時候鍵值對是這樣的:
"A" -> [JavaClass-A, 父類Object,父接口]"
"B" -> [JavaClass-B, 父類A南窗,父接口]
然后當(dāng)解析到第2個jar里的A的時候揍很,覆蓋了之前A的鍵值對郎楼,變成了:
"A" -> [JavaClass-A, 父類B,父接口]
"B" -> [JavaClass-B, 父類A窒悔,父接口]
這2個的繼承關(guān)系在這個cache里被描述成了環(huán)狀呜袁,然后在接下來的populateSCIsForCacheEntry方法里找父類的時候就繞不出來了,最終導(dǎo)致了棧溢出简珠。
這個算是cache設(shè)計(jì)不太合理阶界,沒有考慮到不同jar下面有相同的類的情況。問題確認(rèn)之后聋庵,讓應(yīng)用方去修正自己的依賴就可以了膘融,但應(yīng)用方說之前在7026的時候,是可以正常啟動的祭玉。這就有意思了氧映,接著一番排查之后,發(fā)現(xiàn)在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版本里是沒有這個判斷的振峻。搜了一下臼疫,發(fā)現(xiàn)是在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規(guī)范理解不夠清晰扣孟;之所以改烫堤,是因?yàn)樵趙eb.xml里定義的servlet版本,不再控制tomcat是否去掃描每個類里的注釋信息凤价。也就是說不管web.xml里聲明的servlet版本是什么鸽斟,都會進(jìn)行注釋掃描,除非metadata-complete屬性設(shè)置為true(默認(rèn)是false)料仗。
所以在7029版本之后改為了判斷 webXml.isMetadataComplete() 是否需要進(jìn)行掃描注釋信息湾盗。