Frontend Logging with JavaFX and Log4j 2
Although I’m developing Java EE applications with HTML5, I sometimes have to build Java SE applications with a Frontend. I’ve used Swing or AWT in the past. But since JavaFX 2.0 get lost of that awful JavaFX Script, JavaFX is my weapon of choice. Recently I wrote a multithreaded applications that needs to inform the user of some working results. I guess I was some kind of blue-eyed when I wrote this “log” function:
public static void guiLog(String logstring){
logTextArea.appendText(logstring);
}
and used it like that:
Platform.runLater(()->Controller.guiLog("Hello World :)"));
I still don’t know if that would’ve worked reliable in a single thread application, but in my case the GUI hangs after 30-45 seconds while using 8 threads. And to be honest: That kind of logging didn’t feel correct…
So I took a look at Log4j 2 appenders and I was not disappointed. Implementing your own appender, in my case a TextArea appender is quite easy.
import javafx.application.Platform;
import javafx.scene.control.TextArea;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.io.Serializable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* TextAreaAppender for Log4j 2
*/
@Plugin(
name = "TextAreaAppender",
category = "Core",
elementType = "appender",
printObject = true)
public final class TextAreaAppender extends AbstractAppender {
private static TextArea textArea;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
protected TextAreaAppender(String name, Filter filter,
Layout<? extends Serializable> layout,
final boolean ignoreExceptions) {
super(name, filter, layout, ignoreExceptions);
}
/**
* This method is where the appender does the work.
*
* @param event Log event with log data
*/
@Override
public void append(LogEvent event) {
readLock.lock();
final String message = new String(getLayout().toByteArray(event));
// append log text to TextArea
try {
Platform.runLater(() -> {
try {
if (textArea != null) {
if (textArea.getText().length() == 0) {
textArea.setText(message);
} else {
textArea.selectEnd();
textArea.insertText(textArea.getText().length(),
message);
}
}
} catch (final Throwable t) {
System.out.println("Error while append to TextArea: "
+ t.getMessage());
}
});
} catch (final IllegalStateException ex) {
ex.printStackTrace();
} finally {
readLock.unlock();
}
}
/**
* Factory method. Log4j will parse the configuration and call this factory
* method to construct the appender with
* the configured attributes.
*
* @param name Name of appender
* @param layout Log layout of appender
* @param filter Filter for appender
* @return The TextAreaAppender
*/
@PluginFactory
public static TextAreaAppender createAppender(
@PluginAttribute("name") String name,
@PluginElement("Layout") Layout<? extends Serializable> layout,
@PluginElement("Filter") final Filter filter) {
if (name == null) {
LOGGER.error("No name provided for TextAreaAppender");
return null;
}
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}
return new TextAreaAppender(name, filter, layout, true);
}
/**
* Set TextArea to append
*
* @param textArea TextArea to append
*/
public static void setTextArea(TextArea textArea) {
TextAreaAppender.textArea = textArea;
}
}
The only thing left is to announce the appender in the Log4j configuration
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<TextAreaAppender name="JavaFXLogger">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %c{1}:%L - %m%n"/>
</TextAreaAppender>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="JavaFXLogger"/>
</Root>
</Loggers>
</Configuration>
Happy logging ;) !