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

Open
#714 5 comments 0 reactions 1 assignee Claimed by @laurentschoelens View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.