1. DOM (Document Object Model)
Đôi khi tệp XML quá lớn để tải toàn bộ vào bộ nhớ. Cũng có trường hợp cấu trúc tài liệu chưa biết trước hoặc quá phức tạp. Trong những tình huống khác, bạn chỉ cần xử lý một phần dữ liệu chứ không phải toàn bộ tệp, hoặc thậm chí cần thay đổi tài liệu ngay khi chương trình đang chạy: xóa nút, thêm phần tử mới hoặc tái cấu trúc một phần của cây.
Trong các tình huống như vậy, những công cụ lâu đời và đáng tin cậy — DOM và SAX — rất phù hợp. Chúng cho phép làm việc trực tiếp với nội dung XML mà không cần gắn với các lớp Java được mô tả sẵn.
Cách DOM hoạt động
DOM là cách biểu diễn tài liệu XML dưới dạng một cây đối tượng trong bộ nhớ. Mỗi thẻ trở thành một nút của cây (Node), còn thuộc tính, giá trị văn bản và thậm chí cả chú thích — là các đối tượng riêng. Sau khi tải tài liệu, bạn có quyền truy cập đầy đủ vào cấu trúc của nó: có thể đọc, thay đổi, xóa, thêm phần tử và thuộc tính.
Các lớp DOM chính trong Java
- DocumentBuilderFactory — factory để tạo parser.
- DocumentBuilder — parser chuyển XML thành cây.
- Document — đối tượng gốc của cây.
- Element — phần tử XML (thẻ).
- NodeList — danh sách các nút (ví dụ, tất cả <item> bên trong <items>).
Ví dụ: Đọc tệp XML bằng DOM
Giả sử chúng ta cần đọc một tệp XML như sau:
<contacts>
<person id="1">
<name>Ivan</name>
<email>ivan@example.com</email>
</person>
<person id="2">
<name>Mariya</name>
<email>maria@example.com</email>
</person>
</contacts>
Mã: Đọc và duyệt các phần tử
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.File;
public class DomExample {
public static void main(String[] args) throws Exception {
// 1. Tạo factory và parser
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 2. Nạp tệp XML vào bộ nhớ
Document doc = builder.parse(new File("contacts.xml"));
// 3. Lấy phần tử gốc
Element root = doc.getDocumentElement();
System.out.println("Phần tử gốc: " + root.getTagName());
// 4. Lấy danh sách tất cả <person>
NodeList persons = root.getElementsByTagName("person");
for (int i = 0; i < persons.getLength(); i++) {
Element person = (Element) persons.item(i);
String id = person.getAttribute("id");
String name = person.getElementsByTagName("name").item(0).getTextContent();
String email = person.getElementsByTagName("email").item(0).getTextContent();
System.out.println("id: " + id + ", name: " + name + ", email: " + email);
}
}
}
Ở đây diễn ra điều gì:
- Đầu tiên tạo parser và tải tệp XML.
- Lấy phần tử gốc (contacts).
- Tìm tất cả phần tử <person>, rồi duyệt.
- Với mỗi <person> đọc thuộc tính id, các phần tử <name> và <email>.
Sơ đồ cây DOM cho ví dụ
contacts
├── person (id="1")
│ ├── name ("Ivan")
│ └── email ("ivan@example.com")
└── person (id="2")
├── name ("Mariya")
└── email ("maria@example.com")
Thay đổi XML bằng DOM
DOM cho phép không chỉ đọc mà còn sửa XML. Ví dụ, thêm một người mới:
// Tạo phần tử <person> mới
Element newPerson = doc.createElement("person");
newPerson.setAttribute("id", "3");
// <name>
Element name = doc.createElement("name");
name.setTextContent("Sergey");
newPerson.appendChild(name);
// <email>
Element email = doc.createElement("email");
email.setTextContent("sergey@example.com");
newPerson.appendChild(email);
// Thêm vào gốc
root.appendChild(newPerson);
// Lưu thay đổi ra tệp
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File("contacts-updated.xml")));
Ưu và nhược điểm chính của DOM
- Ưu điểm: thuận tiện cho tệp nhỏ, dễ thực hiện mọi thay đổi, có thể “đi dạo” trong cây theo bất kỳ hướng nào.
- Nhược điểm: toàn bộ XML nằm trong bộ nhớ. Với các tệp lớn (hàng trăm MB trở lên) — không phù hợp, bộ nhớ sẽ nhanh chóng cạn kiệt.
2. SAX (Simple API for XML)
SAX là một parser dựa trên sự kiện. Nó không xây dựng cây mà chỉ đọc XML “từ trái sang phải” và gọi các bộ xử lý sự kiện: “bắt đầu phần tử”, “nội dung văn bản”, “kết thúc phần tử” v.v. Bạn viết bộ xử lý của riêng mình và phản ứng với các sự kiện cần thiết.
So sánh dễ hiểu:
Nếu DOM giống như trải tất cả các trang của một cuốn sổ tay ra bàn, thì SAX giống như đọc cuốn sổ tay theo từng trang và chỉ ghi chú khi gặp trang bạn cần.
Các lớp SAX chính trong Java
- SAXParserFactory, SAXParser — factory và parser.
- DefaultHandler — lớp cơ sở để xử lý sự kiện.
- Các phương thức xử lý: startElement, characters, endElement, startDocument, endDocument.
Ví dụ: Đọc tệp XML bằng SAX
Giả sử cùng một tệp contacts.xml. Chúng ta chỉ muốn in ra tên và e-mail của tất cả mọi người.
Mã: SAX-parser
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
public class SaxExample {
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(new File("contacts.xml"), new ContactHandler());
}
}
class ContactHandler extends DefaultHandler {
private String currentElement = "";
private String name = "";
private String email = "";
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentElement = qName;
if ("person".equals(qName)) {
String id = attributes.getValue("id");
System.out.println("Người mới, id: " + id);
}
}
@Override
public void characters(char[] ch, int start, int length) {
String text = new String(ch, start, length).trim();
if (text.isEmpty()) return;
if ("name".equals(currentElement)) {
name = text;
} else if ("email".equals(currentElement)) {
email = text;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("person".equals(qName)) {
System.out.println("Tên: " + name + ", email: " + email);
name = "";
email = "";
}
currentElement = "";
}
}
Bây giờ cùng giải thích ngắn gọn: khi SAX-parser gặp thẻ mở, ví dụ <person>, nó gọi phương thức startElement. Ở đó, chúng ta đọc thuộc tính id và in ra màn hình. Khi bên trong gặp văn bản — ví dụ, tên hoặc e-mail — luồng điều khiển chuyển đến phương thức characters, nơi chúng ta lưu văn bản này vào biến tạm. Khi parser gặp thẻ đóng </person>, phương thức endElement được gọi. Lúc này, chúng ta đã biết tên và e-mail của người đó và có thể in ra. Sau đó, các biến được đặt lại để sẵn sàng cho liên hệ tiếp theo.
Ý tưởng là SAX không lưu toàn bộ XML trong bộ nhớ mà hoạt động như một “trình đọc” theo luồng: đi qua tệp từ trên xuống và phản ứng với các sự kiện — bắt đầu thẻ, văn bản, kết thúc thẻ. Cách này nhanh và tiết kiệm bộ nhớ, đặc biệt cho các tệp lớn.
So sánh nhanh DOM và SAX
| DOM | SAX | |
|---|---|---|
| Phong cách | Cây (toàn bộ cấu trúc trong bộ nhớ) | Sự kiện (xử lý “tức thời”) |
| Bộ nhớ | Tải toàn bộ XML | Sử dụng bộ nhớ tối thiểu |
| Thay đổi | Có thể đọc/sửa/thêm | Chỉ đọc (thông thường) |
| Tìm kiếm dữ liệu | Dễ tìm ở bất kỳ đâu | Cần theo dõi “đang ở đâu” |
| Kích thước tệp | Cho tệp nhỏ và trung bình | Cho tệp rất lớn |
3. Khi nào dùng DOM, khi nào dùng SAX
DOM rất phù hợp nếu:
- Tệp nhỏ hoặc trung bình.
- Cần truy cập nhiều lần vào các phần khác nhau của XML.
- Cần thay đổi cấu trúc tài liệu.
SAX — là lựa chọn của bạn nếu:
- Tệp khổng lồ và không thể tải vào bộ nhớ.
- Cần nhanh chóng “rút” chỉ một phần thông tin (ví dụ, tìm tất cả phần tử <item> với thuộc tính nhất định).
- Cần hiệu năng cao và sử dụng bộ nhớ tối thiểu.
- Bạn không có kế hoạch sửa XML, chỉ đọc.
Gợi ý:
Trong các dự án thực tế thường dùng cả hai: DOM — cho các cấu hình “dễ đọc của con người” và tệp nhỏ; SAX — để xử lý log, xuất/nhập dữ liệu lớn.
4. Bài tập thực hành: trình phân tích nhỏ cho ứng dụng
Trong ứng dụng học tập của bạn (ví dụ “sổ địa chỉ”), giả sử có yêu cầu: đếm nhanh số lượng người dùng có e-mail trên gmail.com.
Giải pháp DOM:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("contacts.xml"));
NodeList emails = doc.getElementsByTagName("email");
int count = 0;
for (int i = 0; i < emails.getLength(); i++) {
String email = emails.item(i).getTextContent();
if (email.endsWith("@gmail.com")) {
count++;
}
}
System.out.println("Số người dùng có gmail.com: " + count);
Giải pháp SAX:
class GmailCounterHandler extends DefaultHandler {
private String currentElement = "";
int count = 0;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentElement = qName;
}
@Override
public void characters(char[] ch, int start, int length) {
if ("email".equals(currentElement)) {
String email = new String(ch, start, length).trim();
if (email.endsWith("@gmail.com")) {
count++;
}
}
}
@Override
public void endDocument() {
System.out.println("Số người dùng có gmail.com: " + count);
}
}
5. Đặc thù và lưu ý khi làm việc với DOM và SAX
- DOM có thể “ăn” hết bộ nhớ nếu tệp XML rất lớn. Nếu bạn gặp lỗi OutOfMemoryError, rất có thể đã đến lúc chuyển sang SAX.
- SAX đòi hỏi sự cẩn thận: cần theo dõi bạn đang ở phần tử nào và khéo léo gom dữ liệu cần thiết. Đôi khi phải dùng stack hoặc biến bổ trợ để không bị rối với cấu trúc lồng sâu.
- Trong SAX, phương thức characters có thể được gọi nhiều lần cho cùng một khối văn bản (đặc biệt nếu văn bản dài hoặc có ký tự đặc biệt). Tốt nhất nên tích lũy văn bản vào StringBuilder.
- DOM rất phù hợp cho tìm kiếm, điều hướng và thay đổi cấu trúc, nhưng không phù hợp cho xử lý theo luồng.
- Nếu bạn chưa chắc nên chọn cách nào — hãy bắt đầu với DOM cho đơn giản, và nếu “nặng” về bộ nhớ — hãy viết lại sang SAX.
6. Lỗi thường gặp khi làm việc với DOM và SAX
Lỗi số 1: Xử lý sai khoảng trắng và xuống dòng trong SAX.
Phương thức characters có thể trả về các đoạn văn bản, bao gồm cả khoảng trắng và xuống dòng giữa các phần tử. Nếu không lọc bằng .trim().isEmpty(), bạn có thể nhận nhiều lần gọi “rỗng” hoặc ghép văn bản không chính xác.
Lỗi số 2: Cố gắng sửa XML bằng SAX.
SAX — chỉ để đọc! Nếu cần thay đổi cấu trúc — hãy dùng DOM.
Lỗi số 3: Vi phạm thứ tự sự kiện trong SAX.
Nếu không đặt lại các biến trong endElement, dữ liệu có thể “rò rỉ” giữa các phần tử.
Lỗi số 4: Dùng DOM cho tệp khổng lồ.
Hậu quả — OutOfMemoryError hoặc chạy rất chậm.
Lỗi số 5: Ép kiểu sai trong DOM.
Trong DOM, mọi thứ đều là Node, nhưng để làm việc với thuộc tính và phần tử con, cần ép về Element. Lỗi ép kiểu có thể dẫn tới ClassCastException.
GO TO FULL VERSION