Skip to content

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

Description

@ikvict07

Summary

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:

  1. RawXJCMojo runs each module's XJC inside its own closeable URLClassLoader
    (createClassLoader(...) set as TCCL, closed when the module's execution finishes).
  2. org.glassfish.jaxb:jaxb-core since 4.0.7 caches the TransformerFactory it discovers
    in a static SoftReference 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.
  3. 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 DomLoaderIdentityTransformerHandler.startDocument()
    on the cached, dead-classloader factory → NoClassDefFoundError: net/sf/saxon/om/Durability.
mvn clean install -Djaxb-plugins.version=4.0.12   # -> BUILD SUCCESS
mvn clean install -Djaxb-plugins.version=4.0.13   # -> BUILD FAILURE (NoClassDefFoundError: net/sf/saxon/om/Durability)
mvn clean install -Djaxb-plugins.version=4.0.16   # -> same failure
mvn clean install -pl module-a -Djaxb-plugins.version=4.0.16   # -> SUCCESS (single module = single classloader)
mvn clean install -pl module-b -Djaxb-plugins.version=4.0.16   # -> SUCCESS

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.

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions