【问题标题】:Best approach to dynamically load modules (classes) in Java在 Java 中动态加载模块(类)的最佳方法
【发布时间】:2018-07-22 09:06:53
【问题描述】:

我目前正在编写一个需要在不同类型的设备上运行的应用程序。我的方法是制作一个“模块化”应用程序,可以根据需要操作的设备动态加载不同的类。

为了使应用程序易于扩展,我的目标是为其他模块(.jar 或 .class 文件)分配特定路径,使核心程序保持原样。当不同的客户需要不同的模块时,这一点至关重要(无需为每个客户编译不同的应用程序)。

这些模块将实现一个通用接口,而“核心”应用程序可以使用接口上定义的这些方法,并让单个实现完成工作。 按需加载它们的最佳方式是什么?我正在考虑使用 URLClassLoader,但我不知道这种方法是否符合新模式和 Java 趋势的最新情况,就像我想的那样喜欢避免设计不良的应用程序和不推荐使用的技术。 使用 JDK 9 制作模块化且易于扩展的应用程序的最佳替代方法是什么(只需将模块文件添加到文件夹即可扩展)?

【问题讨论】:

  • 没有最好的方法。所有方法都有其优点和缺点。 URLClassLoader 是最古老也是最知名的方法。 Java 9 中还有一个新方法Lookup.defineClass,但它只能在同一个包中加载一个类。您可能还应该看看 OSGi,但它可能非常麻烦。
  • 您可以使用依赖注入框架来为您管理布线。或者您可以使用您的构建系统来创建和管理特定于设备的解决方案(例如 Maven 程序集)。
  • 确保探索服务和 ServiceLoader。可能您可以为每种设备类型创建服务提供者,每个都实现核心应用程序使用的服务接口。

标签: java java-9 java-module


【解决方案1】:

听起来你可能想使用自 Java 6 以来就可用的ServicerLoader 接口。但是,请记住,如果你想使用 Spring 依赖注入,这可能不是你想要的。

【讨论】:

  • 据我了解,OP 想要动态加载模块。所以,ServiceLoader 只是故事的一部分。第二部分是如何获得正确的ClassLoader
  • @ZhekaKozlov 没错,这就是我的目标。如果您想写出您将使用哪种方法作为答案,我可以将最完整的方法标记为该问题的答案。只是为了让其他人下次阅读时可以参考
  • 但是“动态”到底是什么意思? Java 类总是在需要时动态加载。通常,使用ServiceLoader,您将获得表达其功能的服务,而不是直接实现它们。 ServiceLoader 涵盖了“仅将文件放入文件夹”位; “动态”位确实可以使用ClassLoader 完成,但这又取决于您如何看待动态加载的工作。
  • @SeverityOne 你是对的。如果包含模块的文件夹是静态的,那么ClassLoader 确实是不必要的。但是,如果您想让用户能够在运行时从自定义文件夹中导入插件,则必须使用 ClassLoader
  • @SeverityOne 然后我将使用 ServiceLoader 解决方案,这些天我会研究它并使用一个固定文件夹作为我的 .class 模块的插件文件夹(如果我做对了)。谢谢
【解决方案2】:

除了@SeverityOne 给出的ServicerLoader 用法,您可以使用module-info.java 来声明接口的不同实例化,使用“uses”/“provides”关键字。

然后你使用模块路径而不是类路径,它会加载包含你的模块的所有目录,不需要创建特定的类加载器

serviceLoader的用法:

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"));
}

在用户项目中:

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

在提供者项目中:

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

最后是 CLI 用法

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

这是一个例子; Github example(感谢“tfij/”存储库初始示例)

编辑,我意识到存储库已经提供了解耦示例: https://github.com/tfij/Java-9-modules---reducing-coupling-of-modules

【讨论】:

    【解决方案3】:

    有两种情况。

    1. 实现 jar 位于类路径
      在这种情况下,您可以简单地使用 ServiceLoader API(参考@pdem 答案)
    2. 实现 jar 不在类路径中 假设 BankController 是您的接口,CoreController 是您的实现。
      如果您想从动态路径动态加载其实现,请创建一个新的模块层并加载类。

    参考以下代码:

            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;
            }
    

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

    【讨论】:

    • 我想知道是否最好将空的 ModuleFinder 放在第一位,然后将具有真实模块路径的模块放在第二位。据我了解,第一个用于在父配置中找到模块之前注入模块。此外,创建空的更简洁的方法就是 ModuleFinder.of()。
    • @ChapmanFlack 根据文档(答案中的更新链接),emptyfinder 作为第二个参数传递给 resolveAndBind 方法。我不确定取景器的顺序是否有任何意义。你说得对,创建模块查找器的更简洁的方法是 ModuleFinder.of()。
    • 在您为 resolve(before,after,roots) 链接的文档中:“完全按照静态解析方法指定的方式工作”,这反过来又说:“每个根模块都使用给定的 before 模块定位finder。如果找不到模块,则它位于父配置中....如果未找到,则使用给定的后模块查找器定位模块。所以这取决于你是想找到某些模块而不是父模块,还是除了它们之外。
    猜你喜欢
    • 1970-01-01
    • 2013-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-22
    • 1970-01-01
    • 2011-09-07
    相关资源
    最近更新 更多