simple logging facade for java to closure compiler
Provide the familiar slf4j API and use it in j2cl compatible code. Especially in shared code, which can run on the client (as Javascript) and in the backend (as Java).
This lib is a drop-in replacement for slf4j. Even package names are kept the same (based on slf4j 1.7.36).
How this library was created
-
Import sources of slf4j 1.7.36
-
Add custom
pom.xml
and this document to document the creation process -
A naive j2cl compile lists 65 issues in
stripped_sources
for our code. -
Adding a
Platform
which encapsulates the diff between script-world and jre-world worked fine for the first issue, J2cl compile reports 64 remaining issues. -
Creating the actual log-implementation forwarding to
elemental2.dom.Console.debug/info/warn/error
✔️️ = Feature can be used in script mode
❌ = Feature not available in script mode
-
✔️
org.slf4j.event.EventRecodingLogger.java
:-
Issue: Thread cannot be resolved
-
Idea: In script-mode, the thread name is now always
n/a
.
-
-
-
✔️
org.slf4j.helpers.BasicMarker.java
:-
Issue: CopyOnWriteArrayList cannot be resolved to a type
-
Idea: Use a simple
ArrayList
in script-mode
-
-
-
❌
org.slf4j.helpers.BasicMDCAdapter.java
:-
Issue: InheritableThreadLocal cannot be resolved to a type
-
Comment: Although the JRE emulation includes a
java.lang.ThreadLocal
, there is noInheritableThreadLocal
. -
Idea: GwtIncompatible the whole class.
-
-
-
✔️
org.slf4j.helpers.MessageFormatter.java
:-
The import java.text.MessageFormat cannot be resolved
-
Idea: Was just imported for the JavaDocs. Added full package name to JavaDoc.
-
-
-
❌
org.slf4j.helpers.NamedLoggerBase.java
:-
The import java.io.ObjectStreamException cannot be resolved
-
Comment: Not part of JRE emulation. Used in readResolve() method, which is used when objects — here the logger instances — are deserialized. This feature is not required in script-mode.
-
Idea: GwtIncompatible the method
-
-
-
✔️
org.slf4j.helpers.SubstituteLogger.java
:-
Missing imports: java.lang.reflect.InvocationTargetException, java.lang.reflect.Method, java.util.concurrent.LinkedBlockingQueue
-
Missing types: IllegalAccessException, NoSuchMethodException
-
The method getMethod(String, Class<LoggingEvent>) is undefined for the type Class<capture#3-of ? extends Logger>
-
Idea: Too much reflection to work around;
@GwtIncompatible
this class and its factorySubstituteLoggerFactory
-
Issue: Used in many places
-
Idea: Provide a script-impl which is never delegate-aware and always sends
log(LoggingEvent event)
to nowhere. Normal debug/info/… log calls are forwarded to the underlying delegate just fine.
-
-
-
-
-
✔️
org.slf4j.helpers.Util.java
:-
SecurityManager cannot be resolved to a type
-
Idea: Just
@GwtIncompatible
these parts and deal with the call-sites later. Most features work.
-
-
-
✔️
org.slf4j.LoggerFactory.java
:-
✔️ binding phase
-
Missing imports/type: java.net.URL, ClassLoader, java.lang.NoSuchMethodError, NoSuchFieldError, NoClassDefFoundError
-
The method findPossibleStaticLoggerBinderPathSet() from the type LoggerFactory refers to the missing type URL
-
The method getClassLoader() is undefined for the type Class<LoggerFactory>
-
Idea: skip binding and hard-wire to a built-in logger
-
-
-
✔️ replay events after binding
-
The import java.util.concurrent.LinkedBlockingQueue cannot be resolved
-
Idea: replace
LinkedBlockingQueue
withQueue
and add a helper method fordrainTo
to drain a Queue to thePlatform
.
-
-
-
❌ detect logger name mismatch
-
The method format(String, String, String) is undefined for the type String
-
Idea: remove this feature in script mode
-
-
-
-
✔️
org.slf4j.MarkerFactory.java
:-
binding
-
NoClassDefFoundError cannot be resolved to a type
-
NoSuchMethodError cannot be resolved to a type
-
The method bwCompatibleGetMarkerFactoryFromBinder() from the type MarkerFactory refers to the missing type NoClassDefFoundError
-
Idea: hard-code binding
-
-
-
-
✔️
org.slf4j.MDC.java
:-
binding
-
NoClassDefFoundError cannot be resolved to a type
-
NoSuchMethodError cannot be resolved to a type
-
The method bwCompatibleGetMDCAdapterFromBinder() from the type MDC refers to the missing type NoClassDefFoundError
-
Idea: hard-code binding
-
-
-
The JRE whitelist http://www.gwtproject.org/doc/latest/RefJreEmulation.html is really important but not very detailed.
E.g. System.getProperty(String key)
works only, when the given key is known at compile-time.
When instances of AAA should behave different when run in JRE vs. when run in script-mode.
class AAA_script {
void aaa() { /* shared-code implementation for script-mode */ }
}
class AAA extends AAA_script {
@GwtIncompatible
@Override
void aaa() { /* JRE-only implementation */ }
}
class AAA_script { void aaa() { /* shared-code implementation for script-mode */ } } class AAA extends AAA_script { }
AAA a = new AAA(); a.aaa(); // -> polymorphism at work
-
The JRE sees the full code and calls the
AAA.aaa()
impl -
J2CL sees the method as not being overriden and calls
AAA_script.aaa()
As static code cannot overwrite methods, we need to introduce variance points using instances. This transformation was used in several places:
static class AAA { void bbb() { // do JRE-specific stuff } void ccc() { // some code that calls bbb bbb(); } }
static class AAA { private static Vary VARY = new Vary(); private static class Vary_script { void bbb() { // a j2cl-compatible shared code way to do 'bbb' // or do nothing } } private static class Vary implements Vary_script { @GwtIncompatible @Overrride void bbb() { // do JRE-specific stuff } } void ccc() { // some code that calls bbb VARY.bbb(); } }
This is a weird one, but it seems to work.
Use when: You want to run client-side code using e.g. elemental2.dom.Console
in JRE
@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public class Console {
public native void debug(Object... var_data);
}
public class Console_script {
public void debug(Object... var_data) {
DomGlobals.console.debug(var_data);
}
}
public class Console_wrapped extends Console_script {
@Override
@GwtIncompatible
public void debug(Object... var_data) {
System.out.println("DEBUG " + Arrays.toString(var_data));
}
}
public class Console_script {
public void debug(Object... var_data) {
DomGlobals.console.debug(var_data);
}
}
public class Console_wrapped extends Console_script {
}
This is actually delegating twice. Quite cumbersome.
Caveat: Your code needs to use Console_wrapped
instead of Console
.