java-recipes

ホーム ファイルI/O › F-06

F-06: XML の読み書き(DOM / SAX / StAX)

Java 標準ライブラリ(javax.xml)だけで XML を読み書きする方法を解説します。 DOM・SAX・StAX の 3 方式にはそれぞれ得意な用途があり、用途に合わせて使い分けることが重要です。

3 方式の使い分け

方式メモリ処理スタイル向いている用途
DOM多い(全体展開)ツリー操作小〜中規模、要素の検索・変更・追加
SAX少ない(逐次)イベント駆動大容量ファイル、GB 単位の XML 処理
StAX少ない(逐次)カーソル型大容量ファイル、SAX より書きやすい

実務でのポイント:設定ファイルや小規模データには DOM、 ログファイルや大量データには SAX か StAX を選びましょう。 XML の書き込み(生成)は DOM が最も扱いやすいです。

サンプルコード

以下の XML を DOM・SAX・StAX の 3 方式でパースし、DOM で新しい XML を生成するサンプルです。

<?xml version="1.0" encoding="UTF-8"?>
<employees>
  <employee id="1">
    <name>Yamada Taro</name>
    <department>Development</department>
    <salary>450000</salary>
  </employee>
  <employee id="2">
    <name>Suzuki Hanako</name>
    <department>Sales</department>
    <salary>380000</salary>
  </employee>
</employees>
Sample.java
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class XmlSample {

    // ---- DOM 方式: 読み込み ----
    // ツリー全体をメモリに展開するため、小〜中規模向け。
    // ランダムアクセスや要素の追加・削除が可能。

    public static List<Map<String, String>> parseWithDom(String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(new InputSource(new StringReader(xml)));
        doc.getDocumentElement().normalize();

        List<Map<String, String>> employees = new ArrayList<>();
        NodeList list = doc.getElementsByTagName("employee");
        for (int i = 0; i < list.getLength(); i++) {
            Element elem = (Element) list.item(i);
            Map<String, String> emp = new LinkedHashMap<>();
            emp.put("id", elem.getAttribute("id"));
            emp.put("name", getTextContent(elem, "name"));
            emp.put("department", getTextContent(elem, "department"));
            emp.put("salary", getTextContent(elem, "salary"));
            employees.add(emp);
        }
        return employees;
    }

    private static String getTextContent(Element parent, String tagName) {
        NodeList nodes = parent.getElementsByTagName(tagName);
        if (nodes.getLength() > 0) {
            return nodes.item(0).getTextContent();
        }
        return "";
    }

    // ---- DOM 方式: 書き込み ----
    // Transformer を使って DOM ツリーを整形済み XML 文字列に変換する。

    public static String buildWithDom(List<Map<String, String>> employees) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.newDocument();

        Element root = doc.createElement("employees");
        doc.appendChild(root);

        for (Map<String, String> emp : employees) {
            Element empElem = doc.createElement("employee");
            empElem.setAttribute("id", emp.get("id"));
            root.appendChild(empElem);

            Element nameElem = doc.createElement("name");
            nameElem.setTextContent(emp.get("name"));
            empElem.appendChild(nameElem);

            Element deptElem = doc.createElement("department");
            deptElem.setTextContent(emp.get("department"));
            empElem.appendChild(deptElem);

            Element salaryElem = doc.createElement("salary");
            salaryElem.setTextContent(emp.get("salary"));
            empElem.appendChild(salaryElem);
        }

        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
        StringWriter sw = new StringWriter();
        transformer.transform(new DOMSource(doc), new StreamResult(sw));
        return sw.toString();
    }

    // ---- SAX 方式: 読み込み ----
    // イベント駆動でメモリ効率優先。GB 単位の大容量 XML も処理可能。

    private static class SaxEmployeeHandler extends DefaultHandler {
        final List<Map<String, String>> employees = new ArrayList<>();
        private Map<String, String> currentEmp;
        private final StringBuilder currentText = new StringBuilder();

        @Override
        public void startElement(String uri, String localName, String qName,
                Attributes attributes) {
            if ("employee".equals(qName)) {
                currentEmp = new LinkedHashMap<>();
                currentEmp.put("id", attributes.getValue("id"));
            }
            currentText.setLength(0); // テキストバッファをリセット
        }

        @Override
        public void characters(char[] ch, int start, int length) {
            // テキストノードは複数回に分割して呼ばれることがあるため append する
            currentText.append(ch, start, length);
        }

        @Override
        public void endElement(String uri, String localName, String qName) {
            if (currentEmp == null) {
                return;
            }
            String text = currentText.toString().trim();
            if ("name".equals(qName) || "department".equals(qName)
                    || "salary".equals(qName)) {
                currentEmp.put(qName, text);
            }
            if ("employee".equals(qName)) {
                employees.add(currentEmp);
                currentEmp = null;
            }
        }
    }

    public static List<Map<String, String>> parseWithSax(String xml) throws Exception {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
        SaxEmployeeHandler handler = new SaxEmployeeHandler();
        parser.parse(new InputSource(new StringReader(xml)), handler);
        return handler.employees;
    }

    // ---- StAX 方式: 読み込み ----
    // カーソル型ストリーム。SAX より直感的で DOM よりメモリ効率が高い。

    public static List<Map<String, String>> parseWithStax(String xml) throws Exception {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(xml));

        List<Map<String, String>> employees = new ArrayList<>();
        Map<String, String> currentEmp = null;
        String currentTag = null;

        while (reader.hasNext()) {
            int event = reader.next();

            if (event == XMLStreamConstants.START_ELEMENT) {
                String name = reader.getLocalName();
                if ("employee".equals(name)) {
                    currentEmp = new LinkedHashMap<>();
                    currentEmp.put("id", reader.getAttributeValue(null, "id"));
                }
                currentTag = name;

            } else if (event == XMLStreamConstants.CHARACTERS && currentEmp != null) {
                String text = reader.getText().trim();
                if (!text.isEmpty() && currentTag != null) {
                    if ("name".equals(currentTag) || "department".equals(currentTag)
                            || "salary".equals(currentTag)) {
                        currentEmp.put(currentTag, text);
                    }
                }

            } else if (event == XMLStreamConstants.END_ELEMENT) {
                if ("employee".equals(reader.getLocalName()) && currentEmp != null) {
                    employees.add(currentEmp);
                    currentEmp = null;
                }
                currentTag = null;
            }
        }
        reader.close();
        return employees;
    }
}

