【Java】XmlReaderのDTD解決を高速化する小技

XmlReaderのDTD解決が遅い

XMLファイルの「オシ」ともいわれているのがDTD。このファイルひとつで、XMLがフォーマット通りであるか検証できるというものだ。”うまく利用すれば”検証ロジックの簡略がができる。

 

このDTD検証、XMLファイルに記述しているURLを使って真面目にHTTPで読み取りに行く仕様。

オーバーヘッドはもちろん、DTDの構成によって A.dtd 読み込み → A.dtdに書かれた B.ent読み込み → B.entに書かれた C.ent 読み込み ... と、実に冗長なI/Oが発生する。

 

業務で担当したDTDでは、.entが30個もありしかも、シングルスレッドで真面目にI/O待ちしていたため、テスト時の環境では解決にどうやら5秒(!!!)ぐらい要したようだ。。

本番環境ではもうちょっと早いにしても、いくらなんでもこれはだめだろう。

 

ということで、お世話になっている org.xml.saxパッケージでどうにかできないかを調査して、結果2案ほど浮かんだ。(状況によって使い分けさせてもらった)

解決策①外部エンティティを解決させない

いきなり過激な方法。方法は2つある。

(外部エンティティ = ここではdtdとかentファイルなどのこと)

XmlReader.setFeatureでアレコレする

URIをパラメータ名にして色々設定できるメソッド。(以下、全一覧)

https://xerces.apache.org/xerces2-j/features.html

 

とりあえず以下指定すれば有効になる

http://apache.org/xml/features/nonvalidating/load-external-dtd

 

【実例】

SAXParserFactory spf = SAXParserFactory.newInstance();

SAXParser  sp = spf.newSAXParser();

XMLReader  xmlReader = sp.getXMLReader();

 

// コレ

// 外部DTDの解決 -> false

xmlReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

EntityResolverでフックし強制的に読み捨てる

上の方法だと、コードの利用方法(SAXとかJAXBとか)によって無理な場合があった。

本当に危ない方法だがEntityResolverというURLの解決をフックする仕組みで悪いことをするしか思いつかなかった。

SAXParserFactory spf = SAXParserFactory.newInstance();

SAXParser  sp = spf.newSAXParser();

XMLReader  xmlReader = sp.getXMLReader();

 

// コレ

xmlReader.setEntityResolver(new EntityResolver() {
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        return new InputSource(new StringReader(""));
    }
});

 仕組み的には

DTDとか外部URL解決する際にresolveEntity()が呼ばれる

 → すべての解決に対して、空文字データのソースを与える。

こうすると、少なくともXmlReaderはエラーなく(?)無視してくれる。

 

解決策②負荷の少ないInputSourceへのResolveを促す。

負荷は避けたいが少なくともDTD検証とかのために特定のDTDだけでも速くしたいとき。EntityResolveが使える。

SAXParserFactory spf = SAXParserFactory.newInstance();

SAXParser  sp = spf.newSAXParser();

XMLReader  xmlReader = sp.getXMLReader();

 

// 例えば HTTP経由の解決を、ローカルファイルの特定ディレクトリ直下に置き換える場合。

// 引数systemIdに解決したいURLが入ってる。

// 戻り値にnullを返すとデフォルト動作をとる。

xmlReader.setEntityResolver(new EntityResolver() {
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {

        if ("http://www.sample.com/validation-defines.dtd".equals(systemId)) {
            return new InputSource("/var/www/resource/dtd/validation-defines.dtd");

        } else {

            return null;

        }
    }
});

 

やはりというか

以上、コードで突貫対応する処方。

ただ、一番根本的なのは「そもそも(こんな高コストな)DTD使わなきゃだめなの?」みたいなとこ。

強力で疎結合で十分テストされた機能だが、高々validationするにしては色々と高コスト、DTDをモジュール化して~みたいな仕組みは疎結合で勝手に名前空間切っちゃったりとかして色々と開発に優しくない。(仕様を合わせるためにそこそこの規模の仕組みを用意する必要がある)。

 

もう文書メタデータJSONでいいじゃん。とも現場で言っては通らない。つらい。