天天看點

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

一個使用者在使用tomcat7054版本啟動的時候遇到的錯誤:

這是在tomcat解析servlet3注釋時進行類掃描的過程,發現了兩個類的繼承關系存在循環繼承的情況而導緻了棧溢出。

排查了一下,是因為應用所依賴的 dom4j-1.1.jar 裡存在 <code>ancestoraxisiterator</code> 和子類 <code>ancestororselfaxisiterato</code>

<code></code>

同時應用所依賴的 sourceforge.jaxen-1.1.jar 裡面也存在這兩個同名類,但繼承關系正好相反:

簡單的說,在第1個jar裡存在 b繼承自a,在第2個jar裡存在同名的a和b,但卻是a繼承自b。其實也能運作的,隻是可能出現類加載時可能加載的不一定是你想要的那個,但tomcat做類型檢查的時候把這個當成了一個環。

在<code>contextconfig.processannotationsstream</code>方法裡,每次解析之後要對類型做一次檢測,然後才擷取注釋資訊:

再看這個用來檢測類型的<code>checkhandlestypes</code>方法裡面:

每次新解析出來的類(tomcat裡定義了javaclass來描述),會被<code>populatejavaclasscache</code>放入cache,這個cache内部是個<code>map</code>,是以對于key相同的會存在把以前的值覆寫了的情況,這個“環形繼承”的現象就比較好解釋了。

<code>map</code>裡的key是<code>string</code>類型即類名,value是<code>javaclasscacheentry</code>類型封裝了<code>javaclass</code>及其父類和接口資訊。我們假設第一個jar裡b繼承自a,它們被放入cache的時候鍵值對是這樣的:

然後當解析到第2個jar裡的a的時候,覆寫了之前a的鍵值對,變成了:

這2個的繼承關系在這個cache裡被描述成了環狀,然後在接下來的<code>populatescisforcacheentry</code>方法裡找父類的時候就繞不出來了,最終導緻了棧溢出。

這個算是cache設計不太合理,沒有考慮到不同jar下面有相同的類的情況。問題确認之後,讓應用方去修正自己的依賴就可以了,但應用方說之前在7026的時候,是可以正常啟動的。這就有意思了,接着一番排查之後,發現在7026版本裡,<code>contextconfig.webconfig</code>的時候先判斷了一下web.xml裡的版本資訊,如果版本<code>&gt;=3</code>才會去掃描類裡的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版本是什麼,都會進行注釋掃描,除非<code>metadata-complete</code>屬性設定為true(預設是false)。

是以在7029版本之後改為了判斷 <code>webxml.ismetadatacomplete()</code> 是否需要進行掃描注釋資訊。