Самый эффективный способ протоколирования сообщений в JavaFX TextArea через streamи с помощью простых пользовательских фреймворков регистрации

У меня есть простая пользовательская структура ведения журналов:

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

Теперь я хочу расширить его, чтобы он мог правильно обрабатывать ведение журнала сообщений через streamи. Я пробовал решения, такие как использование очередей сообщений с AnimationTimer . Он работает, но он замедляет работу графического интерфейса.

Я также попытался использовать запланированную службу, которая запускает stream, который читает сообщения из очереди сообщений, объединяет их и добавляет в TextArea ( textArea.appendText(stringBuilder.toString()) ). Проблема в том, что элемент управления TextArea нестабилен, т. Е. Вам нужно выделить все тексты с помощью Ctrl-A и попробовать resize windows, чтобы они выглядели хорошо. Есть также некоторые из них, отображаемые на светло-голубом фоне, не уверенные, что вызывает его. Мое первое предположение заключается в том, что условие гонки может не позволить контролеру хорошо обновляться из новых строк. Также стоит отметить, что текстовое поле обернуто вокруг ScrollPane, поэтому он добавляет путаницу, если TextArea на самом деле тот, у кого есть проблема, или ScrollPane. Я должен также упомянуть, что этот подход не делает обновление управления TextArea само по себе сообщениями.

Я думал о binding TextArea.TextProperty() к тому, что делает это обновление, но я не уверен, как бы я сделал это правильно, зная, что сборщик сообщений (будь то служба или одинокий stream) все равно будет отличаться от stream графического интерфейса пользователя.

Я попытался найти другие известные решения для ведения журнала, такие как log4j и некоторые материалы, упомянутые здесь, но ни один из них, похоже, не дает явного подхода к регистрации через streamи в TextArea. Мне также не нравится идея создания моей системы ведения журнала поверх них, поскольку у них уже есть предопределенные механизмы, такие как уровень ведения журнала и т. Д.

Я тоже это видел. Это подразумевает использование SwingUtilities.invokeLater(Runnable) для обновления SwingUtilities.invokeLater(Runnable) управления, но я уже пробовал аналогичный подход, используя javafx.application.platform.runLater() который выполняется в рабочем streamе. Я не уверен, было ли что-то, что я делал неправильно, но это просто зависает. Он может создавать сообщения, но не тогда, когда они достаточно агрессивны. Я считаю, что рабочий stream, работающий чисто синхронно, может фактически производить около 20 или более средних строк в секунду и более, когда он находится в режиме отладки. Возможным обходным решением было бы добавить к нему очередь сообщений, но это уже не имеет смысла.

срубы 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 log = new LinkedBlockingDeque<>(MAX_LOG_ENTRIES); public void drainTo(Collection 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 { 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 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 logItems = FXCollections.observableArrayList(); public BooleanProperty showTimeStampProperty() { return showTimestamp; } public ObjectProperty 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( logItems, logRecord -> logRecord.getLevel().ordinal() >= filterLevel.get().ordinal() ) ); }); filterLevel.set(Level.DEBUG); setCellFactory(param -> new ListCell() { { 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 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, поэтому вы можете использовать модель выбора multipe , гарантируя, что CSS настроен на надлежащий стиль выбранных строк по вашему желанию. Это будет делать выбор по ряду строк, а не прямой выбор текста. Вы можете добавить слушателя к выбранным элементам в модели выбора и выполнить соответствующую обработку, когда это изменится.
  2. Вы можете использовать фабрику для ListView, которая устанавливает каждую ячейку в соответствующее текстовое поле только для чтения. Это позволит кому-то выбрать только часть текста внутри строки, а не целую строку. Но они не смогут выбирать текст из нескольких строк за один раз.
    • Копируемый ярлык / TextField / LabeledText в JavaFX
  3. Вместо ListView вы можете использовать эту реализацию на стороннем элементе управления RichTextFX только для чтения, что позволит выбирать текст из нескольких строк.

Попробуйте применить подход выбора текста, который подходит вам, и, если вы не можете заставить его работать, создайте новый вопрос, специфичный для выбираемых текстовых журналов, с mcve .

  • Ожидание списка Будущего
  • Как вы убиваете Thread в Java?
  • Когда Java Thread.sleep запускает InterruptedException?
  • Неблокирующий ввод-вывод действительно быстрее, чем многопоточный блокирующий ввод-вывод? Как?
  • Как отменить будущее в Scala?
  • Каковы основные виды использования yield () и чем оно отличается от join () и interrupt ()?
  • Android: Тост в streamе
  • Давайте будем гением компьютера.