Skip to content

Fix BEASTClassLoader dual-loader class identity split#66

Merged
walterxie merged 1 commit intomasterfrom
fix-classloader-dual-load
Apr 13, 2026
Merged

Fix BEASTClassLoader dual-loader class identity split#66
walterxie merged 1 commit intomasterfrom
fix-classloader-dual-load

Conversation

@alexeid
Copy link
Copy Markdown
Member

@alexeid alexeid commented Apr 13, 2026

Fixes #65.

Summary

  • Add resolveLoaderFor(provider) to BEASTClassLoader: prefer the class-loader of the boot-layer module that owns the provider's package; fall back to the thread-context loader only when no named module claims the package.
  • Use it from instance addServices(String, Map) and static addService(String, String, String) instead of unconditional fallbackClassLoader().

This brings the version.xml registration path in line with the JPMS provides registration path (mergeAllProviders / collectProviders), which already used m.getClassLoader() from the providing module.

Why

When beast-base is reachable via both the JPMS module path and the classpath in the same JVM (typical of surefire and IDE runs), the old code stored the app class-loader against every version.xml provider. BEASTClassLoader.forName() then returned the app-loader copy of (e.g.) beast.base.evolution.tree.Node, while application code resolved through the module-loader copy. The two Class<?> instances were incompatible, producing ClassCastException and an empty data-type registry. See #65 for the full diagnosis and reproduction.

Test plan

  • mvn -pl beast-pkgmgmt test — 40/40 pass with the patch.
  • End-to-end validation against LPhyBeast on a machine that previously hit the failures:
    cd ~/Git/beast3
    git fetch origin
    git checkout fix-classloader-dual-load
    mvn install -DskipTests
    Then in LPhyBeast/pom.xml change <beast.version>2.8.0-beta4</beast.version> to <beast.version>2.8.0-SNAPSHOT</beast.version> and run mvn -pl lphybeast test. The previously-failing H5N1TutorialTest.testDPG and the two LPhyScriptsToBEASTTest cases should pass.

When beast-base is reachable via both the JPMS module path and the
classpath in the same JVM (a common surefire/IDE configuration),
`BEASTClassLoader.addServices(...)` and `addService(...)` registered
every provider class against `fallbackClassLoader()` (the thread
context = app class-loader), even when the class actually lived in a
named boot-layer module owned by a different loader.

The result: `BEASTClassLoader.forName(...)` returned the app-loader
copy of (e.g.) `beast.base.evolution.tree.Node`, while caller code
compiled against `import beast.base...` resolved to the module-loader
copy. The two `Class<?>` instances were incompatible, producing
ClassCastException and an apparently-empty data-type registry.

Add `resolveLoaderFor(provider)`: walk the boot layer's modules and
prefer the loader of the module that owns the provider's package.
Fall back to `fallbackClassLoader()` only when no named module owns
the package (genuine classpath-only providers).

This makes the version.xml registration path consistent with the
JPMS `provides` registration path (mergeAllProviders /
collectProviders), which already used the module's class-loader.
@alexeid alexeid requested a review from walterxie April 13, 2026 03:34
@walterxie walterxie merged commit ddf4e14 into master Apr 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BEASTClassLoader maps boot-layer module classes to wrong class-loader, causing ClassCastException

2 participants