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