9

I'm currently writing an application that requires to operate on different type of devices. My approach would be to make a "modular" application that can dynamically load different classes according to the device they need to operate on.

To make the application easily extensible, my goal is to assign a specific path to the additional modules (either .jar or .class files) leaving the core program as it is. This would be crucial when having different customers requiring different modules (without having to compile a different application for each of them).

These modules would implement a common interface, while the "core" application can use these methods defined on the interface and let the single implementations do the work. What's the best way to load them on demand? I was considering the use of URLClassLoader but i don't know if this approach is up-to-date according to new patterns and Java trends, as I would like to avoid a poorly designed application and deprecated techniques. What's an alternative best approach to make a modular and easily extensible application with JDK 9 (that can be extended just by adding module files to a folder) ?

Naman
  • 27,789
  • 26
  • 218
  • 353
Mastarius
  • 305
  • 3
  • 13
  • 4
    There is no **best way**. All approaches have their pros and cons. `URLClassLoader` is the oldest approach and the most well-known. There is also a new method `Lookup.defineClass` in Java 9 but it can only load a class in the same package. You should also probably look at OSGi but it can be pretty cumbersome. – ZhekaKozlov Feb 12 '18 at 09:07
  • You could use a dependency injection framework to manage the wiring for you. Or you could use your build system to create and manage device specific solutions (for example Maven assemblies). – leftbit Feb 12 '18 at 09:26
  • 1
    https://stackoverflow.com/questions/16102010/dynamically-loading-plugin-jars-using-serviceloader – vikingsteve Feb 12 '18 at 09:27
  • 3
    Make sure to explore services and ServiceLoader. It may be that you can create service providers for each device type, each implements a service interface the core application uses. – Alan Bateman Feb 12 '18 at 09:48
  • Better answer at https://stackoverflow.com/a/62749934/6225803 – krishna T Oct 28 '21 at 12:36

3 Answers3

7

Additionnaly to the ServicerLoader usage given by @SeverityOne, you can use the module-info.java to declare the different instanciation of the interface, using "uses"/"provides" keywords.

Then you use a module path instead of a classpath, it loads all the directory containing your modules, don't need to create a specific classLoader

The serviceLoader usage:

public static void main(String[] args) {
    ServiceLoader<IGreeting> sl = ServiceLoader.load(IGreeting.class);
    IGreeting greeting = sl.findFirst().orElseThrow(NullPointerException::new);
    System.out.println( greeting.regular("world"));
}

In the users project:

module pl.tfij.java9modules.app {
    exports pl.tfij.java9modules.app;
    uses pl.tfij.java9modules.app.IGreeting;
}

In the provider project:

module pl.tfij.java9modules.greetings {
    requires pl.tfij.java9modules.app;
    provides pl.tfij.java9modules.app.IGreeting
            with pl.tfij.java9modules.greetings.Greeting;
}

And finally the CLI usage

java --module-path mods --module pl.tfij.java9modules.app

Here is an example; Github example (Thanks for "tfij/" repository initial exemple)

Edit, I realized the repository already provides decoupling examples: https://github.com/tfij/Java-9-modules---reducing-coupling-of-modules

pdem
  • 3,880
  • 1
  • 24
  • 38
5

It sounds like you might want to use the ServicerLoader interface, which has been available since Java 6. However, bear in mind that, if you want to use Spring dependency injection, this is probably not what you want.

SeverityOne
  • 2,476
  • 12
  • 25
  • 2
    As far as I understand, the OP wants to load modules dynamically. So, `ServiceLoader` is only a part of the story. The second part is how to get a right `ClassLoader`. – ZhekaKozlov Feb 12 '18 at 10:20
  • @ZhekaKozlov Exactly, this is what i'm aiming at. If you would like to write what approach you would use as an answer, i can mark the most complete one as the answer to this question. It's just to have it as a reference for any other person reading it the next time – Mastarius Feb 12 '18 at 13:03
  • 3
    But what eactly do you mean by "dynamically"? Java classes are always loaded dynamically, as soon a they are needed. Typically, with a `ServiceLoader`, you would get services that express their capabilities, rather than directly implement them. The "just putting files in a folder" bit is covered by `ServiceLoader`; the "dynamic" bit could indeed be done with a `ClassLoader`, but again it depends on how you perceive the dynamic loading to work. – SeverityOne Feb 12 '18 at 13:14
  • 2
    @SeverityOne You are right. If the folder with modules is static, then `ClassLoader` is indeed unnecessary. However, if you want to give users the ability to import plugins from custom folders at runtime, you will have to use `ClassLoader`. – ZhekaKozlov Feb 12 '18 at 14:13
  • @SeverityOne I'll go with the ServiceLoader solution then, i'll study it in these days and use a fixed folder as a plugin folder for my .class modules (if i got it right). Thank you – Mastarius Feb 12 '18 at 20:55
  • Just to clarify, these `.jar` files have to be in your classpath. The simplest solution would be to modify the startup file to include this folder in the classpath. The problem with custom classloaders is that they get very complicated very quickly, and are remarkably difficult to get right. I always prefer the simplest solution, even if it's not the most technically sophisticated solution. – SeverityOne Feb 12 '18 at 22:47
