3

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.

qutax
  • 828
  • 1
  • 11
  • 20
cybersoft
  • 1,453
  • 13
  • 31
  • google shows this [snippet on gist](https://gist.github.com/rkx-bloodshed/badc6d0798b145123897a68ae4e3d6bd) but it looks a hack – cybersoft Jun 09 '18 at 17:26
  • 1
    Sorry. Hacked solutions are called like that for a reason. They are typically hard to understand and very often not very robust. A hack is your last resort, as a professional, you avoid them where possible. – GhostCat Jun 09 '18 at 18:45
  • 1
    The snippet on gist is indeed a hack because it's breaking into JDK internal APIs (invoking non-visible method with `setAccessible(true)`). The entire point of Java Modules is to block code like this from working. It works _for now_ in JDK 9/10 because `--illegal-access=permit` is the default option, but at some point in the future (JDK 11 or later) this approach will stop working. – Andy Guibert Jun 09 '18 at 19:49
  • Seems more like you're [looking to find a module by its name.](https://stackoverflow.com/questions/46287644/how-to-get-a-module-by-its-name-in-java-9) – Naman Jun 10 '18 at 04:42
  • @ IMustBeSomeone I think you're not understanding the correct use of the work hack in programming, a hack is a poor solution, a work around, because a refined solution cannot be found. Hacks are undesirable, as they are generally not reusable and modular. –  Jun 10 '18 at 05:00
  • Edited qestion to show problem with sample project – cybersoft Jun 10 '18 at 11:09
  • Containers need to launched with `--add-modules ALL-SYSTEM` so that all modules in the run-time image are in the boot layer. I any case, the issue with your code fragment is that you've specified the afterFinder parameter to be the empty module finder; if you had specified ModuleFinder.ofSystem() instead then it would have located modules in the run-time image that are in the boot layer. This will work for modules that don't have java.* packages. – Alan Bateman Jun 14 '18 at 04:49
  • @AlanBateman adding all modules from runtime image will break app when it uses artifacts conflicting with app dependencies (e.g., `java.xml.ws.annotation` and `javax.annotation-api`), but adding required modules using `--add-modules ` will do work, and I dont know why to use `ModuleFinder.ofSystem()` (It seems to me that second module finder is useless at all, it is enaugh to include modules to first finder) – cybersoft Jun 25 '18 at 17:04

1 Answers1

1

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);
qutax
  • 828
  • 1
  • 11
  • 20