New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LogFactory should use same classloader for feature detection and loading the correct logcreator #1922
Comments
Running './gradlew clean assemble test' will cause a deployment failure in embedded Glassfish caused by: Caused by: java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory at org.flywaydb.core.internal.util.logging.slf4j.Slf4jLogCreator.createLogger(Slf4jLogCreator.java:27) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:82) at org.flywaydb.core.internal.util.ClassUtils.<clinit>(ClassUtils.java:37) at org.flywaydb.core.internal.util.FeatureDetector.isSlf4jAvailable(FeatureDetector.java:96) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:71) at org.flywaydb.core.internal.util.FeatureDetector.<clinit>(FeatureDetector.java:25) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:68) at org.flywaydb.core.Flyway.<clinit>(Flyway.java:72) at test.flywaydb.ContextListener.contextInitialized(ContextListener.java:26)
Running './gradlew clean assemble test' will cause a deployment failure in embedded Glassfish caused by: Caused by: java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory at org.flywaydb.core.internal.util.logging.slf4j.Slf4jLogCreator.createLogger(Slf4jLogCreator.java:27) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:82) at org.flywaydb.core.internal.util.ClassUtils.<clinit>(ClassUtils.java:37) at org.flywaydb.core.internal.util.FeatureDetector.isSlf4jAvailable(FeatureDetector.java:96) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:71) at org.flywaydb.core.internal.util.FeatureDetector.<clinit>(FeatureDetector.java:25) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:68) at org.flywaydb.core.Flyway.<clinit>(Flyway.java:72) at test.flywaydb.ContextListener.contextInitialized(ContextListener.java:26)
Running './gradlew clean assemble test' will cause a deployment failure in embedded Glassfish caused by: Caused by: java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory at org.flywaydb.core.internal.util.logging.slf4j.Slf4jLogCreator.createLogger(Slf4jLogCreator.java:27) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:82) at org.flywaydb.core.internal.util.ClassUtils.<clinit>(ClassUtils.java:37) at org.flywaydb.core.internal.util.FeatureDetector.isSlf4jAvailable(FeatureDetector.java:96) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:71) at org.flywaydb.core.internal.util.FeatureDetector.<clinit>(FeatureDetector.java:25) at org.flywaydb.core.api.logging.LogFactory.getLog(LogFactory.java:68) at org.flywaydb.core.Flyway.<clinit>(Flyway.java:72) at test.flywaydb.ContextListener.contextInitialized(ContextListener.java:26)
Thanks for pointing out the inconsistency. I guess we have two choices:
What do you think? Which one would you favor and why? |
Thanks for the follow-up and questions. Before deciding how the TCCL should be used - it would be useful (to me anyway) to understand why it was introduced in the first-place, especially for the logging-case. Is there any brief write-up (other than in the following linked issues)? History: I figured logging in Flyway code (and location/configuration of logging-apis) would be mostly an internal-affair, but I do notice that at one point logging was moved from the internal to public API - maybe to support extension and plugins (I'm not familiar-enough with Flyway to know)? Note that So in-summary, I'm interested in knowing what the TCCL provides here. Maybe it is access to resource configuration-files for the logging API that only exist in the child/context classloader. Just seems like for something as fundamental as logging, Flyway should be self-sufficient within its own classloader. |
Logging is part of the public API to allow Java migrations, custom resolvers and executors, Java callbacks, etc to use the same logging abstraction and work flawlessly across environments (API, Maven, CLI, etc). Flyway's own ClassLoader may not be sufficient as the logging framework it loads most likely needs access to its config file which resides somewhere within your app. I am currently leaning towards option 2. This lets users like you define initialize their classloader with what they need (this would have to be very early on ear startup) and keeps the existing behavior for everyone else. |
Let me think on it a bit and maybe run a test or two. Of course these sorts of problems only occur if Flyway is in a shared/common classloader, parent of the TCCLs. Central to my concern is that If option 2, all applications would need to take great care in setting the classloader before logging is initialized ... noting that classloading As I say I'll think on it some more, unless you are in a hurry to proceed. |
Not in a hurry. I'm hoping to address this in the coming month to get it out as part of Flyway 5.1. If we miss that window, it'll be 5.2. No biggie. |
Note that you can work around this in 5.0.7 by manually calling |
I still wasn't 100% clear on what the required logging behaviour is, so I updated the issue with a section Desired behaviour (Flyway logging) - DRAFT. Please review. I'm wondering whether the requirements for the shared-classloader behaviour are too ambitions ... do we shoot for completely independent logging-configurations, or settle for first-initializer-wins? |
@javabrett To be honest with you, using different logging frameworks for different wars in an ear is just asking for trouble and creating an unnecessary mess. Also this situation is very rare. I am therefore now leaning towards my suggestion 1. |
The different logging-frameworks was an obtuse/general example I agree. But won't there be also be simpler examples of logging configuration cross-wires ... if Application A is using SLF4J -> log4j and logging to That might be acceptable, but will probably best be documented. Or is that not going to happen with the current code if Flyway is deployed to a shared/parent classloader? I'm interested in how either option 1 or 2 are going to create the right classloading partitioning. Maybe dynamic proxies will help. |
Well let's not start solving theoretical problems here and stick to the issue at hand. Would my suggestion 1 solve your specific issue? |
Loading But given the set-up above, where Flyway is not in the direct scope of the TCCL (it is deployed into a parent classloader of the TCCL), this won't just be a matter of asking the TCCL to load that class, since it will delegate to the parent which will load it and then the NCDFE occurs. You are likely already aware of that. So I'm not sure what is planned - I would expect a non-trivial partitioning classloader would need to be injected to bridge between the classloaders. Or as I say maybe there is something that can be done with dynamic proxies. If we let the current classloader (rather than the TCCL) do the test for logging APIs, it will only find APIs that are visible to Flyway. I would hope that in the (perhaps typical) case where Flyway and its caller are co-deployed to the same-classloader this would behave exactly as it does now. What would change is that what is currently not-working would never work, that is, logging would never be controlled by a child classloader application, and if the deployer wanted to control Flyway logging when it is loaded in a parent, they would be responsible for supplying the logging API and configuration. |
The individual logcreators are now also instantiated using the TCCL. Please build the latest sources and confirm this is now working for you. |
Thanks for the update and the code-change. I tested this with a local Here's the new stacktrace:
The reason this doesn't work is because you can't just make If I add a little debug output:
... I see this:
Showing that the TCCL ( |
Ok, I have decided to mark this as won't fix. You can fix your scenario by either moving the logging libraries to the ear or Flyway to the war. Both are much more sane and consistent options than what you have now. Alternatively you can also supply sour own logcreator via the |
… detection and loading the correct logcreator
What version of Flyway are you using?
5.0.7.
Which client are you using? (Command-line, Java API, Maven plugin, Gradle plugin, SBT plugin, ANT tasks)
Java API.
What database are you using (type & version)?
MySQL, Oracle DB, multiple versions.
What operating system are you using?
Linux, OS/X
TL;DR
org.flywaydb.core.api.logging.LogFactory
uses the Thread Context Class Loader (TCCL) when discovering which logging APIs/backends are available for its own logging. It should not do this as the TCCL is not subsequently used to load the logging-API, which is loaded as-normal by the classloader ofLogFactory
itself, which is not necessarily the same classloader with the same class-visibility. This can lead to aNoClassDefFoundError: org/slf4j/LoggerFactory
, especially in certain JavaEE packaging scenarios.Problem demonstration
This problem was noticed in a real application-scenario, but can be simplified and reproduced as-follows:
When
Flyway
is first classloaded, it initializes its own logging, and uses the TCCL classloader for detection of the available logging APIs (SLF4J, Apache Commons Logging), but when loading the API-wrapper e.g.Slf4jLogCreator
, normal classloading is used, which is not the TCCL. In the above scenario this will yield:Code
LogFactory
creates aFeatureDetector
passing the TCCL as the classloader to be used in feature-tests. Later at https://github.com/flyway/flyway/blob/master/flyway-core/src/main/java/org/flywaydb/core/api/logging/LogFactory.java#L72 aSlf4jLogCreator
is instantiated directly usingLogFactory
's classloader, which is not necessarily the TCCL, leading to the possibility of a NCDFE.Analysis
In a JavaEE application the TCCL will often be set to a leaf classloader e.g a
WebappClassLoader
with visibility of parent classloaders, and perhaps other libraries like SLF4J. It is reasonable that Flyway might be loaded by some parent/shared classloader. It should not use the TCCL classloaders when testing what classes it can load for itself e.g. for the purposes of obtaining a logging-API, since the TCCL may be a child classloader with classes not-visible to Flyway. This is the case in a JavaEE deployment where FLyway is loaded by a classloader which is a parent to the TCCL e.g. anEarLibClassLoader
.Desired behaviour (Flyway logging) - DRAFT
Overall logging goal
When an application loads Flyway, Flyway's own internal-logging and public logging API attempt to discern the logging-framework used by the application, and load and use that framework along with any associated configuration. Applications should expect log-messages from Flyway to be logged similarly to the application's own logging, and any extensions which use
org.flywaydb.core.api.logging.LogFactory
likewise expect similar logging.Simple case - single application/classloader, or no parent/shared classloader
f1.properties
. Flyway is also deployed-to and also loaded from C1, and detects and uses F1 for logging.f2.properties
. Flyway is also deployed-to and also loaded from C2, and detects and uses F2 for logging.Multiple applications, Flyway is deployed to a parent/shared classloader of the applications
Same goal as for the simple case. Now Flyway is installed in a classloader which is shared between the applications, and doesn't have visibility of each application's classes (or logging APIs, unless they are co-deployed with Flyway) through it's own classloader. Difficulties:
The text was updated successfully, but these errors were encountered: