通过具有简单自定义日志记录框架的线程将消息记录到 JavaFX TextArea 的最有效方式

2022-09-04 19:18:11

我有一个简单的自定义日志记录框架,如下所示:

package something;

import javafx.scene.control.TextArea;

public class MyLogger {
    public final TextArea textArea;

    private boolean verboseMode = false;
    private boolean debugMode = false;

    public MyLogger(final TextArea textArea) {
        this.textArea = textArea;
    }

    public MyLogger setVerboseMode(boolean value) {
        verboseMode = value;
        return this;
    }

    public MyLogger setDebugMode(boolean value) {
        debugMode = value;
        return this;
    }

    public boolean writeMessage(String msg) {
        textArea.appendText(msg);
        return true;
    }

    public boolean logMessage(String msg) {
        return writeMessage(msg + "\n");
    }

    public boolean logWarning(String msg) {
        return writeMessage("Warning: " + msg + "\n");
    }

    public boolean logError(String msg) {
        return writeMessage("Error: " + msg + "\n");
    }

    public boolean logVerbose(String msg) {
        return verboseMode ? writeMessage(msg + "\n") : true;
    }

    public boolean logDebug(String msg) {
        return debugMode ? writeMessage("[DEBUG] " + msg + "\n") : true;
    }
}

现在我想做的是扩展它,以便它能够通过线程正确处理消息的日志记录。我尝试过解决方案,例如将消息队列与动画计时器一起使用。它可以工作,但它减慢了GUI的速度。

我还尝试使用调度服务,该服务运行一个线程,该线程从消息队列中读取消息,连接它们,并将它们附加到TextArea()。问题是TextArea控件不稳定,即您必须突出显示所有文本并尝试调整窗口大小以使其显示良好。还有一些它们以浅蓝色背景显示,不确定是什么原因造成的。我在这里的第一个猜测是,争用条件可能不允许控件从新字符串很好地更新自身。同样值得注意的是,文本区域被包裹在ScrollPane周围,因此如果TextArea实际上是有问题的区域或ScrollPane,则会增加混乱。我还必须提到,这种方法不会使TextArea控件快速更新消息。textArea.appendText(stringBuilder.toString())Ctrl-A

我想到了做更新的事情,但我不确定如何正确地做到这一点,因为知道消息的收集器(无论是通过服务还是单独的线程)仍然会与GUI线程运行不同。bindingTextArea.TextProperty()

我试图查找其他已知的日志记录框架解决方案,如log4j和这里提到的一些东西,但它们似乎都没有给出一种通过线程记录到TextArea的明显方法。我也不喜欢在它们之上构建我的日志记录系统的想法,因为它们已经具有预定义的机制,如日志记录级别等。

我也看到了这一点。它意味着 使用 来更新控件,但我已经尝试了类似的方法,使用它在工作线程上执行。我不确定我是否做错了什么,但它只是挂起了。它可以生成消息,但当它们足够激进时就不生成了。我估计,以纯同步方式运行的工作线程实际上每秒可以产生大约 20 个或更多的平均行,而当它处于调试模式时,它会产生更多。一种可能的解决方法是向其添加消息队列,但这不再有意义了。SwingUtilities.invokeLater(Runnable)javafx.application.platform.runLater()


答案 1

日志视图.css

.root {
    -fx-padding: 10px;
}

.log-view .list-cell {
    -fx-background-color: null; // removes alternating list gray cells.
}

.log-view .list-cell:debug {
    -fx-text-fill: gray;
}

.log-view .list-cell:info {
    -fx-text-fill: green;
}

.log-view .list-cell:warn {
    -fx-text-fill: purple;
}

.log-view .list-cell:error {
    -fx-text-fill: red;
}

LogViewer.java

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

class Log {
    private static final int MAX_LOG_ENTRIES = 1_000_000;

    private final BlockingDeque<LogRecord> log = new LinkedBlockingDeque<>(MAX_LOG_ENTRIES);

    public void drainTo(Collection<? super LogRecord> collection) {
        log.drainTo(collection);
    }

    public void offer(LogRecord record) {
        log.offer(record);
    }
}

class Logger {
    private final Log log;
    private final String context;

    public Logger(Log log, String context) {
        this.log = log;
        this.context = context;
    }

    public void log(LogRecord record) {
        log.offer(record);
    }

    public void debug(String msg) {
        log(new LogRecord(Level.DEBUG, context, msg));
    }

    public void info(String msg) {
        log(new LogRecord(Level.INFO, context, msg));
    }

    public void warn(String msg) {
        log(new LogRecord(Level.WARN, context, msg));
    }

    public void error(String msg) {
        log(new LogRecord(Level.ERROR, context, msg));
    }

    public Log getLog() {
        return log;
    }
}

enum Level { DEBUG, INFO, WARN, ERROR }

class LogRecord {
    private Date   timestamp;
    private Level  level;
    private String context;
    private String message;

