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() 是否须要进行扫描注释信息。

相关文章
相关标签/搜索