よくあるミス・注意点

⚠️ SAX の characters() は複数回呼ばれる

テキストノードが長い場合、characters() が複数回に分割して呼ばれることがあります。currentText.setLength(0) でリセットし、append() で連結するパターンを使ってください。 「最後の呼び出しだけ使えばいい」という思い込みがバグの原因になります。

⚠️ StAX の reader.close() を忘れずに

XMLStreamReaderCloseable を実装していないため、try-with-resources が使えません(Java 8 時点)。 必ず finally ブロックか、処理後に reader.close() を呼んでください。

⚠️ DOM の normalize() を呼ぶこと

doc.getDocumentElement().normalize() を呼ばないと、 隣接するテキストノードが分割されたままになることがあります。 パース後は normalize() を呼ぶ習慣をつけましょう。

⚠️ XXE(XML外部エンティティ)脆弱性に注意

外部からの入力を DOM や SAX でパースする場合、XXE 攻撃を防ぐためfactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)を設定してください。信頼できる内部データだけを処理する場合は不要です。

テストする観点

  • ✅ 正常系:複数の <employee> 要素が正しく読み込めるか(DOM/SAX/StAX 全方式)
  • ✅ 属性値:id 属性が正しく取得できるか
  • ✅ ネスト要素:子要素のテキストコンテンツが正しく取得できるか
  • ✅ 空要素:<salary></salary> のような空タグで例外が出ないか
  • ✅ 特殊文字:&(アンパサンド)、<(不等号)がエスケープされているか
  • ✅ 書き込み:DOM で生成した XML を再パースしてラウンドトリップが成立するか
  • ✅ 大容量:SAX/StAX が大きなファイルでメモリ使用量が増えすぎないか

GitHub でソースコードを見る →