Java Tips | 基本ユーティリティ:XMLパース

Java Java
スポンサーリンク

XMLパースは「外部仕様を自分のオブジェクトに落とし込む」技

XML生成が「こちらの世界のデータを外に出す」なら、
XMLパースはその逆で、「外の世界から来た XML を、自分の世界(Java のオブジェクト)に安全に落とし込む」技です。

ここを雑にやると、「タグがなかった」「想定外の値が入っていた」「文字コードでコケた」みたいな、本番でしか出ないイヤなバグが出ます。
だからこそ、XMLパース用のユーティリティを一つ決めておくと、コード全体がかなり落ち着きます。


基本の考え方:文字列を直接いじらず「パーサに任せる」

substring や indexOf で XML を触るのはやめる

まず大前提として、次のようなコードは封印してほしいです。

// 絶対にやめたほうがいい例
int start = xml.indexOf("<name>");
int end   = xml.indexOf("</name>");
String name = xml.substring(start + 6, end);
Java

一見動きそうですが、改行や空白、名前空間、属性が入った瞬間に壊れます。
XML は「構造化データ」なので、文字列操作ではなく「XML パーサ」に任せるのが鉄則です。

Java には標準で DOM(ツリー構造)や SAX(イベント駆動)のパーサが用意されていますが、
ここではまず「DOM を使って XML をオブジェクトとして扱う」流れから見ていきます。


DOM を使った「XML → DOMツリー」の基本

DocumentBuilder で XML を読み込む

XML を DOM として読み込む最小コードはこんな感じです。

import org.w3c.dom.Document;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public final class XmlParser {

    private XmlParser() {}

    public static Document parse(String xml) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            // 必要に応じて安全設定を入れる(後述)
            DocumentBuilder builder = factory.newDocumentBuilder();
            return builder.parse(new java.io.ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new IllegalArgumentException("XML パースに失敗しました。入力が正しい XML か確認してください。", e);
        }
    }
}
Java

使い方はこうです。

String xml = """
        <user>
            <id>u-001</id>
            <name>山田太郎</name>
        </user>
        """;

Document doc = XmlParser.parse(xml);
Java

ここで深掘りしたい重要ポイントは、「XML を一度 Document(DOMツリー)にしてしまえば、タグやテキストを“オブジェクトとして”扱えるようになる」ことです。
文字列を直接いじるのではなく、「ノードをたどる」感覚に切り替わります。


例題:単純な XML から値を取り出す

<user> の <id> と <name> を読む

さきほどの XML から、idname を取り出してみます。

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public final class UserXmlReader {

    private UserXmlReader() {}

    public static String getId(Document doc) {
        Element root = doc.getDocumentElement(); // <user>
        NodeList idNodes = root.getElementsByTagName("id");
        if (idNodes.getLength() == 0) {
            return null;
        }
        return idNodes.item(0).getTextContent();
    }

    public static String getName(Document doc) {
        Element root = doc.getDocumentElement(); // <user>
        NodeList nameNodes = root.getElementsByTagName("name");
        if (nameNodes.getLength() == 0) {
            return null;
        }
        return nameNodes.item(0).getTextContent();
    }
}
Java

使い方はこうです。

Document doc = XmlParser.parse(xml);

String id   = UserXmlReader.getId(doc);
String name = UserXmlReader.getName(doc);

System.out.println(id);   // u-001
System.out.println(name); // 山田太郎
Java

ここでのポイントは、「タグ名でノードを検索し、getTextContent で中身を取る」という基本パターンを押さえることです。
getTextContent は、子要素を含めたテキストをまとめて返してくれるので、
単純な構造ならこれだけで十分なことが多いです。


例題:XML → POJO へのマッピング

業務エンティティに落とし込む

XML を読む最終目的は、「業務で使えるオブジェクトに落とし込む」ことです。
例えば、さきほどの <user>User クラスにマッピングしてみます。

public class User {

    private final String id;
    private final String name;

    public User(String id, String name) {
        this.id   = id;
        this.name = name;
    }

    public String getId()   { return id; }
    public String getName() { return name; }
}
Java

これを XML から作るユーティリティを用意します。

import org.w3c.dom.Document;
import org.w3c.dom.Element;

public final class UserXmlMapper {

    private UserXmlMapper() {}

    public static User fromDocument(Document doc) {
        Element root = doc.getDocumentElement();
        if (!"user".equals(root.getTagName())) {
            throw new IllegalArgumentException("ルート要素が <user> ではありません。実際: " + root.getTagName());
        }

        String id   = getRequiredChildText(root, "id");
        String name = getRequiredChildText(root, "name");

        return new User(id, name);
    }

    private static String getRequiredChildText(Element parent, String tagName) {
        var nodes = parent.getElementsByTagName(tagName);
        if (nodes.getLength() == 0) {
            throw new IllegalArgumentException("必須要素 <" + tagName + "> が存在しません。");
        }
        String text = nodes.item(0).getTextContent();
        if (text == null || text.isBlank()) {
            throw new IllegalArgumentException("必須要素 <" + tagName + "> が空です。");
        }
        return text.trim();
    }
}
Java

