XMLBeansでUnexpected end of file after null

これまで快調に動いていたアプリケーションが、唐突に例外を吐くようになった。よりにもよって納品間近のこのタイミングで…!!

org.apache.xmlbeans.XmlException: error: Unexpected end of file after null
        at org.apache.xmlbeans.impl.store.Locale$SaxLoader.load(Locale.java:3486)
        at org.apache.xmlbeans.impl.store.Locale.parseToXmlObject(Locale.java:1276)
        at org.apache.xmlbeans.impl.store.Locale.parseToXmlObject(Locale.java:1263) 
        at org.apache.xmlbeans.impl.schema.SchemaTypeLoaderBase.parse(SchemaTypeLoaderBase.java:345)

...

caused by: org.xml.sax.SAXParseException: Unexpected end of file after null
        at org.apache.xmlbeans.impl.piccolo.xml.Piccolo.reportFatalError(Piccolo.java:1038)
        at org.apache.xmlbeans.impl.piccolo.xml.Piccolo.parse(Piccolo.java:723)
        at org.apache.xmlbeans.impl.store.Locale$SaxLoader.load(Locale.java:3454)

クライアントから送られてきているはずのXMLドキュメントを、サーバ側で解析する、ということをやっているのだが、なぜか失敗する。

MyDataDocument document = MyDataDocument.Factory.parse(request.getInputStream());

なんか変なことしてるかぁ?
この例外、基本的に解析しようとしたXMLドキュメントが空っぽだったときなんかに投げられた気がする。しかし、ログを見ている限りでは正しいXMLドキュメントを受け取っているようだ。しかも、この例外はthrowされるときとされない時がある。わけがわからん。

ここに情報があった。

Thus the first request is OK, the second one gives this exception, the third one is OK again, the fourth one again gives this exception etc.

After some investigation, I have tracked down the problem to Piccolo which only closes the InputStream of the previous parse when doing a new parse,

Exception "Unexpected end of file after null"

なるほど。

サーバはリクエストからXMLドキュメントを受け取る際、当然ServletRequestのInputStreamを利用する。で、XMLBeansのparse()はInputStreamを受け取れるのでそのまま渡すのが普通だ。しかし、XMLBeansが内部で使っているPiccoloという代物が、これを内部で勝手にclose()してしまうらしい。ServletRequestのInputStreamはサーブレットコンテナが管理しているので、アプリケーションで勝手にclose()してはいかんのだが…。

Piccoloは新しく解析を始める際、前回の解析で使ったInputStreamをclose()している。これは前回と今回で渡されたInputStreamが別物であるという前提の動作であるが、確かに同じストリームを二回連続で解析するようなことはないだろうから、それで問題無いのだろう。

だが、今回のケースではよろしくない。

どうやらTomcatやJettyは、パフォーマンス向上を狙ってか別々のリクエストで同じストリームを使い回しているらしいのである。そのため、前回の解析で使った、もう用済みのはずのInputStreamをclose()しているつもりが、実はこれから読み込もうとしているInputStreamをclose()していることになってしまう。一度close()したInputStreamからデータを取ろうと思っても、すでに中身はクリアされていて何も得られない。結果、XMLBeansはデータが無いよ!と例外を投げてくる。

この問題を回避するためには、ServletRequestのInputStreamをXMLBeansに渡さなければ良い、ということになる。つまり、中身をあらかじめ別のInputStreamなり文字列なりbyte配列なりにダンプしておいて、それをXMLBeansに渡すのである。

違う対処法もある。

I found an elegant solution to this issue of InputStream cleanup. Apache Commons IO released v1.4 which includes an AutoCloseInputStream wrapper which cleans up InputStreams once they have been read.

'[jira] Commented: (XMLBEANS-226) Exception "Unexpected end of file' - MARC

Commons IO 1.4に含まれるAutoCloseInputStreamを使うのである。AutoCloseInputStreamは、Javadocによれば入力が終わった時点で即座にcloseされるInputStream、というものらしい。これでInputStreamをラッピングしてやるのである。

MyDataDocument document = MyDataDocument.Factory.parse(
    new AutoCloseInputStream(request.getInputStream()));

すると、XMLBeansが全てのデータを取り出した時点でInputStreamは自動的にclose()される。そうすると、サーブレットコンテナ側もInputStreamの使い回しを断念して次のリクエストでは新しいInputStreamを作り直す。これにて、意図しないタイミングでclose()されてしまう問題は解決である。理解が怪しいのだが、たぶんそういうことだろう。実際、この手法を使うと問題は再現しなくなる。しかし、これだとせっかくサーブレットコンテナ側が行っている工夫を台無しにしてしまうのが問題。

そんなわけで、今回は前者の解決策をとることにした。いやぁ、さすがに冷や汗が出たわ。ありがとう、賢い人たち。