highsource / highsource/jaxb-tools
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
- Dominant language
- Java
- Stars
- 465
- Forks
- 105
- PR merge metrics
- No merged PRs in 30d
Description
## 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 (``, 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
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.` →
`JAXBContextImpl.createTransformerHandler(...)` → `IdentityTransformerHandler.startDocument()`,
reached whenever schema annotation parsing captures DOM content — e.g. an ``
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 `` —
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 `` block containing nested markup (``, ``), which
routes annotation parsing through `DomLoader` → `IdentityTransformerHandler.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. (Builder.java:66)
at net.sf.saxon.dom.DOMWriter. (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. (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 `` 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.
Contributor guide
Assessment
This issue has not been assessed yet.