使い方はこうです。

Document doc = XmlParser.parse(xml);
User user = UserXmlMapper.fromDocument(doc);

System.out.println(user.getId());
System.out.println(user.getName());
Java

ここで深掘りしたい重要ポイントは、「“XML が仕様どおりかどうか”を、マッピングの中でしっかりチェックしている」ことです。
getRequiredChildText で「要素がない」「空文字」の場合に例外を投げることで、
「おかしな XML をそのまま業務ロジックに流さない」ようにしています。

XMLパースは「ただ値を取る」のではなく、「仕様に合っているかを確認しながらオブジェクトにする」作業だ、という感覚を持っておくと強いです。


例題:複数要素(リスト)を読む

<users> の中の <user> を全部読む

少しだけ複雑な例として、こんな XML を考えます。

<users>
    <user>
        <id>u-001</id>
        <name>山田太郎</name>
    </user>
    <user>
        <id>u-002</id>
        <name>佐藤花子</name>
    </user>
</users>

これを List<User> にしたいとします。

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.List;

public final class UsersXmlMapper {

    private UsersXmlMapper() {}

    public static List<User> fromDocument(Document doc) {
        Element root = doc.getDocumentElement();
        if (!"users".equals(root.getTagName())) {
            throw new IllegalArgumentException("ルート要素が <users> ではありません。");
        }

        NodeList userNodes = root.getElementsByTagName("user");
        List<User> result = new ArrayList<>();

        for (int i = 0; i < userNodes.getLength(); i++) {
            Element userElem = (Element) userNodes.item(i);
            String id   = getRequiredChildText(userElem, "id");
            String name = getRequiredChildText(userElem, "name");
            result.add(new User(id, name));
        }
        return result;
    }

    private static String getRequiredChildText(Element parent, String tagName) {
        var nodes = parent.getElementsByTagName(tagName);
        if (nodes.getLength() == 0) {
            throw new IllegalArgumentException("必須要素 <" + tagName + "> が存在しません。");
        }
        return nodes.item(0).getTextContent().trim();
    }
}
Java

使い方はこうです。

Document doc = XmlParser.parse(xml);
List<User> users = UsersXmlMapper.fromDocument(doc);

System.out.println(users.size());          // 2
System.out.println(users.get(0).getName()); // 山田太郎
Java

ここでのポイントは、「NodeList をループして、1件ずつ POJO に落とし込む」というパターンを覚えることです。
「1要素 → 1オブジェクト」「複数要素 → List<オブジェクト>」という対応が見えてくると、
XML を「ただのテキスト」ではなく「木構造のデータ」として扱えるようになります。


XMLパースで絶対に意識したい「安全性」の話

XXE(外部エンティティ攻撃)対策を入れる

業務で XML を外部から受け取るとき、必ず意識したいのが XXE(XML External Entity)攻撃です。
ざっくり言うと、「XML の中からローカルファイルを読ませたり、外部にアクセスさせたりする」攻撃です。

これを防ぐために、DocumentBuilderFactory に安全設定を入れておきます。

private static DocumentBuilderFactory secureFactory() throws Exception {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
    factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    factory.setXIncludeAware(false);
    factory.setExpandEntityReferences(false);
    return factory;
}
Java

そして XmlParser.parse でこれを使います。

public static Document parse(String xml) {
    try {
        DocumentBuilderFactory factory = secureFactory();
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(new java.io.ByteArrayInputStream(xml.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
    } catch (Exception e) {
        throw new IllegalArgumentException("XML パースに失敗しました。", e);
    }
}
Java

ここで深掘りしたいのは、「XMLパースは“ただの構文解析”ではなく、“外部からの入力を扱うセキュリティ境界”でもある」という意識です。
安全設定をユーティリティに閉じ込めておけば、
どこで XML をパースしても、同じ防御が効くようになります。


XMLパースと業務バリデーションを分けて考える

「構造として正しい」と「意味として正しい」は別物

XMLパースが保証してくれるのは、「構文として正しい XML か」「指定したタグが存在するか」までです。
「この金額は 0 以上か」「この状態遷移は許されるか」といった業務ルールは、
パース後に別の層でチェックする必要があります。

つまり、

XMLパース層では
・XML として壊れていないか
・必須タグがあるか
・型(数値/日付など)がパースできるか

業務バリデーション層では
・値が業務ルールに合っているか
・組み合わせとして矛盾していないか

というふうに責務を分けておくと、コードの見通しがかなり良くなります。


まとめ:XMLパースユーティリティで身につけたい感覚

XMLパースは、「文字列を手で切り刻む」のではなく、
「パーサで DOM にしてから、仕様に従ってオブジェクトに落とし込む」ための技です。

押さえておきたい感覚は、まず「DocumentBuilderFactory+DocumentBuilder で XML を DOM にする」こと。
次に、「Element/NodeList を使ってタグをたどり、必須要素の有無や中身をチェックしながら POJO にマッピングする」こと。
そして、「安全設定(XXE 対策)をユーティリティに閉じ込めて、どこでパースしても同じ防御が効くようにする」ことです。

タイトルとURLをコピーしました