Decoupling Turbine from log4j-core - Turbine.configureLogging forces log4j2-core onto classpath
David Maffitt <[email protected]> Fri, 3 Jul 2026 11:32:59 -0500
| Newsgroups | gmane.comp.jakarta.turbine.devel |
|---|---|
| Message-ID | <[email protected]> |
--Apple-Mail=_9FD1205E-CBB1-4E93-804C-F8734E13DCBD Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 Hi devs, We=E2=80=99re migrating a large open-source application (XNAT = <https://www.xnat.org/about/>) onto Turbine 5.1, with 7.0 as the = eventual target. Turbine has been great to bring forward, but we=E2=80=99v= e hit one hard coupling that=E2=80=99s awkward for applications that = don=E2=80=99t use log4j2 as their backend, and I=E2=80=99d like to = propose decoupling it. The last discussion of log4j on this list seems to be [PR] Use JDK9+ = System logger [turbine-core] = <https://lists.apache.org/[email protected]:2026-1:log4j> I don't necessarily trust Claude's proposed solution, but the log4j-core = on the classpath is an issue. I=E2=80=99m happy to contribute the PR if = you=E2=80=99re open to the approach. Root cause org.apache.turbine.Turbine.configureLogging(java.nio.file.Path) =E2=80=94 = invoked from Turbine.configure(ServletConfig, ServletContext) during = init =E2=80=94 begins by casting the active logging context to the = log4j2-core implementation class: LoggerContext ctx =3D (org.apache.logging.log4j.core.LoggerContext) = LogManager.getContext(false); ctx.getConfiguration().getConfigurationSource().getLocation(); // then = optionally resolveLog4j2(...) + reconfigure The cast is the first instruction in the method, so it executes on every = init regardless of whether a log4j2.xml is present. Turbine=E2=80=99s = log field is org.apache.logging.log4j.Logger (the API), which is fine = =E2=80=94 but this method reaches past the API into log4j-core. Why it=E2=80=99s a problem for consumers log4j-core becomes mandatory. An application that logs through = SLF4J/Logback and excludes log4j-core gets NoClassDefFoundError: = org/apache/logging/log4j/core/LoggerContext at Turbine.init(). It also blocks routing Turbine=E2=80=99s log4j2 through SLF4J. The = natural workaround =E2=80=94 add log4j-to-slf4j so Turbine=E2=80=99s = log4j2 API calls flow into SLF4J =E2=80=94 fails, because = LogManager.getContext(false) then returns an = org.apache.logging.slf4j.SLF4JLoggerContext, and the unconditional cast = to core.LoggerContext throws ClassCastException. Net effect: consumers are forced both to include log4j-core and to make = it the live backend, even if the rest of the app is on Logback. For = context, this is the same in 7.0 (log4j2 2.24.3), so it isn=E2=80=99t = 5.1-specific. A secondary snag we hit: mixed log4j-api/log4j-core versions on the = graph (2.23.1 vs 2.14.1) cause NoSuchMethodError: = LoaderUtil.getClassLoaders(). Not Turbine=E2=80=99s fault, but aligning = the versions Turbine declares would spare consumers that. Proposed change: make log4j2 auto-configuration optional, and core a = truly optional dependency The goal is that log4j-core becomes an optional (provided/runtime) = dependency: present it and Turbine auto-loads log4j2.xml as today; omit = it and Turbine simply leaves logging to whatever the app configured. = Three parts: Gate the behavior behind a config/init parameter (e.g. = turbine.logging.log4j2.autoconfigure, default true for backward = compatibility). Apps that manage their own logging set it false. Isolate every direct log4j-core reference into a separate helper class, = and invoke it only after a capability check =E2=80=94 so = Turbine.configure() itself contains no bytecode reference to a core = class on the always-executed path: if (isLog4j2AutoConfigureEnabled()) { try { Class.forName("org.apache.logging.log4j.core.LoggerContext"); = // core present? Log4j2ConfigHelper.configure(applicationRoot); = // helper holds all core refs } catch (ClassNotFoundException | NoClassDefFoundError e) { log.debug("log4j2-core not on classpath; skipping Turbine log4j2 = auto-configuration"); } } Because Log4j2ConfigHelper (the only class referencing = core.LoggerContext) is loaded lazily inside the guarded branch, the JVM = never needs to resolve core when it=E2=80=99s absent. Note an instanceof = guard alone is not sufficient =E2=80=94 instanceof core.LoggerContext = still puts a core reference in the constant pool of an always-run = method, which triggers the same NoClassDefFoundError. Keep log4j-api as the compile dependency (the Logger facade) and move = log4j-core to provided/optional in Turbine=E2=80=99s POM. That lets = consumers route log4j-api =E2=86=92 SLF4J via log4j-to-slf4j and run = entirely on their own backend, while log4j2-backed apps add core and = keep today=E2=80=99s behavior unchanged. The helper approach also naturally sidesteps the ClassCastException, = since the core-typed getContext(false) call lives only in the path taken = when core is actually present. This keeps 100% backward compatibility (core on the classpath =E2=86=92 = identical behavior) while letting non-log4j2 apps run Turbine without = it. I=E2=80=99ve reproduced the failure with a minimal standalone = Turbine 5.1 boot and can share it, and I=E2=80=99m glad to open a JIRA + = PR implementing the above if the direction sounds right to you. Thanks for maintaining Turbine =E2=80=94 Dave Maffitt, and Claude= --Apple-Mail=_9FD1205E-CBB1-4E93-804C-F8734E13DCBD--