最近在公司更新一個老項目的時候,發現部署項目后tomcat報錯,錯誤如下:
1 Caused by: java.lang.IllegalStateException: 2 Unable to complete the scan for annotations for web application [/test] 3 due to a StackOverflowError. Possible root causes include a too low setting 4 for -Xss and illegal cyclic inheritance dependencies. 5 The class hierarchy being processed was 6 [org.jaxen.util.AncestorAxisIterator-> 7 org.jaxen.util.AncestorOrSelfAxisIterator-> 8 org.jaxen.util.AncestorAxisIterator] 9 at org.apache.catalina.startup.ContextConfig.checkHandlesTypes(ContextConfig.java:2112) 10 at org.apache.catalina.startup.ContextConfig.processAnnotationsStream(ContextConfig.java:2059) 11 at org.apache.catalina.startup.ContextConfig.processAnnotationsJar(ContextConfig.java:1934) 12 at org.apache.catalina.startup.ContextConfig.processAnnotationsUrl(ContextConfig.java:1900) 13 at org.apache.catalina.startup.ContextConfig.processAnnotations(ContextConfig.java:1885) 14 at org.apache.catalina.startup.ContextConfig.webConfig(ContextConfig.java:1317) 15 at org.apache.catalina.startup.ContextConfig.configureStart(ContextConfig.java:876) 16 at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:374) 17 at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117) 18 at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90) 19 at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5355) 20 at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
這是在tomcat解析servlet3注釋時進行類掃描的過程,發現了兩個類的繼承關系存在循環繼承的情況而導致了棧溢出。
排查了一下,是因為應用所依賴的 dom4j-1.1.jar 里存在 AncestorAxisIterator 和子類 AncestorOrSelfAxisIterato。
1 % javap org.jaxen.util.AncestorAxisIterator 2 Compiled from "AncestorAxisIterator.java" 3 public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.StackedIterator { 4 protected org.jaxen.util.AncestorAxisIterator(); 5 public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator); 6 protected java.util.Iterator createIterator(java.lang.Object); 7 } 8 % javap org.jaxen.util.AncestorOrSelfAxisIterator 9 Compiled from "AncestorOrSelfAxisIterator.java" 10 public class org.jaxen.util.AncestorOrSelfAxisIterator extends org.jaxen.util.AncestorAxisIterator { 11 public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator); 12 protected java.util.Iterator createIterator(java.lang.Object); 13 }
同時應用所依賴的 sourceforge.jaxen-1.1.jar 里面也存在這兩個同名類,但繼承關系正好相反:
1 % javap org.jaxen.util.AncestorAxisIterator 2 Compiled from "AncestorAxisIterator.java" 3 public class org.jaxen.util.AncestorAxisIterator extends org.jaxen.util.AncestorOrSelfAxisIterator { 4 public org.jaxen.util.AncestorAxisIterator(java.lang.Object, org.jaxen.Navigator); 5 } 6 % javap org.jaxen.util.AncestorOrSelfAxisIterator 7 Compiled from "AncestorOrSelfAxisIterator.java" 8 public class org.jaxen.util.AncestorOrSelfAxisIterator implements java.util.Iterator { 9 public org.jaxen.util.AncestorOrSelfAxisIterator(java.lang.Object, org.jaxen.Navigator); 10 public boolean hasNext(); 11 public java.lang.Object next(); 12 public void remove(); 13 }
簡單的說,在第1個jar里存在B繼承自A,在第2個jar里存在同名的A和B,但卻是A繼承自B。其實也能運行的,只是可能出現類加載時可能加載的不一定是你想要的那個,但tomcat做類型檢查的時候把這個當成了一個環。
在ContextConfig.processAnnotationsStream方法里,每次解析之后要對類型做一次檢測,然后才獲取注釋信息:
1 ClassParser parser = new ClassParser(is, null); 2 JavaClass clazz = parser.parse(); 3 checkHandlesTypes(clazz); 4 ... 5 AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries(); 6 ...
再看這個用來檢測類型的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內部是個Map,所以對於key相同的會存在把以前的值覆蓋了的情況,這個“環形繼承”的現象就比較好解釋了。
Map里的key是String類型即類名,value是JavaClassCacheEntry類型封裝了JavaClass及其父類和接口信息。我們假設第一個jar里B繼承自A,它們被放入cache的時候鍵值對是這樣的:
"A" -> [JavaClass-A, 父類Object,父接口]" "B" -> [JavaClass-B, 父類A,父接口]
然后當解析到第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注釋信息。
1 // Parse context level web.xml 2 InputSource contextWebXml = getContextWebXmlSource(); 3 parseWebXml(contextWebXml, webXml, false); 4 if (webXml.getMajorVersion() >= 3) { 5 // 掃描jar里的web-fragment.xml 和 servlet3注釋信息 6 ... 7 }
而在7054版本里是沒有這個判斷的。搜了一下,發現是在7029這個版本里去掉的這個判斷。在7029的changelog里:
1 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() 是否需要進行掃描注釋信息。