    public LogRecord(Level level, String context, String message) {
        this.timestamp = new Date();
        this.level     = level;
        this.context   = context;
        this.message   = message;
    }

    public Date getTimestamp() {
        return timestamp;
    }

    public Level getLevel() {
        return level;
    }

    public String getContext() {
        return context;
    }

    public String getMessage() {
        return message;
    }
}

class LogView extends ListView<LogRecord> {
    private static final int MAX_ENTRIES = 10_000;

    private final static PseudoClass debug = PseudoClass.getPseudoClass("debug");
    private final static PseudoClass info  = PseudoClass.getPseudoClass("info");
    private final static PseudoClass warn  = PseudoClass.getPseudoClass("warn");
    private final static PseudoClass error = PseudoClass.getPseudoClass("error");

    private final static SimpleDateFormat timestampFormatter = new SimpleDateFormat("HH:mm:ss.SSS");

    private final BooleanProperty       showTimestamp = new SimpleBooleanProperty(false);
    private final ObjectProperty<Level> filterLevel   = new SimpleObjectProperty<>(null);
    private final BooleanProperty       tail          = new SimpleBooleanProperty(false);
    private final BooleanProperty       paused        = new SimpleBooleanProperty(false);
    private final DoubleProperty        refreshRate   = new SimpleDoubleProperty(60);

    private final ObservableList<LogRecord> logItems = FXCollections.observableArrayList();

    public BooleanProperty showTimeStampProperty() {
        return showTimestamp;
    }

    public ObjectProperty<Level> filterLevelProperty() {
        return filterLevel;
    }

    public BooleanProperty tailProperty() {
        return tail;
    }

    public BooleanProperty pausedProperty() {
        return paused;
    }

    public DoubleProperty refreshRateProperty() {
        return refreshRate;
    }

    public LogView(Logger logger) {
        getStyleClass().add("log-view");

        Timeline logTransfer = new Timeline(
                new KeyFrame(
                        Duration.seconds(1),
                        event -> {
                            logger.getLog().drainTo(logItems);

                            if (logItems.size() > MAX_ENTRIES) {
                                logItems.remove(0, logItems.size() - MAX_ENTRIES);
                            }

                            if (tail.get()) {
                                scrollTo(logItems.size());
                            }
                        }
                )
        );
        logTransfer.setCycleCount(Timeline.INDEFINITE);
        logTransfer.rateProperty().bind(refreshRateProperty());

        this.pausedProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue && logTransfer.getStatus() == Animation.Status.RUNNING) {
                logTransfer.pause();
            }

            if (!newValue && logTransfer.getStatus() == Animation.Status.PAUSED && getParent() != null) {
                logTransfer.play();
            }
        });

        this.parentProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue == null) {
                logTransfer.pause();
            } else {
                if (!paused.get()) {
                    logTransfer.play();
                }
            }
        });

        filterLevel.addListener((observable, oldValue, newValue) -> {
            setItems(
                    new FilteredList<LogRecord>(
                            logItems,
                            logRecord ->
                                logRecord.getLevel().ordinal() >=
                                filterLevel.get().ordinal()
                    )
            );
        });
        filterLevel.set(Level.DEBUG);

        setCellFactory(param -> new ListCell<LogRecord>() {
            {
                showTimestamp.addListener(observable -> updateItem(this.getItem(), this.isEmpty()));
            }

            @Override
            protected void updateItem(LogRecord item, boolean empty) {
                super.updateItem(item, empty);

                pseudoClassStateChanged(debug, false);
                pseudoClassStateChanged(info, false);
                pseudoClassStateChanged(warn, false);
                pseudoClassStateChanged(error, false);

                if (item == null || empty) {
                    setText(null);
                    return;
                }

                String context =
                        (item.getContext() == null)
                                ? ""
                                : item.getContext() + " ";

                if (showTimestamp.get()) {
                    String timestamp =
                            (item.getTimestamp() == null)
                                    ? ""
                                    : timestampFormatter.format(item.getTimestamp()) + " ";
                    setText(timestamp + context + item.getMessage());
                } else {
                    setText(context + item.getMessage());
                }

                switch (item.getLevel()) {
                    case DEBUG:
                        pseudoClassStateChanged(debug, true);
                        break;

                    case INFO:
                        pseudoClassStateChanged(info, true);
                        break;

                    case WARN:
                        pseudoClassStateChanged(warn, true);
                        break;

                    case ERROR:
                        pseudoClassStateChanged(error, true);
                        break;
                }
            }
        });
    }
}