4

There are two scenarios.

  1. Implementation jar's are on classpath
    In this scenario you can simply use ServiceLoader API (refer to @pdem answer)
  2. Implementation jar's not on classpath Lets Assume BankController is your interface and CoreController is your implementation.
    If you want to load its implementation dynamically from dynamic path,c create a new module layer and load class.

Refer to the following piece of code:

        private final BankController loadController(final BankConfig config) {
            System.out.println("Loading bank with config : " + JSON.toJson(config));
            try {
                //Curent ModuleLayer is usually boot layer. but it can be different if you are using multiple layers
                ModuleLayer currentModuleLayer       = this.getClass().getModule().getLayer(); //ModuleLayer.boot();
                final Set<Path> modulePathSet        = Set.of(new File("path of implementation").toPath());
                //ModuleFinder to find modules 
                final ModuleFinder moduleFinder      = ModuleFinder.of(modulePathSet.toArray(new Path[0]));
                //I really dont know why does it requires empty finder.
                final ModuleFinder emptyFinder       = ModuleFinder.of(new Path[0]);
                //ModuleNames to be loaded
                final Set<String>  moduleNames       = moduleFinder.findAll().stream().map(moduleRef -> moduleRef.descriptor().name()).collect(Collectors.toSet());
                // Unless you want to use URLClassloader for tomcat like situation, use Current Class Loader 
                final ClassLoader loader             = this.getClass().getClassLoader();
                //Derive new configuration from current module layer configuration
                final Configuration  configuration   = currentModuleLayer.configuration().resolveAndBind(moduleFinder, emptyFinder, moduleNames);
                //New Module layer derived from current modulee layer 
                final ModuleLayer    moduleLayer     = currentModuleLayer.defineModulesWithOneLoader(configuration, loader);
                //find module and load class Load class 
                final Class<?>       controllerClass = moduleLayer.findModule("org.util.npci.coreconnect").get().getClassLoader().loadClass("org.util.npci.coreconnect.CoreController");
                //create new instance of Implementation, in this case org.util.npci.coreconnect.CoreController implements org.util.npci.api.BankController
                final BankController bankController  = (BankController) controllerClass.getConstructors()[0].newInstance(config);
                return bankController;
            } catch (Exception e) {BootLogger.info(e);}
            return null;
        }

Reference : https://docs.oracle.com/javase/9/docs/api/java/lang/module/Configuration.html

enter image description here

krishna T
  • 425
  • 4
  • 14
  • 1
    I wonder if it would be better to put the empty ModuleFinder first, and the one with your real module path second. As I understand it, the first one is used to inject modules ahead of what would be found in the parent Configuration. Also, a tidier way to create the empty one would just be ModuleFinder.of(). – Chapman Flack Oct 27 '21 at 16:18
  • @ChapmanFlack As per documentation(updated link in answer) emptyfinder is passed as second argument to resolveAndBind method. I am not sure if order of finder has any significance. you are right about tidier way to create module finder is ModuleFinder.of(). – krishna T Oct 28 '21 at 12:28
  • 1
    In the documentation you linked for resolve(before,after,roots): "works exactly as specified by the static resolve method" which says in turn: "Each root module is located using the given before module finder. If a module is not found then it is located in the parent configuration .... If not found then the module is located using the given after module finder." So it depends on whether you want to find certain modules instead of the parent ones, or in addition to them. – Chapman Flack Oct 29 '21 at 14:38