cybersoft
cybersoft

Reputation: 1473

How to load JDK module programmatically?

Suppose we have module A that dynamically loads module B (using classes ModuleFinder, ModuleLayer, etc). Last one requires standard module java.sql that is not loaded into boot layer with module A. How to load required java.sql from JDK (or JRE) using java code?

EDIT

This sample maven project demonstrates my problem:

Project structure:

│   pom.xml
│
├───loader
│   │   pom.xml
│   │
│   └───src
│       ├───main
│       │   ├───java
│       │   │   │   module-info.java
│       │   │   │
│       │   │   └───app
│       │   │       └───module
│       │   │           └───loader
│       │   │                   AppLoader.java
│       │   │                   AppModule.java
│       │   │
│       │   └───resources
│       └───test
│           └───java
└───sql-module
    │   pom.xml
    │
    └───src
        ├───main
        │   ├───java
        │   │   │   module-info.java
        │   │   │
        │   │   └───app
        │   │       └───module
        │   │           └───sql
        │   │                   SQLAppModule.java
        │   │
        │   └───resources
        └───test
            └───java

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>sample-app</artifactId>
        <groupId>sample-app</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>loader</artifactId>
</project>

loader/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>sample-app</artifactId>
        <groupId>sample-app</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>loader</artifactId>
</project>

loader/src/main/java/module-info.java:

module app.module.loader {
    exports app.module.loader;
    uses AppModule;
}

loader/src/main/java/app/module/loader/AppLoader.java:

public class AppLoader {
    public static void main(String[] args) {
        var path = Paths.get("sql-module", "target", "classes");
        var moduleFinder = ModuleFinder.of(path);
        var boot = ModuleLayer.boot();
        var config = boot.configuration().resolveAndBind(moduleFinder, ModuleFinder.of(), Collections.emptyList());
        var newLayer = boot.defineModulesWithOneLoader(config, Thread.currentThread().getContextClassLoader());
        var testModule = ServiceLoader.load(newLayer, AppModule.class)
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Module not found!"));
        System.out.println("Module name: " + testModule.name());
        System.out.println("Module version: " + testModule.version());
    }
}

loader/src/main/java/app/module/loader/AppModule.java:

public interface AppModule {
    String name();
    String version();
}

sql-module/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>sample-app</artifactId>
        <groupId>sample-app</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>sql-module</artifactId>
    <dependencies>
        <dependency>
            <groupId>sample-app</groupId>
            <artifactId>loader</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

sql-module/src/main/java/module-info.java:

module app.module.sql {
    requires app.module.loader;
    requires java.sql;

    provides AppModule with SQLAppModule;
}

sql-module/src/main/java/app/module/sql/SQLAppModule.java:

public class SQLAppModule implements AppModule {
    public SQLAppModule() {
        List<Driver> drivers = DriverManager.drivers().collect(Collectors.toList());
        System.out.println("Drivers on class path: " + drivers.size());
        drivers.forEach(d -> {
            System.out.println("Driver: " + d.toString());
            System.out.println("Version: " + d.getMajorVersion() + "." + d.getMinorVersion());
        });
    }

    @Override
    public String name() {
        return "SQL Module";
    }

    @Override
    public String version() {
        return "1.0-SNAPSHOT";
    }
}

When you try to launch application using main in AppLauncher, you will get an error (I use jdk-10.0.1 now):

Exception in thread "main" java.lang.module.FindException: Module java.sql not found, required by app.module.sql
    at java.base/java.lang.module.Resolver.findFail(Resolver.java:877)
    at java.base/java.lang.module.Resolver.resolve(Resolver.java:191)
    at java.base/java.lang.module.Resolver.bind(Resolver.java:297)
    at java.base/java.lang.module.Configuration.resolveAndBind(Configuration.java:482)
    at java.base/java.lang.module.Configuration.resolveAndBind(Configuration.java:288)
    at app.module.loader/app.module.loader.AppLoader.main(AppLoader.java:14)

What about hacks: I think they must be used in the last place, so we try to find more or less "official" way to do it now.

This answer does not solve this problem, because layer.findModule(moduleName).orElse(null) on boot or any other layer will return null.

Upvotes: 4

Views: 1670

Answers (1)

qutax
qutax

Reputation: 898

You cannot and should not do that at runtime.

It is possible to get the missing modules that are required by your "app.module.sql" module:

var missingModuleNames = moduleFinder.find("app.module.sql")
                                     .map(ModuleReference::descriptor)
                                     .map(ModuleDescriptor::requires)
                                     .orElse(Collections.emptySet())
                                     .stream()
                                     .map(ModuleDescriptor.Requires::name)
                                     .filter(name -> boot.findModule(name).isEmpty())
                                     .collect(Collectors.toSet());

And you can even create a ModuleFinder for the Java Platform Modules:

var platformModules = Files.list(Paths.get(URI.create("jrt:/modules")))
                           .collect(Collectors
                               .toMap(AppLoader::getModuleName, Function.identity()));

var missingModulePaths = missingModules.stream()
                                       .filter(systemModules::containsKey)
                                       .map(systemModules::get)
                                       .toArray(Path[]::new);

var missingModuleFinder = ModuleFinder.of(missingModulePaths);

But even if you do this recursive (java.sql requires java.transaction.xa), your attempt to load any of the platform modules will fail with a LayerInstantiationException as soon as you try to define the modules:

var cfg = boot.configuration()
              .resolveAndBind(missingModuleFinder, ModuleFinder.of(), missingModules);

// This will throw the exception, because 'a layer cannot be created if the 
// configuration contains a module named "java.base", or a module contains a 
// package named "java" or a package with a name starting with "java.".'
// (see Javadoc of ModuleLayer#defineModulesWithOneLoader(Configuration, List<ModuleLayer>, ClassLoader)
ModuleLayer.defineModulesWithOneLoader(cfg, List.of(boot), null);

Upvotes: 1

Related Questions