Tomcat啓動時檢測到循環繼承而棧溢出的問題

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

相關文章
相關標籤/搜索