通过具有简单自定义日志记录框架的线程将消息记录到 JavaFX TextArea 的最有效方法 [英] Most efficient way to log messages to JavaFX TextArea via threads with simple custom logging frameworks

查看:38
本文介绍了通过具有简单自定义日志记录框架的线程将消息记录到 JavaFX TextArea 的最有效方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a simple custom logging framework like this:

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 + "
");
    }

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

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

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

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

现在我想做的是扩展它,以便它能够通过线程正确处理消息记录.我尝试过诸如使用带有 AnimationTimer 的消息队列 之类的解决方案.它可以工作,但会减慢 GUI.

Now what I want to do is to extend it so that it would be able to properly handle logging of messages via threads. I have tried solutions like using message queues with an AnimationTimer. It works but it slows the GUI down.

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

I also tried using a scheduled service which runs a thread that reads messages from the message queue, concatenates them, and appends them to TextArea (textArea.appendText(stringBuilder.toString())). The problem is that the TextArea control goes unstable i.e. you had to highlight all texts with Ctrl-A and try resizing the window to make them appear well. There are also some of them being displayed in a light-blue background not sure what's causing it. My first guess here is that the race condition may not be allowing the control to update itself well from the new strings. It is also worth noting that the textarea is wrapped around a ScrollPane so it adds the confusion if TextArea is actually the one having the problem or ScrollPane. I have to mention as well that this approach doesn't make the TextArea control update itself with messages quickly.

我想过将 binding TextArea.TextProperty() 绑定到一些可以进行更新的东西,但我不知道如何正确地做到这一点,知道消息的收集者(无论是通过服务还是单独的线程)仍会以与 GUI 线程不同的方式运行.

I thought about binding TextArea.TextProperty() to something that does the update but I'm not sure how I would do that properly knowing that the gatherer of messages (be it by a service or a lone thread) would still be running different from the GUI thread.

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

I have tried to look up on other known logging framework solutions like log4j and some stuffs referred here but none of them seems to give an apparent approach to logging via threads to TextArea. I also don't like the idea of building my logging system on top of them as they already have their pre-defined mechanisms like logging level, etc.

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

I've seen this as well. It implies using SwingUtilities.invokeLater(Runnable) to update the control but I already tried a similar approach using javafx.application.platform.runLater() which gets executed on the worker thread. I'm not sure if there was something I was doing wrong but it just hangs. It can produce messages but not when they're aggressive enough. I estimate that the worker thread running in a purely synchronous fashion can actually produce about 20 or more average lines per second and more when it's in debug mode. A possible workaround would be to add message queueing to it as well but that doesn't make sense anymore.

推荐答案

log-view.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);
    }
}

<小时>

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


The section below on selectable text is supplemental to the solution posted above. If you don't need selectable text, you can ignore the selection below.

是否可以使文本可选?

有几个不同的选项:

  1. 它是一个 ListView,所以你可以使用一个 multipe selection model,确保 CSS 配置为根据需要适当地设置所选行的样式.这将逐行选择,而不是直接选择文本.您可以向选择模型中的选定项添加侦听器,并在发生变化时进行适当的处​​理.
  2. 您可以为 ListView 使用工厂,它将每个单元格设置为适当样式的只读文本字段.这将允许某人只选择一行中的一部分文本而不是整行.但他们无法一次性选择多行文本.
  1. It is a ListView, so you could use a multipe selection model, ensuring the CSS is configured to appropriately style the selected rows as you wish. That will do a row by row selection, not a straight text selection. You can add a listener to the selected items in the selection model and do appropriate processing when that changes.
  2. You could use a factory for the ListView which sets each cell to an appropriately styles read-only text field. That would allow somebody to select just a portion of text within a row rather than a whole row. But they wouldn't be able to select text across multiple rows in one go.
    • Copiable Label/TextField/LabeledText in JavaFX

尝试实施适合您的文本选择方法,如果您无法使用它,请创建一个特定于可选文本日志的新问题,并带有 mcve.

Try implementing the text selection approach which is appropriate for you and, if you can't get it to work, create a new question specific to selectable text logs, with a mcve.

这篇关于通过具有简单自定义日志记录框架的线程将消息记录到 JavaFX TextArea 的最有效方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