class Lorem {
    private static final String[] IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque hendrerit imperdiet mi quis convallis. Pellentesque fringilla imperdiet libero, quis hendrerit lacus mollis et. Maecenas porttitor id urna id mollis. Suspendisse potenti. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras lacus tellus, semper hendrerit arcu quis, auctor suscipit ipsum. Vestibulum venenatis ante et nulla commodo, ac ultricies purus fringilla. Aliquam lectus urna, commodo eu quam a, dapibus bibendum nisl. Aliquam blandit a nibh tincidunt aliquam. In tellus lorem, rhoncus eu magna id, ullamcorper dictum tellus. Curabitur luctus, justo a sodales gravida, purus sem iaculis est, eu ornare turpis urna vitae dolor. Nulla facilisi. Proin mattis dignissim diam, id pellentesque sem bibendum sed. Donec venenatis dolor neque, ut luctus odio elementum eget. Nunc sed orci ligula. Aliquam erat volutpat.".split(" ");
    private static final int MSG_WORDS = 8;
    private int idx = 0;

    private Random random = new Random(42);

    synchronized public String nextString() {
        int end = Math.min(idx + MSG_WORDS, IPSUM.length);

        StringBuilder result = new StringBuilder();
        for (int i = idx; i < end; i++) {
            result.append(IPSUM[i]).append(" ");
        }

        idx += MSG_WORDS;
        idx = idx % IPSUM.length;

        return result.toString();
    }

    synchronized public Level nextLevel() {
        double v = random.nextDouble();

        if (v < 0.8) {
            return Level.DEBUG;
        }

        if (v < 0.95) {
            return Level.INFO;
        }

        if (v < 0.985) {
            return Level.WARN;
        }

        return Level.ERROR;
    }

}

public class LogViewer extends Application {
    private final Random random = new Random(42);

    @Override
    public void start(Stage stage) throws Exception {
        Lorem  lorem  = new Lorem();
        Log    log    = new Log();
        Logger logger = new Logger(log, "main");

        logger.info("Hello");
        logger.warn("Don't pick up alien hitchhickers");

        for (int x = 0; x < 20; x++) {
            Thread generatorThread = new Thread(
                    () -> {
                        for (;;) {
                            logger.log(
                                    new LogRecord(
                                            lorem.nextLevel(),
                                            Thread.currentThread().getName(),
                                            lorem.nextString()
                                    )
                            );

                            try {
                                Thread.sleep(random.nextInt(1_000));
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                        }
                    },
                    "log-gen-" + x
            );
            generatorThread.setDaemon(true);
            generatorThread.start();
        }

        LogView logView = new LogView(logger);
        logView.setPrefWidth(400);

        ChoiceBox<Level> filterLevel = new ChoiceBox<>(
                FXCollections.observableArrayList(
                        Level.values()
                )
        );
        filterLevel.getSelectionModel().select(Level.DEBUG);
        logView.filterLevelProperty().bind(
                filterLevel.getSelectionModel().selectedItemProperty()
        );

        ToggleButton showTimestamp = new ToggleButton("Show Timestamp");
        logView.showTimeStampProperty().bind(showTimestamp.selectedProperty());

        ToggleButton tail = new ToggleButton("Tail");
        logView.tailProperty().bind(tail.selectedProperty());

        ToggleButton pause = new ToggleButton("Pause");
        logView.pausedProperty().bind(pause.selectedProperty());

        Slider rate = new Slider(0.1, 60, 60);
        logView.refreshRateProperty().bind(rate.valueProperty());
        Label rateLabel = new Label();
        rateLabel.textProperty().bind(Bindings.format("Update: %.2f fps", rate.valueProperty()));
        rateLabel.setStyle("-fx-font-family: monospace;");
        VBox rateLayout = new VBox(rate, rateLabel);
        rateLayout.setAlignment(Pos.CENTER);

        HBox controls = new HBox(
                10,
                filterLevel,
                showTimestamp,
                tail,
                pause,
                rateLayout
        );
        controls.setMinHeight(HBox.USE_PREF_SIZE);

        VBox layout = new VBox(
                10,
                controls,
                logView
        );
        VBox.setVgrow(logView, Priority.ALWAYS);

        Scene scene = new Scene(layout);
        scene.getStylesheets().add(
            this.getClass().getResource("log-view.css").toExternalForm()
        );
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

下面有关可选文本的部分是对上面发布的解决方案的补充。如果不需要可选文本,可以忽略下面的选择。

是否可以使文本可选?

有几种不同的选择:

  1. 它是一个 ListView,因此您可以使用多选模型,确保将 CSS 配置为根据需要适当地设置所选行的样式。这将逐行选择,而不是直接的文本选择。您可以向选择模型中的选定项添加侦听器,并在更改时执行适当的处理。
  2. 您可以对 ListView 使用工厂,它将每个单元格设置为适当的样式只读文本字段。这将允许某人只选择一行中的一部分文本,而不是整行。但是他们无法一次在多行中选择文本。
  1. 您可以基于第三方只读的 RichTextFX 控件而不是 ListView,该控件允许跨多行选择文本。

尝试实现适合您的文本选择方法,如果无法使其正常工作,请使用 mcve 创建一个特定于可选文本日志的新问题。


答案 2