Skip to content

Latest commit

 

History

History
228 lines (182 loc) · 7.27 KB

README.adoc

File metadata and controls

228 lines (182 loc) · 7.27 KB

slf4j2cl

simple logging facade for java to closure compiler

Provide the familar 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).

Work Process

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

Issues and Solutions

Legend

✔️️ = Feature can be used in script mode
❌ = Feature not available in script mode

Issues
  1. ✔️ org.slf4j.event.EventRecodingLogger.java:

    • Issue: Thread cannot be resolved

      • Idea: In script-mode, the thread name is now always n/a.

  2. ✔️ org.slf4j.helpers.BasicMarker.java:

    • Issue: CopyOnWriteArrayList cannot be resolved to a type

      • Idea: Use a simple ArrayList in script-mode

  3. 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 no InheritableThreadLocal.

      • Idea: GwtIncompatible the whole class.

  4. ✔️ 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.

  5. 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

  6. ✔️ 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 factory SubstituteLoggerFactory

        • 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.

  7. ✔️ 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.

  8. ✔️ 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 with Queue and add a helper method for drainTo to drain a Queue to the Platform.

    • ❌ detect logger name mismatch

      • The method format(String, String, String) is undefined for the type String

        • Idea: remove this feature in script mode

  9. ✔️ 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

  10. ✔️ 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

J2CL Gotchas

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.

J2CL Patterns

Vary by Subclassing

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 */ }
}
What j2cl sees
class AAA_script {
    void aaa() { /* shared-code implementation for script-mode */ }
}

class AAA extends AAA_script {
}
Calling
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()

Vary in Static Code

As static code cannot overwrite methods, we need to introduce variance points using instances. This transformation was used in several places:

Before (j2cl incompatible)
static class AAA {
    void bbb() {
        // do JRE-specific stuff
    }
    void ccc() {
        // some code that calls bbb
        bbb();
    }
}
After (j2cl compatible)
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();
    }
}

Back-wrap elemental2-APIs for Testing

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

elemental2.dom.Console (Given)
@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public class Console {
  public native void debug(Object... var_data);
}
Console_script & Console_wrapped (NEW)
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));
    }
}
What J2CL sees
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.