Skip to content

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

@alexeid

Description

@alexeid

Symptom

When beast-base is reachable via both the JPMS module path and the
classpath in the same JVM (a common surefire/IDE configuration), code
that resolves BEAST classes through BEASTClassLoader.forName(...)
gets a different Class<?> instance than code that resolves them
directly via import beast.base.... Two visible failures:

  1. ClassCastException on beast.base.evolution.tree.Node:
    class beast.base.evolution.tree.Node cannot be cast to class beast.base.evolution.tree.Node
    (beast.base.evolution.tree.Node is in module BEAST.base of loader jdk.internal.loader.Loader @...;
     beast.base.evolution.tree.Node is in unnamed module of loader 'app')
    
  2. Empty data-type registry: data type 'nucleotide' cannot be found. Choose one of [],
    even though the registry shows nucleotide was registered — the
    registration happened on a different Class<?> instance of the
    registry holder.

Originally surfaced when running mvn -pl lphybeast test against
LPhyBeast on the beast3 branch.

Root cause

BEASTClassLoader.addServices(String, Map) (instance method) and
BEASTClassLoader.addService(String, String, String) (static) both
register every provider class name in class2loaderMap against
fallbackClassLoader() — i.e. the thread context class-loader, which
in a surefire/JPMS run is the app class-loader.

Meanwhile, the JPMS-driven path (mergeAllProviders /
collectProviders) correctly uses the providing module's
getClassLoader(). So provider classes that exist in a named boot-layer
module get a different loader depending on which discovery path
registered them, and BEASTClassLoader.forName() returns the
app-loader copy regardless of where the caller is.

Proposed fix

Before storing fallbackClassLoader(), check whether a named boot-layer
module owns the provider's package. If so, prefer that module's
class-loader; otherwise fall back as today.

private static ClassLoader resolveLoaderFor(String provider) {
    int dot = provider.lastIndexOf('.');
    if (dot < 0) return fallbackClassLoader();
    String pkg = provider.substring(0, dot);
    for (Module m : ModuleLayer.boot().modules()) {
        var desc = m.getDescriptor();
        if (desc != null && desc.packages().contains(pkg)) {
            ClassLoader loader = m.getClassLoader();
            if (loader != null) return loader;
        }
    }
    return fallbackClassLoader();
}

This makes the version.xml registration path consistent with the
provides/module-descriptor registration path, removing the
class-identity split.

Validation

  • mvn -pl beast-pkgmgmt test — 40/40 tests pass with the patch.
  • LPhyBeast mvn -pl lphybeast test — 3/3 tests pass against a
    patched 2.8.0-beta4 install.

PR forthcoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions