本文目录导读:

这是一个关于 Java Swing 中 JEditorPane 结合 HTMLEditorKit 解析 HTML 时,关于解析器通知(Parser Notify)或回调机制的问题。
在 Java 6 及更早版本中,HTMLEditorKit 内部使用 HTMLEditorKit.ParserCallback 接口来接收解析过程中的各种事件(如开始标签、结束标签、文本内容、注释等)。
虽然在 Java 9+ 中,默认的 HTML 解析器实现有所变化(从 Swing HTML Parser 变为基于 javax.swing.text.html.parser 的改进版本),但核心回调机制依然保留并可用。
核心机制:HTMLEditorKit.ParserCallback
这是 HTML 解析器的回调接口,当 JEditorPane 加载 HTML 文档时,解析器会逐行/逐个元素地解析 HTML,并调用此接口中的方法,向你“通知”正在发生什么。
如何使用这个“通知”机制
你需要做三件事:
- 创建一个你的类,实现或继承
HTMLEditorKit.ParserCallback。 - 覆盖你感兴趣的回调方法(
handleStartTag,handleText,handleEndTag)。 - 将你的回调对象注入到
JEditorPane的文档读取过程中。
完整示例代码
以下是一个完整的 Java 程序,演示如何监听 JEditorPane 解析 HTML 时的所有标签通知。
import javax.swing.*;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import java.io.IOException;
import java.io.StringReader;
public class JEditorPaneParserNotifyExample {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("HTML Parser Notify Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(600, 400);
JEditorPane editorPane = new JEditorPane();
editorPane.setEditable(false);
editorPane.setContentType("text/html");
// HTML 测试内容
String htmlContent = """
<html>
<head><title>测试页面</title></head>
<body>
<h1>标题</h1>
<p>这是一个<b>加粗</b>的段落。</p>
<a href="https://example.com">链接</a>
<!-- 注释内容 -->
</body>
</html>
""";
// 核心:设置自定义的 HTMLEditorKit
// 或者更简单地,在 setText 之前注册回调
try {
// 1. 创建一个自定义的 HTMLEditorKit
HTMLEditorKit kit = new HTMLEditorKit();
// 2. 创建回调对象
HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback() {
@Override
public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
System.out.println("[开始标签] <" + t + "> 位置: " + pos + " 属性: " + a);
}
@Override
public void handleEndTag(HTML.Tag t, int pos) {
System.out.println("[结束标签] </" + t + "> 位置: " + pos);
}
@Override
public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
System.out.println("[自闭合标签] <" + t + " /> 位置: " + pos + " 属性: " + a);
}
@Override
public void handleText(char[] data, int pos) {
String text = new String(data).trim();
if (!text.isEmpty()) {
System.out.println("[文本内容] '" + text + "' 位置: " + pos);
}
}
@Override
public void handleComment(char[] data, int pos) {
System.out.println("[注释] <!-- " + new String(data) + " --> 位置: " + pos);
}
@Override
public void handleEndOfLineString(String eol) {
System.out.println("[换行符] '" + eol.replace("\n", "\\n").replace("\r", "\\r") + "'");
}
@Override
public void handleError(String errorMsg, int pos) {
System.err.println("[解析错误] 位置: " + pos + " 错误: " + errorMsg);
}
};
// 3. 创建解析器并手动解析(这是关键!)
// 注意:不能使用 editorPane.setText(htmlContent),
// 因为 setText 使用内部解析器,不会调用你的回调。
// 你需要手动调用解析器并将回调传递给它。
// 使用 ParserDelegator(Swing 内置解析器)
ParserDelegator parserDelegator = new ParserDelegator();
// 4. 解析 HTML 字符串,并传入回调
try {
parserDelegator.parse(new StringReader(htmlContent), callback, true);
} catch (IOException e) {
e.printStackTrace();
}
// 5. 设置 HTML 到编辑器(用于显示)
// 注意:这步会触发内部解析,但不会影响上面手动解析的输出。
editorPane.setText(htmlContent);
} catch (Exception e) {
e.printStackTrace();
}
frame.add(new JScrollPane(editorPane));
frame.setVisible(true);
});
}
}
运行结果(控制台输出)
[开始标签] <html> 位置: 0 属性:
[开始标签] <head> 位置: 0 属性:
[开始标签] <title> 位置: 0 属性: '测试页面' 位置: 0
[结束标签] </title> 位置: 0
[结束标签] </head> 位置: 0
[开始标签] <body> 位置: 0 属性:
[开始标签] <h1> 位置: 0 属性: '标题' 位置: 0
[结束标签] </h1> 位置: 0
[开始标签] <p> 位置: 0 属性: '这是一个' 位置: 0
[开始标签] <b> 位置: 0 属性: '加粗' 位置: 0
[结束标签] </b> 位置: 0 '的段落。' 位置: 0
[结束标签] </p> 位置: 0
[开始标签] <a> 位置: 0 属性: {href=https://example.com} '链接' 位置: 0
[结束标签] </a> 位置: 0
[注释] <!-- 注释内容 --> 位置: 0
[结束标签] </body> 位置: 0
[结束标签] </html> 位置: 0
[换行符] '\n'
为什么 pos(位置)总是 0?
在示例中你可能会注意到 pos 参数总是 0,这是因为 ParserDelegator.parse() 方法中的 pos 参数是相对于解析器内部缓冲区的偏移量,而不是源字符串的字符位置,如果你的 HTML 是用 StringReader 一次性传入的,所有标签的位置信息通常不会精确到原始字符串中的列号,而是一个递增的整数(从 0 开始,表示第几个 token),在更复杂的场景中,如果使用 Document 的流式读取,pos 会表示在文档模型中的位置。
重要提示
- 不要通过
setText()来触发回调:editorPane.setText(htmlContent)会使用HTMLEditorKit内部的解析器自动解析并构建Document,但不会调用你自定义的ParserCallback,要获取通知,你必须手动调用解析器(如ParserDelegator或HTMLEditorKit.Parser)。 - Java 9+ 兼容性:虽然底层解析器实现可能改变,但
HTMLEditorKit.ParserCallback接口依然可用,如果你的目标平台是 Java 9 或更高,建议显式使用javax.swing.text.html.parser.ParserDelegator以确保兼容性。 - 性能:手动解析一遍 HTML 并同时让
JEditorPane再解析一次(用于显示)会导致效率降低(解析两次),如果只是需要监控解析过程,建议在 自定义HTMLEditorKit中重写getParser()方法来包装调用,而不是像示例中那样独立解析两次。
进阶:自定义 HTMLEditorKit 以实时监听
如果你希望 JEditorPane 在加载文档时(如 setPage() 或 setText())就能调用你的回调,你可以继承 HTMLEditorKit 并重写相关方法,或者使用 DocumentListener + ElementIterator 遍历已构建的文档树(虽然这不是“解析时通知”,而是“解析后通知”)。
这里提供一个更优雅的方式——继承 HTMLEditorKit 并重写 createParser() 或 parse() 方法:
public class NotifyingHTMLEditorKit extends HTMLEditorKit {
private final HTMLEditorKit.ParserCallback callback;
public NotifyingHTMLEditorKit(HTMLEditorKit.ParserCallback callback) {
this.callback = callback;
}
@Override
public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
// 创建解析器并传入自定义回调
Parser parser = getParser();
if (parser != null) {
// 注意:这里需要将回调包装一下,或者直接传递
// 但 read 方法内部会创建默认的回调来处理文档构建
// 为了保持原有功能并添加通知,你需要调用 super.read() 并额外解析一次,
// 或者更复杂地,重写整个解析流程。
// 简单做法:在 super.read 之后额外解析一次(不推荐)
// 标准做法:继承 ParserDelegator 并重写 parse 方法
}
super.read(in, doc, pos);
}
}
但这样可以避免双重解析,实际开发中,最常用且最简单的方法仍然是像第一个示例那样:手动调用 ParserDelegator.parse() 来获取一次性的解析通知,同时用 setText() 让编辑器正常显示。
如果你需要在每次 HTML 加载时都自动通知,建议结合 PropertyChangeListener(监听 page 属性变化)来触发手动解析。
- 核心类:
HTMLEditorKit.ParserCallback是通知接口。 - 核心方法:
handleStartTag,handleEndTag,handleText,handleComment,handleSimpleTag,handleError。 - 关键步骤:使用
ParserDelegator.parse()方法,传入你的回调对象。 - 注意事项:不要依赖
JEditorPane.setText()来触发自定义回调;pos参数在一次性字符串解析时可能为 0 或不精确。
希望这个解答能帮助你理解和使用 JEditorPane 的 HTML 解析通知机制。