接线与查找
Java长期以来都有一个ServiceLoader
类。 它是在1.6中引入的,但是自Java 1.2以来就使用了类似的技术。 一些软件组件使用了它,但是使用并不广泛。 它可以用于模块化应用程序(甚至更多),并提供一种使用应用程序不依赖于编译时间的插件扩展应用程序的方法。 而且,这些服务的配置非常简单:只需将其放在类/模块路径上即可。 我们将看到详细信息。
服务加载器可以定位某些接口的实现。 在EE环境中,还有其他方法可以配置实现。 在非EE环境中,Spring变得无处不在,它具有相似的解决方案,尽管对相似但并非完全相同的问题的解决方案并不完全相同。 Spring提供的控制反转(IoC)和依赖注入(DI)是解决不同组件布线的解决方案,并且是行业最佳实践,如何将布线描述/代码与功能的实际实现分开该类必须执行。
实际上,Spring还支持使用服务加载器,因此您可以连接由服务加载器定位并实例化的实现。 您可以在此处找到一篇简短且写得很好的文章。
ServiceLoader
在我们将其注入需要它的组件之前,是关于如何找到实现的更多信息。 初级程序员有时会错误地将两者混为一谈,这并非没有道理:它们之间有着密切的联系。
也许由于这个原因,大多数应用程序,至少我所看到的那些应用程序,并没有将接线和实现的发现分开。 这些应用程序通常使用Spring配置进行查找和接线,这没关系。 尽管这是一种简化,但我们应该对此感到满意并感到高兴。 我们不应仅仅因为可以就将两个功能分开。 大多数应用程序不需要将它们分开。 它们整齐地坐在Spring应用程序的XML配置的简单行上。
我们应该在需要的抽象水平上进行编程,但绝对不要再抽象。
是的,这句话是爱因斯坦的一句话的解释。 如果您考虑一下,您还可以意识到,此声明只不过是KISS原理(保持简单而愚蠢)。 代码,不是你。
ServiceLoader
查找某个类的实现。 并非所有可能在类路径上的实现。 它仅查找那些“广告”的广告。 (我将在稍后说明“公告”的含义。)Java程序无法遍历类路径上的所有类,或者它们可以遍历吗?
浏览类路径
本节稍作绕道,但重要的是要理解ServiceLoader
为何以这种方式工作,甚至在我们讨论其工作方式之前。
Java代码无法查询类加载器以列出类路径上的所有类。 您可能会说我撒谎,因为Spring确实浏览了这些类并自动找到了实现候选者。 春天实际上是作弊。 我会告诉你它是怎么做的。 现在,请接受不能浏览类路径。 如果查看ClassLoader
的文档,则找不到会返回类的数组,流或集合的任何方法。 您可以获取软件包的数组,但是即使从软件包中也无法获取类。
其原因是Java处理类的抽象级别。 类加载器将类加载到JVM中,而JVM不在乎。 它不假定实际的类在文件中。 有很多应用程序可以从文件而不是文件中加载类。 实际上,大多数应用程序都从不同的媒体加载某些类。 还有您的程序,您可能不知道。 您是否曾经使用过Spring,Hibernate或其他框架? 这些框架大多数都在运行时创建代理对象,并使用特殊的类加载器从内存中加载这些对象。 类加载器无法告诉您它支持的框架是否会创建一个新对象。 在这种情况下,类路径不是静态的。 这些特殊类加载器甚至没有类路径之类的东西。 他们动态地找到类。
好的。 说得好,并详细介绍。 但是再说一遍:Spring如何找到类? Spring实际上做出了一个大胆的假设。 假定类加载器是一种特殊的加载器: URLClassLoader
。 (并且正如Nicolai Parlog在他的文章中所写,Java 9不再适用。)它与包含URL的类路径一起使用,并且可以返回URL数组。
ServiceLoader
不会做出这样的假设,因此不会浏览类。
ServiceLoader如何查找类
ServiceLoader可以查找和实例化实现特定接口的类。 当我们调用静态方法ServiceLoader.load(interfaceKlass)
,它将返回实现此接口的类的“列表”。 我在引号之间使用“列表”,因为从技术上讲,它返回一个ServiceLoader
实例,该实例本身实现Iterable
因此我们可以迭代实现该接口的类的实例。 迭代通常在for
循环中完成,该循环在(:)冒号之后调用load()
方法。
为了成功找到实例,包含实现的JAR文件应该在目录META-INF/service
具有一个特殊文件,该文件应具有接口的完全限定名称。 是的,名称中包含点,并且没有任何特定的文件扩展名,但是,它必须是文本文件。 它必须包含在该JAR文件中实现接口的类的标准名称。
ServiceLoader
调用ClassLoader
方法findResources
来获取文件的URL,并读取类的名称,然后再次要求ClassLoader
加载这些类。 这些类应具有一个公共的零参数构造函数,以便ServiceLoader
可以实例化每个实例。
使这些文件包含类的名称,以使用资源加载来搭载类和实例化,但效果并不理想。
Java 9在保留烦人的META-INF/services
解决方案的同时引入了一种新方法。 随着拼图的引入,我们有了模块,而模块有了模块描述符。 模块可以定义ServiceLoader
可以加载的服务,模块还可以指定可能需要通过ServiceLoader
加载哪些服务。 发现服务接口实现的这种新方式从文本资源转移到Java代码。 它的纯粹优点是可以在编译期间或模块加载时间识别与错误名称相关的编码错误,以使失败的代码更快地失败。
为了使事情变得更加灵活,或者只是使它们变得无用的变得更加复杂(将来会告诉人们),如果该类不是服务接口的实现,但确实具有返回该类实例的public static provider()
方法,则Java 9也可以使用实现该接口。 (顺便说一句:在这种情况下,提供程序类甚至可以根据需要实现服务接口,但是通常它是工厂,所以为什么要这样做。请注意SRP。)
样例代码
您可以从https://github.com/verhas/module-test
下载多模块Maven项目。
该项目包含三个模块Consumer
, Provider
和ServiceInterface
。 使用者调用ServiceLoader
并使用服务,该服务由ServiceInterface
模块中的接口javax0.serviceinterface.ServiceInterface
定义,并在Provider
模块中实现。 下图显示了代码的结构:
module-info
文件包含以下声明:
module Provider {requires ServiceInterface;provides javax0.serviceinterface.ServiceInterfacewith javax0.serviceprovider.Provider;
}module Consumer {requires ServiceInterface;uses javax0.serviceinterface.ServiceInterface;
}module ServiceInterface {exports javax0.serviceinterface;
}
陷阱
在这里,我将告诉您我在创建此非常简单的示例时所犯的一些愚蠢的错误,以便您可以从错误中学习,而不必重复这些错误。 首先, ServiceLoader
中的Java 9 JDK文档中有一句话是:
另外,如果服务不在应用程序模块中,则模块声明必须具有一个require指令,该指令指定导出服务的模块。
我不知道它想说什么,但是对我来说意味着什么是不正确的。 也许我误解了这句话,这很可能。
看我们的示例, Consumer
模块使用实现javax0.serviceinterface.ServiceInterface
接口的东西。 这实际上是Provider
模块及其中的实现,但是它仅在运行时确定,并且可以由任何其他合适的实现替换。 因此,它需要接口,因此必须在requires
ServiceInterface
模块的模块信息文件中具有ServiceInterface
指令。 它不需要Provider
模块! Provider
模块类似地依赖于ServiceInterface
模块,并且必须要求它。 ServiceInterface
模块不需要任何内容。 它仅导出包含接口的包。
同样重要的是要注意,不需要Provider
模块和Consumer
模块都可以导出任何程序包。 Provider
提供由接口声明的服务,并由模块信息文件中以with
关键字命名的类实现。 它为世界提供了这一类,仅此而已。 如果仅提供此类,则导出包含它的包将是多余的,并且可能不必要地打开同一包中可能发生但属于模块内部的类。 使用–m
选项从命令行调用Consumer
,并且它也不需要模块导出任何包。
像启动程序一样的命令是
java -p Consumer/target/Consumer-1.0.0-SNAPSHOT.jar:ServiceInterface/target/ServiceInterface-1.0.0-SNAPSHOT.jar:Provider/target/Provider-1.0.0-SNAPSHOT.jar -m Consumer/javax0.serviceconsumer.Consumer
它可以在成功执行mvn
install命令后执行。 请注意,maven编译器插件必须至少为3.6版,否则,在编译期间,ServiceInterface-1.0.0-SNAPSHOT.jar将位于类路径而不是模块路径上,并且编译将无法找到module-info.class
文件。
有什么意义
当应用程序仅在运行时与某些模块连接时,才可以使用ServiceLoader
。 一个典型的示例是带有插件的应用程序。 当我将ScriptBasic for Java从Java 7移植到Java 9时,我自己就参与了该练习。BASIC语言解释器可以由包含公共静态方法的类扩展,并且必须将其注释为BasicFunction
。 最后一个版本要求嵌入解释器的主机应用程序列出所有在代码中调用API的扩展类。 这是多余的,不需要的。 ServiceLoader
可以找到在ClassSetProvider
定义了接口( ClassSetProvider
)的服务实现,然后主程序可以依次调用服务实现并注册在集合中返回的类。 这样,主机应用程序无需了解有关扩展类的任何信息,将扩展类放在模块路径上就可以了,每个扩展类都可以提供服务。
JDK本身也使用此机制来定位记录器。 新的Java 9 JDK包含System.LoggerFinder
类,可以通过任何模块将其实现为服务,并且如果存在实现,则ServiceLoader
可以找到方法System.getLogger()
将找到该类。 这样,日志记录不绑定到JDK,也不在编译时绑定到库。 在运行时和应用程序之间提供记录器就足够了,应用程序使用的库和JDK都将使用相同的记录工具。
通过服务加载机制中的所有这些更改,并使之成为语言的一部分,而不再依赖于资源加载,人们可能希望这种类型的服务发现将获得动力,并将像以前一样被广泛使用。
翻译自: https://www.javacodegeeks.com/2018/01/java-9-module-services.html