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>"); } }
问题根源
- EDT线程被阻塞:你把等待
botThread结束的循环放到了SwingUtilities.invokeLater()中,这意味着整个循环在Swing的事件分发线程(EDT)上执行。EDT负责所有UI绘制和用户事件响应,一旦被这个耗时循环占用,就无法及时处理JEditorPane的更新请求,所有日志会积压到循环结束后才一次性刷新。 - 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
相关产品推荐
相关产品推荐

