Gradle build overrides Log4J and Slf4J classes and fails to compile code that uses these libraries
rizsi opened this issue · comments
If a project uses the log4j or the SLF4J API then when it is compiled then the version used by the build tool instead of the version provided by the gradle configuration is used.
The version used by the build tool is not compatible with current teaVM (9.2.0) and compile to JS fails.
This makes it impossible to use these logging APIs and compile the code with gradle.
A minimal project with 2 Java files and gradle configuration that reproduces the issue is attached. In this project the org.apache.log4j.Logger class gets a trivial re-implementation but the compiler uses the original version of this class even though the original version is not referenced in the gradle file at all. report.tar.gz
I suspect that the problem is caused by that the InProcessBuildStrategy.buildClassloader() classloader that is used to load classes of the code to compile preloads the SLF4J and log4j implementations because they are configured when loading the InProcessBuildStrategy class and this method uses its own classloader to bootstrap the classloader hierarchy.
I have tested executing the same build using the direct configuration of org.teavm.tooling.TeaVMTool
and that works the code is compiled and the result works as expected.
The error message (part of the log is omitted to only show the essence):
Class java.util.concurrent.LinkedBlockingQueue was not found
at org.slf4j.LoggerFactory.replayEvents(LoggerFactory.java:206)
at org.slf4j.LoggerFactory.postBindCleanUp(LoggerFactory.java:183)
at org.slf4j.LoggerFactory.bind(LoggerFactory.java:177)
at org.slf4j.LoggerFactory.performInitialization(LoggerFactory.java:124)
at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:417)
at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:362)
at org.apache.log4j.Category.<init>(Category.java:57)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.Logger.getLogger(Logger.java:41)
at org.apache.log4j.Logger.getLogger(Logger.java:49)
at example.MainClass.main(MainClass.java:7)
Method java.lang.ClassLoader.getResources(Ljava/lang/String;)Ljava/util/Enumeration; was not found
Class java.util.concurrent.CopyOnWriteArrayList was not found
Class java.lang.reflect.Proxy was not found
(To make SLF4J work with TeaVM the references to the missing JVM features also have to be removed from SLF4J code but that is not too much work and is an acceptable workaround for our goals.)
As a workaround you can try to use out-of-process build
Thanks! TeaVM is a great project everything I tried works perfectly in the browser. I only miss reflection because then I could do RPC simpler but it can also be worked around. It makes my old dream possible to program the browser in Java running on the client side.
I have worked it around executing it directly calling TeaVMTool from own code. We have an exotic build system (OSGI bundle manifest based) and it was even simpler to integrate it this way.
I solved same VM execution with a trick that loads the TeaVM compiler directly over the boot classloader so nothing else is accessible in its classloader. And then it is executed reflectively so the codes using loggers need not be present in the same classloader as the compiler itself.
For me it is solved with the workaround but I think the bug can discourage other developers to use TeaVM when their simplest examples don't compile due to using Log4j or SLF4J APIs.
As for RPC: don't use reflection for it. If you have a chance to find reflection-less (for example, based on annotation processors) library or to write it yourself, please, do! Reflection is usually pain-in-the-ass for AOT compilers, Graal Native Image or proguard-based builds suffer from this issue as well
Yes, you are right, thanks! I have done a simple code generator that serializes the public fields and public accessors and knows the basic types, arrays and few from the collection framework. It will be enough for my goals and the footprint will be minimal. Remoting also automatically transforms a Java interface to one that returns promise wrapped objects on the client side. I have tried the primitives that it uses and they work . But it is not ready yet.
For the record configuring compilation out of process solves issue. The configuration in build.gradle that works:
teavm.js {
addedToWebApp = true
mainClass = "example.MainClass"
outOfProcess = true
// this is also optional, default value is <project name>.js
targetFileName = "example.js"
}