You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Since 4.0.13, a statically cached TransformerFactory outlives the per-module XJC plugin classloader, causing nondeterministic NoClassDefFoundError: net/sf/saxon/... in multi-module reactors #714
When a JAXP TransformerFactory provider (e.g. Saxon-HE, or any artifact that registers one
via META-INF/services/javax.xml.transform.TransformerFactory) ends up on the XJC plugin
classpath (<configuration><plugins>, directly or transitively), a multi-module reactor build
can fail nondeterministically starting with jaxb-tools 4.0.13:
Failed to execute goal org.jvnet.jaxb:jaxb-maven-plugin:4.0.16:generate (default) on project module-b:
... A required class was missing while executing org.jvnet.jaxb:jaxb-maven-plugin:4.0.16:generate:
net/sf/saxon/om/Durability
The missing class name varies from run to run (net/sf/saxon/om/Durability, net/sf/saxon/om/NoNamespaceName, net/sf/saxon/serialize/ContentHandlerProxy, ...), the
failure only occurs on clean builds (incremental builds skip XJC), and only when two or
more modules run the generate goal in the same JVM — each module in isolation builds fine.
That combination makes it very hard to diagnose in a real project.
Interplay of three parties:
RawXJCMojo runs each module's XJC inside its own closeableURLClassLoader
(createClassLoader(...) set as TCCL, closed when the module's execution finishes).
org.glassfish.jaxb:jaxb-core since 4.0.7 caches the TransformerFactory it discovers
in a staticSoftReference cache (XmlFactory.transformerFactoryCache, introduced by [#1824] createTransformerFactory useCache new param eclipse-ee4j/jaxb-ri#1872 as a performance fix — hardcoded useCache=true from JAXBContextImpl.createTransformer(Handler), and the cache is keyed only by the disableSecureProcessing boolean, not by classloader). jaxb-tools 4.0.13 bumped the
bundled jaxb.version from 4.0.6 to 4.0.8, which is how this cache first reaches the XJC
classpath — that's why 4.0.12 is fine and 4.0.13 is the first affected release.
JAXP service discovery inside module A's XJC run finds the Saxon-backed factory through
module A's (closeable) plugin classloader and jaxb-core caches it JVM-wide. When module
B's XJC run later triggers a Transformer use (details below), it gets served the cached
factory whose defining classloader has been closed — any Saxon class that wasn't already
loaded during module A's lifetime now fails to load, producing a NoClassDefFoundError whose
class name depends on which lazy-initialization path runs first.
The concrete in-XJC consumer is DomLoader$State.<init> → JAXBContextImpl.createTransformerHandler(...) → IdentityTransformerHandler.startDocument(),
reached whenever schema annotation parsing captures DOM content — e.g. an <xs:documentation>
element containing nested markup, or plugin customizations kept as DOM. Full stack below.
Reproduction
Attached: saxon-repro.zip — parent + two schema modules, fully synthetic.
Both modules list net.sf.saxon:Saxon-HE under the shared <configuration><plugins> —
a stand-in for any real-world XJC-plugin dependency that transitively drags a JAXP TransformerFactory provider onto the XJC classpath (in our case an in-house dvpcommons-xml-jaxb registering a custom Saxon factory; Saxon-HE self-registers, so it
alone is enough).
module-a has an external bindings.xjb; internalizing it makes XJC create (and jaxb-core
cache) the TransformerFactory during module A's run without ever running a transform —
so most Saxon classes stay unloaded.
module-b has an <xs:documentation> block containing nested markup (<b>, <code>), which
routes annotation parsing through DomLoader → IdentityTransformerHandler.startDocument()
on the cached, dead-classloader factory → NoClassDefFoundError: net/sf/saxon/om/Durability.
Verified on JDK 21, both mvn and mvnd. We hit this originally in a large multi-module
project (21+ XSDs, episodes, annox) where the failing module differed from the module that
poisoned the cache, and the missing-class name changed between runs.
Crash stack (identical, including line numbers, to what we see in the real project):
Caused by: java.lang.NoClassDefFoundError: net/sf/saxon/om/Durability
at net.sf.saxon.event.Builder.<init> (Builder.java:66)
at net.sf.saxon.dom.DOMWriter.<init> (DOMWriter.java:43)
at net.sf.saxon.dom.DOMObjectModel.getDocumentBuilder (DOMObjectModel.java:236)
at net.sf.saxon.lib.SerializerFactory.getReceiverForNonSerializedResult (SerializerFactory.java:464)
at net.sf.saxon.lib.SerializerFactory.getReceiver (SerializerFactory.java:383)
at net.sf.saxon.lib.SerializerFactory.getReceiver (SerializerFactory.java:147)
at net.sf.saxon.jaxp.IdentityTransformerHandler.startDocument (IdentityTransformerHandler.java:114)
at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.DomLoader$State.<init> (DomLoader.java:54)
at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.DomLoader.startElement (DomLoader.java:89)
at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.ProxyLoader.startElement (ProxyLoader.java:30)
at org.glassfish.jaxb.runtime.v2.runtime.unmarshaller.UnmarshallingContext._startElement (UnmarshallingContext.java:530)
...
at com.sun.tools.xjc.reader.xmlschema.bindinfo.ForkingFilter.startElement (ForkingFilter.java:121)
at com.sun.tools.xjc.reader.xmlschema.bindinfo.AnnotationParserFactoryImpl$1$1.startElement (AnnotationParserFactoryImpl.java:85)
at com.sun.xml.xsom.impl.parser.state.NGCCRuntime.startElement (NGCCRuntime.java:228)
Workaround
Exclude the TransformerFactory provider (and Saxon) from the <plugins> entry that drags it
in, so JAXP falls back to the JDK's built-in factory — that one is loaded by the platform
classloader, which never closes, so caching it across modules is harmless.
Possible fixes
On the jaxb-tools side (either would do):
Reset/invalidate jaxb-core's static factory cache when the per-module classloader is closed,
or run XJC with the cache disabled (jaxb-ri#1872 added a useCache parameter, but nothing
currently exposes it and the internal callers hardcode true).
Alternatively, filter JAXP TransformerFactory service registrations out of the XJC plugin
classloader — XJC has no legitimate reason to pick up a third-party transformer that happens
to ride along with schema/plugin artifacts.
Arguably jaxb-ri's cache itself is unsound (a statically cached, classloader-discovered service
keyed without regard to its classloader), so I'm happy to file a companion issue at
eclipse-ee4j/jaxb-ri if you think the fix belongs (partly) there — jaxb-tools is where the
closeable classloaders live, so filing here first.
Related: #713 — a different regression that also first appears in 4.0.13 (resolver rewrite);
both were uncovered by the same version bump in our project, but the mechanisms are independent.
Summary
When a JAXP
TransformerFactoryprovider (e.g. Saxon-HE, or any artifact that registers onevia
META-INF/services/javax.xml.transform.TransformerFactory) ends up on the XJC pluginclasspath (
<configuration><plugins>, directly or transitively), a multi-module reactor buildcan fail nondeterministically starting with jaxb-tools 4.0.13:
The missing class name varies from run to run (
net/sf/saxon/om/Durability,net/sf/saxon/om/NoNamespaceName,net/sf/saxon/serialize/ContentHandlerProxy, ...), thefailure only occurs on clean builds (incremental builds skip XJC), and only when two or
more modules run the
generategoal in the same JVM — each module in isolation builds fine.That combination makes it very hard to diagnose in a real project.
Interplay of three parties:
RawXJCMojoruns each module's XJC inside its own closeableURLClassLoader(
createClassLoader(...)set as TCCL, closed when the module's execution finishes).org.glassfish.jaxb:jaxb-coresince 4.0.7 caches theTransformerFactoryit discoversin a static
SoftReferencecache (XmlFactory.transformerFactoryCache, introduced by[#1824] createTransformerFactory useCache new param eclipse-ee4j/jaxb-ri#1872 as a performance fix — hardcoded
useCache=truefromJAXBContextImpl.createTransformer(Handler), and the cache is keyed only by thedisableSecureProcessingboolean, not by classloader). jaxb-tools 4.0.13 bumped thebundled
jaxb.versionfrom 4.0.6 to 4.0.8, which is how this cache first reaches the XJCclasspath — that's why 4.0.12 is fine and 4.0.13 is the first affected release.
module A's (closeable) plugin classloader and jaxb-core caches it JVM-wide. When module
B's XJC run later triggers a
Transformeruse (details below), it gets served the cachedfactory whose defining classloader has been closed — any Saxon class that wasn't already
loaded during module A's lifetime now fails to load, producing a
NoClassDefFoundErrorwhoseclass name depends on which lazy-initialization path runs first.
The concrete in-XJC consumer is
DomLoader$State.<init>→JAXBContextImpl.createTransformerHandler(...)→IdentityTransformerHandler.startDocument(),reached whenever schema annotation parsing captures DOM content — e.g. an
<xs:documentation>element containing nested markup, or plugin customizations kept as DOM. Full stack below.
Reproduction
Attached:
saxon-repro.zip— parent + two schema modules, fully synthetic.net.sf.saxon:Saxon-HEunder the shared<configuration><plugins>—a stand-in for any real-world XJC-plugin dependency that transitively drags a JAXP
TransformerFactoryprovider onto the XJC classpath (in our case an in-housedvpcommons-xml-jaxbregistering a custom Saxon factory; Saxon-HE self-registers, so italone is enough).
module-ahas an externalbindings.xjb; internalizing it makes XJC create (and jaxb-corecache) the TransformerFactory during module A's run without ever running a transform —
so most Saxon classes stay unloaded.
module-bhas an<xs:documentation>block containing nested markup (<b>,<code>), whichroutes annotation parsing through
DomLoader→IdentityTransformerHandler.startDocument()on the cached, dead-classloader factory →
NoClassDefFoundError: net/sf/saxon/om/Durability.Verified on JDK 21, both
mvnandmvnd. We hit this originally in a large multi-moduleproject (21+ XSDs, episodes, annox) where the failing module differed from the module that
poisoned the cache, and the missing-class name changed between runs.
Crash stack (identical, including line numbers, to what we see in the real project):
Workaround
Exclude the TransformerFactory provider (and Saxon) from the
<plugins>entry that drags itin, so JAXP falls back to the JDK's built-in factory — that one is loaded by the platform
classloader, which never closes, so caching it across modules is harmless.
Possible fixes
On the jaxb-tools side (either would do):
or run XJC with the cache disabled (jaxb-ri#1872 added a
useCacheparameter, but nothingcurrently exposes it and the internal callers hardcode
true).TransformerFactoryservice registrations out of the XJC pluginclassloader — XJC has no legitimate reason to pick up a third-party transformer that happens
to ride along with schema/plugin artifacts.
Arguably jaxb-ri's cache itself is unsound (a statically cached, classloader-discovered service
keyed without regard to its classloader), so I'm happy to file a companion issue at
eclipse-ee4j/jaxb-ri if you think the fix belongs (partly) there — jaxb-tools is where the
closeable classloaders live, so filing here first.
Related: #713 — a different regression that also first appears in 4.0.13 (resolver rewrite);
both were uncovered by the same version bump in our project, but the mechanisms are independent.