概要

業務システムでは、外部システムとのデータ連携、設定ファイル、SOAP メッセージなど、XML を扱う場面がいまだに少なくありません。Java 標準 API には DOM、SAX、StAX の3つのパーサーが用意されていますが、「どれを使えばいいのか」「それぞれの利点と制約は何か」を整理できている人は多くありません。DOM はツリー全体をメモリに保持するため小〜中規模ファイルに向き、SAX はイベント駆動で大容量ファイルに対応でき、StAX はカーソル方式で SAX より直感的に書けます。この記事では、3方式のパースと DOM による XML 生成を実装しながら、用途に応じた選択基準を整理します。

使いどころ

取引先から送られてくる注文データの XML を読み込み、DB に登録する

帳票出力用のデータを XML 形式で生成し、XSLT で変換してからPDF出力する

SOAP ベースの外部 API と連携するために、リクエスト・レスポンスの XML を処理する

コード例

DOM・SAX・StAX による XML 読み書き
import javax.xml.parsers.DocumentBuilderFactory;

public class XmlProcessing {

    record Employee(String id, String name, String department, int salary) {}

    /** DOM で XML をパース */
    public static List<Employee> parseWithDom(String xml) throws Exception {
        var factory = DocumentBuilderFactory.newInstance();
        var doc = factory.newDocumentBuilder()
                .parse(new InputSource(new StringReader(xml)));
        doc.getDocumentElement().normalize();

        var employees = new ArrayList<Employee>();
        var list = doc.getElementsByTagName("employee");
        for (int i = 0; i < list.getLength(); i++) {
            var elem = (Element) list.item(i);
            employees.add(new Employee(
                elem.getAttribute("id"),
                elem.getElementsByTagName("name").item(0).getTextContent(),
                elem.getElementsByTagName("department").item(0).getTextContent(),
                Integer.parseInt(elem.getElementsByTagName("salary").item(0).getTextContent())
            ));
        }
        return employees;
    }

    /** DOM で XML を生成 */
    public static String buildWithDom(List<Employee> employees) throws Exception {
        var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        var root = doc.createElement("employees");
        doc.appendChild(root);

        for (var emp : employees) {
            var e = doc.createElement("employee");
            e.setAttribute("id", emp.id());
            root.appendChild(e);
            appendText(doc, e, "name", emp.name());
            appendText(doc, e, "department", emp.department());
            appendText(doc, e, "salary", String.valueOf(emp.salary()));
        }

        var transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        var sw = new StringWriter();
        transformer.transform(new DOMSource(doc), new StreamResult(sw));
        return sw.toString();
    }

    private static void appendText(Document doc, Element parent, String tag, String text) {
        var elem = doc.createElement(tag);
        elem.setTextContent(text);
        parent.appendChild(elem);
    }

    public static void main(String[] args) throws Exception {
        var xml = """
                <?xml version="1.0" encoding="UTF-8"?>
                <employees>
                  <employee id="1">
                    <name>Yamada Taro</name>
                    <department>Development</department>
                    <salary>450000</salary>
                  </employee>
                </employees>
                """;

        parseWithDom(xml).forEach(System.out::println);
        System.out.println(buildWithDom(List.of(
            new Employee("2", "Suzuki Hanako", "Sales", 380000))));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でパース結果を型安全に保持できる。var で変数宣言が簡潔になる。switch 式(アロー構文)で SAX の endElement 処理が書きやすい。

Java 17
// Java 17: record + switch 式(アロー構文)
record Employee(String id, String name, String dept, int salary) {}

switch (qName) {
    case "name"       -> name = text;
    case "department" -> department = text;
    case "salary"     -> salary = text;
}

Library Comparison

Java 標準 API(DOM / SAX / StAX)外部依存を増やしたくない場合。XML の読み書きで標準 API だけで完結させたいとき。コード量が多くなりがち。XPath クエリは別途 javax.xml.xpath が必要。
JAXBXML とJava オブジェクトの双方向マッピングが必要な場合。Java 11 以降は JDK から分離され、外部依存として追加が必要。アノテーションの学習コストがある。
Jackson XML(jackson-dataformat-xml)JSON と XML を同じ ObjectMapper 体系で扱いたい場合。Jackson 本体 + XML モジュールの依存が必要。複雑な XML 構造には向かない場合もある。

注意点

DOM は XML 全体をメモリに展開するため、数十MB を超えるファイルでは OutOfMemoryError のリスクがある。大容量なら SAX か StAX を使うこと。

外部から受け取る XML に対して DTD 処理を有効にしたままパースすると、XXE(XML External Entity)攻撃のリスクがある。外部入力のパースでは DTD を無効化すること。

SAX の characters() メソッドは1つのテキストノードでも複数回に分割して呼ばれることがある。StringBuilder に append して endElement() で確定させること。

Transformer の出力エンコーディングは指定しないとプラットフォーム依存になる場合がある。OutputKeys.ENCODING に UTF-8 を設定するのが安全。

FAQ

DOM・SAX・StAX のどれを選べばよいですか。

数MB 以下で要素の追加・削除もしたいなら DOM、大容量で読み取り専用なら SAX、大容量で直感的に書きたいなら StAX が適しています。

XXE 攻撃を防ぐにはどうしますか。

DocumentBuilderFactory や SAXParserFactory で FEATURE_EXTERNAL_ENTITIES を false に設定し、DTD 処理を無効化してください。

名前空間付き XML のパースは標準 API で対応できますか。

DocumentBuilderFactory.setNamespaceAware(true) を設定し、getElementsByTagNameNS() を使えば名前空間付き要素を正しく取得できます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます