You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swing EditorPane无法每秒输出线程终止倒计时日志问题排查

问题:JEditorPane在Bot停止时一次性输出所有日志,而非实时更新

我正在开发一款带“启动”按钮的GUI程序,点击启动按钮会启动botThread,按钮同步切换为停止按钮。点击停止按钮时,希望JEditorPane像控制台那样每秒输出一条日志(格式如下),但控制台能正常每秒输出一行,JEditorPane却要等到botThread完全终止后才一次性输出所有日志。

期望的日志输出格式:

19:10:40.027 [AWT-EventQueue-0] INFO  bot.StartStopListener - Stopping bot...
19:10:41.096 [AWT-EventQueue-0] INFO  bot.StartStopListener - Stopping bot...(1s)
19:10:42.099 [AWT-EventQueue-0] INFO  bot.StartStopListener - Stopping bot...(2s)
19:10:43.158 [AWT-EventQueue-0] INFO  bot.StartStopListener - Stopping bot...(3s)
19:10:44.160 [AWT-EventQueue-0] INFO  bot.StartStopListener - Stopping bot...(4s)
19:10:45.106 [AWT-EventQueue-0] INFO  bot.StartStopListener - Bot stopped

相关代码

停止按钮触发代码

stop = true;
settingsPanel.getStartButton().setEnabled(false);
LOGGER.info("Stopping bot...");

//wait for the bot thread to die
SwingUtilities.invokeLater(() -> {
    long last = System.currentTimeMillis();
    int seconds = 0;
    while(botThread.isAlive()){
        try {
            long now = System.currentTimeMillis();
            if(now-last > 1000){
                seconds++;
                last = now;
                LOGGER.info(String.format("Stopping bot...(%ds)", seconds));
            }
            Thread.sleep(50);
        } catch (InterruptedException ex) {
            LOGGER.error("BotThread Interrupted");
            throw new RuntimeException(ex);
        }
    }
    LOGGER.info("Bot stopped");
});

logback.xml配置

<appender name="TEXTAREA" class="io.logging.EditorPaneAppender"/>

EditorPaneAppender类代码

public class EditorPaneAppender extends AppenderBase<ILoggingEvent> {

    private final static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
    public final static JEditorPane editorPane;

    static{
        editorPane = new JEditorPane();
        editorPane.setEditorKit(JEditorPane.createEditorKitForContentType("text/html"));
        editorPane.setEditable(false);
    }

    @Override
    protected void append(final ILoggingEvent e) {
        String txt = editorPane.getText();
        txt = txt.replace("<html>","")
                .replace("</html>","")
                .replace("<body>","")
                .replace("</body>","") // im too lazy to do it properly
                .replace("<head>","")
                .replace("</head>","")
                .replace("<p style=\"margin-top: 0\">","")
                .replace("</p>","");


        String colored_level_template = switch (e.getLevel().toInt()) {
            case 20000 -> //info
                    "<span style=\"color:2259E3FF\">%s</span>";
            case 30000 -> //warn
                    "<span style=\"color:E38E0AFF\">%s</span>";
            case 40000 -> //error
                    "<span style=\"color:E71019FF\">%s</span>";
            default -> "";
        };

        editorPane.setText(txt + String.format("<span>[%s]</span> %s - %s"
                , sdf.format(new Date(e.getTimeStamp()))
                , String.format(colored_level_template, e.getLevel())
                , e.getFormattedMessage()) + "<br>");
    }

}

问题根源

  1. EDT线程被阻塞:你把等待botThread结束的循环放到了SwingUtilities.invokeLater()中,这意味着整个循环在Swing的事件分发线程(EDT)上执行。EDT负责所有UI绘制和用户事件响应,一旦被这个耗时循环占用,就无法及时处理JEditorPane的更新请求,所有日志会积压到循环结束后才一次性刷新。
  2. UI更新未即时触发:即使调用了editorPane.setText(),在EDT被阻塞的情况下,Swing无法立即执行重绘操作,只能等EDT空闲后统一处理所有积压的UI更新。

修复方案

方案1:将等待逻辑移到后台线程

把等待botThread的循环放到独立的后台线程中,仅将UI操作(比如按钮状态恢复)委托给EDT执行:

stop = true;
settingsPanel.getStartButton().setEnabled(false);
LOGGER.info("Stopping bot...");

// 后台线程执行等待逻辑,避免阻塞EDT
new Thread(() -> {
    long last = System.currentTimeMillis();
    int seconds = 0;
    while(botThread.isAlive()){
        try {
            long now = System.currentTimeMillis();
            if(now-last > 1000){
                seconds++;
                last = now;
                LOGGER.info(String.format("Stopping bot...(%ds)", seconds));
            }
            Thread.sleep(50);
        } catch (InterruptedException ex) {
            LOGGER.error("BotThread Interrupted");
            throw new RuntimeException(ex);
        }
    }
    LOGGER.info("Bot stopped");
    // 恢复按钮状态必须在EDT中执行
    SwingUtilities.invokeLater(() -> settingsPanel.getStartButton().setEnabled(true));
}).start();

方案2:确保UI更新在EDT中执行

Logback的Appender默认可能在后台线程调用append方法,直接操作Swing组件存在线程安全问题,同时会导致更新不及时。修改EditorPaneAppender的append方法,将UI更新逻辑包装到SwingUtilities.invokeLater()中:

@Override
protected void append(final ILoggingEvent e) {
    // 将UI更新操作委托给EDT执行
    SwingUtilities.invokeLater(() -> {
        String txt = editorPane.getText();
        txt = txt.replace("<html>","")
                .replace("</html>","")
                .replace("<body>","")
                .replace("</body>","")
                .replace("<head>","")
                .replace("</head>","")
                .replace("<p style=\"margin-top: 0\">","")
                .replace("</p>","");

        String colored_level_template = switch (e.getLevel().toInt()) {
            case 20000 -> "<span style=\"color:2259E3FF\">%s</span>";
            case 30000 -> "<span style=\"color:E38E0AFF\">%s</span>";
            case 40000 -> "<span style=\"color:E71019FF\">%s</span>";
            default -> "";
        };

        editorPane.setText(txt + String.format("<span>[%s]</span> %s - %s"
                , sdf.format(new Date(e.getTimeStamp()))
                , String.format(colored_level_template, e.getLevel())
                , e.getFormattedMessage()) + "<br>");
    });
}

额外建议

  • 绝对不要在EDT中执行任何耗时操作(比如循环等待、IO操作),必须保持EDT空闲以响应用户操作和UI更新。
  • 所有Swing组件的修改操作都应该通过SwingUtilities.invokeLater()或invokeAndWait()在EDT中执行,这是Swing线程安全的基本要求。

内容的提问来源于stack exchange,提问作者smilly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 11:31:01