使用简单的自定义日志logging框架,通过线程将消息logging到JavaFX TextArea的最有效方式

我有一个简单的自定义日志框架是这样的:

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; } } 

现在我想要做的就是扩展它,以便它能够正确处理通过线程的消息日志logging。 我已经尝试使用animation定时器使用消息队列的解决scheme。 它可以工作,但会降低GUI的速度。

我也尝试使用计划的服务 ,它运行从消息队列中读取消息的线程,连接它们,并将它们附加到TextArea( textArea.appendText(stringBuilder.toString()) )。 问题是TextArea控件变得不稳定,也就是说你必须用Ctrl-A来突出显示所有的文本,然后尝试调整窗口的大小,使它们看起来很好。 还有一些在浅蓝色的背景中显示,不知道是什么原因造成的。 我的第一个猜测是竞赛条件可能不允许控制从新string更新自己。 还值得注意的是,textarea包裹在ScrollPane中,所以如果TextArea实际上是有问题或ScrollPane的话,就会增加混淆。 我不得不提一下,这种方法并不能使TextArea控件快速更新自己的消息。

我想binding TextArea.TextProperty()到更新的东西,但我不知道我会怎么做,正确知道收集的消息(是由服务或孤独的线程)仍然会运行不同GUI线程。

我试图查找其他已知的日志框架解决scheme,如log4j和这里提到的一些东西,但没有一个似乎给出了一个明显的方法来通过线程login到TextArea。 我也不喜欢在他们之上build立我的日志logging系统的想法,因为他们已经有了预定义的机制,比如日志级别等等。

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

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