解读:Java 11中的模块感知服务加载器

2019 年 1 月 10 日

解读:Java 11中的模块感知服务加载器

Java模块是一个自包含、自描述组件,隐藏了内部细节,为客户端使用提供接口、类和服务。Java的ServiceLoader可以用来加载实现给定服务接口程序。Java的服务加载机制可以通过库进行扩展,以减少样板代码,并提供一些有用的特性。


正文


本文要点


  • Java模块是一个自包含、自描述的组件,它隐藏内部细节,为客户端使用提供接口、类和服务。

  • 服务是一组我们熟知的接口或类(通常是抽象的)。服务提供程序是服务的具体实现。Java的ServiceLoader是一种用来加载实现了给定服务接口的服务提供程序的工具。

  • Java的服务加载机制可以通过库进行扩展,以减少样板代码,并提供一些有用的特性,如注入服务引用和激活给定的服务提供程序。


如果有机会在某个 Java 项目中使用 Simple Logging Facade for Java (SLF4J),你就会知道,它允许你(最终用户)在部署时插入你选择的日志框架,如 java.util.logging(JUL)、logback 或 log4j。在开发期间,你通常使用 SLF4J API,它提供了一个接口或抽象,你可以使用它来记录应用程序消息。


比如说,在部署期间,你最初选择 JUL 作为你的日志框架,但后来你注意到,日志性能没有达到标准。因为你的应用程序是按照 SLF4J 接口进行编码的,所以你可以很容易地插入高性能日志框架,如 log4j,而不需要修改任何代码及重新部署应用程序。应用程序本质上是一个可扩展的应用程序。它能够通过 SLF4J 在运行时选择类路径上可用的兼容的日志框架。


可扩展应用程序的特定部分可以扩展或增强,而不需要对应用程序的核心代码库进行代码更改。换句话说,应用程序可以通过接口编程和委托工作来定位和加载一个中心框架的具体实现,从而实现松耦合。


Java 为开发人员提供了在不修改原始代码库的情况下设计和实现可扩展应用程序的能力,其解决方案是服务和 ServiceLoader 类——在 Java 版本 6 中引入。SLF4J 使用这种服务加载机制来提供我们前面描述的插件模型。


当然,依赖注入或控制反转框架是达到这种目的的另一种方式。但是,本文将专注于原生解决方案。为了了解 ServiceLoader 机制,我们需要看一些 Java 语境下的定义:


  • 服务:一个服务就是我们所熟知的接口或类(通常是抽象的);

  • 服务提供程序:服务提供程序是服务的具体实现;

  • ServiceLoader:ServiceLoader是一种用来加载实现了给定服务接口的服务提供程序的工具。


有了这些定义,让我们来看一下如何构建一个可扩展的应用程序。假设一个虚拟的电子商务平台允许客户从一个支付服务提供程序列表中选择要部署在其站点上的服务。平台可以根据支付服务接口进行编码,该接口具有加载所需的支付服务提供程序的机制。开发人员和供应商可以使用一个或多个特定的实现提供支付功能。让我们先定义一个支付服务接口:


package com.mycommerce.payment.spi;
public interface PaymentService { Result charge(Invoice invoice);}
复制代码


在电子商务平台启动的时候,我们将使用类似下面这样的代码从 Java 的 ServiceLoader 类请求支付服务:


import java.util.Optional;import java.util.ServiceLoader;
import com.mycommerce.payment.spi;
Optional<PaymentService> loadPaymentService() { return ServiceLoader .load(PaymentService.class .findFirst();}
复制代码


在默认情况下,ServiceLoader 的“load”方法使用默认的类加载器搜索应用程序类路径。你可以使用重载的“load”方法传递自定义加载器来实现对服务提供程序的更复杂的搜索。为了使 ServiceLoader 定位服务提供程序,服务提供程序应该实现服务接口——在我们的例子中是 PaymentService 接口。下面是一个支付服务提供程序的例子:


package com.mycommerce.payment.stripe;
public class StripeService implements PaymentService { @Override public Result charge(Invoice invoice) { // 收取客户的费用并返回结果 ... return new Result.Builder() .build(); }}
复制代码


接下来,服务提供程序应通过创建一个提供程序配置文件来对自己进行注册,该文件必须保存在 META-INF/services 目录下,这也是保存服务提供程序 jar 文件的目录。配置文件的名称是服务提供程序的完全限定类名,名称的每个部分以句点(.)分割。文件本身应该包含服务提供程序的完全限定类名,每行一个。文件还必须是 UTF-8 编码的。文件中可以包含注释,注释行以井号(#)开始。


在我们的例子中,将 StripeService 注册为服务提供程序,我们必须创建一个名为“com.mycommerce.payment.spi.Payment”的文件,并添加以下行:


com.mycommerce.payment.stripe.StripeService
复制代码


使用上述设置和配置,该电子商务平台就可以在它们变得可用时加载新的支付服务提供程序,而不需要任何代码更改。遵循这个模式,你就可以构建可扩展的应用程序。


现在,随着 Java 9 中模块系统的引入,服务机制已经得到增强,可以支持模块所提供的功能强大的封装和配置。Java 模块是一个自包含、自描述的组件,它隐藏了内部细节,为客户端提供接口、类和服务。


让我们看一下,在新 Java 模块系统的语境下,如何定义和使用服务。使用我们前面定义的 PaymentService 创建相应的模块描述符:


module com.mycommerce.payment {    exports com.mycommerce.payment.spi;}
复制代码


通过配置其模块描述符,电子商务平台的主模块现在可以根据支付服务接口进行编码:


module com.mycommerce.main {    requires com.mycommerce.payment;     uses com.mycommerce.payment.spi.PaymentService;}
复制代码


注意,上面的模块描述符中使用了“uses”关键字。我们就是通过它通知 Java 我们需要它使用 ServiceLoader 类来定位和加载支付服务接口的具体实现。在应用程序启动(或稍后)过程中的某个点,主模块将使用类似下面这样的代码从 ServiceLoader 请求支付服务:


import java.util.Optional;import java.util.ServiceLoader;
import com.mycommerce.payment.spi;
Optional<PaymentService> loadPaymentService() { return ServiceLoader .load(PaymentService.class .findFirst();}
复制代码


为了让 ServiceLoader 能够定位支付服务提供程序,我们必须遵循一些规则。显然,服务提供程序需要实现 PaymentService 接口。然后,该支付服务提供程序的模块描述符应指定其意图,向客户端提供支付服务:


module com.mycommerce.payment.stripe {    requires com.mycommerce.payment;
exports com.mycommerce.payment.stripe; provides com.mycommerce.payment.spi.PaymentService with com.mycommerce.payment.stripe.StripeService;}
复制代码


如你所见,我们使用“provides”关键字指定这个模块提供的服务。“with”关键字用于指明实现给定服务接口的具体类。注意,单个模块中的多个具体实现可以提供相同的服务接口。一个模块也可以提供多个服务。


到目前为止一切顺利,但是,当我们开始使用这种新的服务机制实现一个完整的系统时,我们很快就会意识到,每次我们需要定位和加载一个服务时,都必须编写样板代码,每次加载服务提供程序时,都必须运行一些初始化逻辑,这使开发人员的工作变得更加繁琐和复杂。


典型的做法是将样板代码重构为实用工具类,并将其添加到应用程序中,作为和其他模块共享的公共模块的一部分。虽然这是个良好的开端,但是,由于 Java 模块系统提供的强大封装和可靠的配置保障,我们的实用工具方法将无法使用 ServiceLoader 类加载服务。


由于公共模块不知道给定的服务接口,其模块描述符中未包含“uses”子句,所以 ServiceLoader 不能定位实现服务接口的提供程序,尽管它们可能出现在模块路径中。不仅如此,如果你将“uses”子句添加到公共模块描述符中,就违背了封装的本意,更糟的是引入循环依赖。


我们将构建一个名为Susel的自定义库来解决上述问题。该库的主要目标是帮助开发人员构建利用原生 Java 模块系统构建模块化、可扩展的应用程序。该库将消除定位和加载服务所需的样板代码。此外,它允许服务提供程序编写者可以依赖于其他服务,而这些服务会自动定位并注入到给定的服务提供程序。Susel 还将提供一个简单的激活生命周期事件,服务提供程序可以使用该事件对自身进行配置并运行一些初始化逻辑。


首先,让我们看一下,Susel 如何解决因模块描述符中没有明确的“uses”子句而无法定位服务的问题。Java 的模块类方法“addUses()”提供了一种方法来更新模块,并添加一个依赖于给定服务接口的服务。该方法专门用于支持像 Susel 这样的库,它们使用 ServiceLoader 类来代表其他模块定位服务。下面的代码展示了我们如何使用这个方法:


var module = SuselImpl.class.getModule();module.addUses(PaymentService.class);
复制代码


如你所见,Susel 有到自己模块的引用,可以通过自我更新来确保 ServiceLoader 可以看到所请求的服务。在模块 API 上调用“addUses()”方法时有几个注意事项。首先,如果调用者模块是不同的模块(“this”),就会抛出 IllegalCallerException 异常。其次,该方法不适用于匿名模块和自动模块。


我们已经提到过,Susel 可以定位并将其他服务注入到给定的服务提供程序。Susel 借助构建时生成的注解和相关元数据提供了这项功能。让我们看一下注解。


@ServiceReference 注解用于标记引用类(服务提供者)中的公共方法,Susel 将使用它注入指定的服务。注解接受一个可选的 cardinality 参数。Susel 使用 Cardinality 来决定要注入的服务的数量,以及请求的服务是必须的还是可选的。


public @interface ServiceReference {    /**     * 指定引用者请求的服务cardinality     * 默认值是 {@link Cardinality#ONE}     *     * 返回引用者请求的服务cardinality     */    Cardinality cardinality() default Cardinality.ONE;}
复制代码


@Activate 注解用于标记服务提供程序类中的公共方法,Susel 将使用该方法来激活服务提供程序的实例。和该事件挂钩到的典型用例是一些重要方面的初始化,如服务提供程序的配置。


public @interface Activate {}
复制代码


Susel 提供了一个工具,它使用反射来构建给定模块的元数据。该工具会读取模块描述符识别出服务提供程序,对于每个服务提供程序,该工具会扫描带有 @ServiceReference 和 @Activate 注解的方法,并创建一个元数据条目。然后,该工具将元数据项保存到一个名为 susel.metadata 的文件中。该文件位于 META-INF 文件夹下,会和 jar 文件一起打包。现在,在运行时,当模块向 Susel 请求实现了特定服务接口的服务提供程序时,Susel 会执行以下步骤:


  • 调用Susel模块的addUses()方法使ServiceLoader定位请求的服务;

  • 调用ServiceLoader获取服务提供程序迭代器;

  • 对于每个服务提供程序,加载并获取包含服务提供程序的模块的元数据;

  • 定位与服务提供程序相对应的元数据项;

  • 对于元数据项中指定的每个服务引用从步骤1开始重复上述过程;

  • 如果注册了可选的激活事件,则通过传递全局上下文来触发激活;

  • 返回完全加载的服务提供程序的列表。


下面是一个执行上述步骤的高级代码片段:


public <S> List<S> getAll(Class<S> service{    List<S> serviceProviders = new ArrayList<>();           // Susel的模块应该指明使用给定服务的意图,    // 以便ServiceLoader可以查找所请求的服务提供程序    SUSEL_MODULE.addUses(service);
// 传递通常加载Susel的应用程序模块层 var iterator = ServiceLoader.load(SUSEL_MODULE.getLayer(), service); for (S serviceProvider : iterator) { // 加载元数据注入引用并激活服务 prepare(serviceProvider); serviceProviders.add(serviceProvider); } return serviceProviders;}
复制代码


请注意下我们如何使用 ServiceLoader 类中的重载方法 load()来传递应用程序模块层。这种重载方法(在 Java 9 中引入)会为给定的服务接口创建一个新的服务加载器,并从给定模块层及其祖先的模块中加载服务提供程序。


值得一提的是,为了避免在应用程序运行时进行大量反射,在定位和加载服务提供程序时,Susel 会使用元数据文件来标识服务引用和激活方法。还有一点要注意,虽然 Susel 具有 OSGI(Java 生态系统中一个可用的成熟而强大的模块系统)和/或 IoC 框架的一些特性,但 Susel 的目标是通过原生 Java 模块系统增强服务加载机制,减少定位和调用服务所需的样板代码。


让我们看一下,如何在我们的支付服务示例中使用 Susel。假设我们使用 Stripe 实现了一个支付服务。下面的代码片段展示了 Susel 的注解:


package com.mycommerce.payment.stripe;
import io.github.udaychandra.susel.api.Activate;import io.github.udaychandra.susel.api.Context;import io.github.udaychandra.susel.api.ServiceReference;
public class StripeService implements PaymentService { private CustomerService customerService; private String stripeSvcToken; @ServiceReference public void setCustomerService(CustomerService customerService) { this.customerService = customerService; } @Activate public void activate(Context context) { stripeSvcToken = context.value("STRIPE_TOKEN"); }
@Override public Result charge(Invoice invoice) { var customer = customerService.get(invoice.customerID()); // 使用customer服务和stripe token来调用Stripe // 服务,收取客户的费用 ... return new Result.Builder() .build(); }}
复制代码


为了在构建阶段生成元数据,我们必须调用 Susel 的工具。有一个现成的 gradle插件可以自动完成这个步骤。让我们看一个 build.gradle 示例文件,它会自动配置该工具以便在构建阶段调用。


plugins {    id "java"    id "com.zyxist.chainsaw" version "0.3.0"    id "io.github.udaychandra.susel" version "0.1.2"}
dependencies { compile "io.github.udaychandra.susel:susel:0.1.2"}
复制代码


请注意下我们如何把两个自定义插件与 Java 模块系统及 Susel 搭配使用。chainsaw插件帮助 gradle 构建模块 jar 包。Susel 插件帮助创建和打包关于服务提供程序的元数据。


最后,让我们来看一个代码片段,在应用程序启动期间引导 Susel 并从 Susel 检索支付服务提供程序:


package com.mycommerce.main;
import com.mycommerce.payment.spi.PaymentService;import io.github.udaychandra.susel.api.Susel;
public class Launcher { public static void main(String... args) { // 在理想情况下,配置应该从外部源加载 Susel.bootstrap(Map.of("STRIPE_TOKEN""dev_token123")); ... // Susel将加载它在其模块层中发现的Stripe服务提供程序 // 并准备好该服务供客户端使用 var paymentService = Susel.getPaymentService.class); paymentService.charge(invoice); }}
复制代码


现在,我们可以使用 gradle 构建模块化 jar 并运行示例应用程序。下面是要运行的命令:


java --module-path :build/libs/:$JAVA_HOME/jmods \     -m com.mycommerce.main/com.mycommerce.main.Launcher
复制代码


为支持 Java 模块系统,现有的命令行工具(如“Java”)添加了新的选项。让我们看一下,可以在上面的命令中使用的新选项:


  • -p或–module-path用于告诉Java查看包含Java模块的特定文件夹;

  • -m或–module用于指定用于启动应用程序的模块和主类。


当你开始使用 Java 模块系统开发应用程序时,你可以利用模块解析策略来创建特别的 Java 运行时环境(JRE)发行版。这些自定义的发行版或运行时镜像只包含运行应用程序所需的模块。Java 9 引入了一个名为jlink的新组装工具,可用于创建自定义运行时镜像。不过,我们应该知道,与 ServiceLoader 的运行时模块解析相比,它的模块解析是如何实现的。由于服务提供程序几乎总是被认为是可选的,jlink 不能根据 “uses” 子句自动解析包含服务提供程序的模块。jlink 提供了几个选项帮助我们解析服务提供程序模块:


  • –bind-services用于让jlink解析所有服务提供程序及其依赖;

  • –suggest-providers用于让jlink提供模块路径中实现了服务接口的提供程序。


建议使用–suggest-providers,只添加那些对你的特定用例有意义的模块,而不是盲目地使用–bind-services 添加所有可用的提供程序。让我们借助我们的支付服务示例实际地看一下–suggest-providers 开关:


"${JAVA_HOME}/bin/jlink" --module-path "build/libs" \    --add-modules com.mycommerce.main \    --suggest-providers com.mycommerce.payment.PaymentService
复制代码


上述命令的输出类似下面这个样子:


Suggested providers:  com.mycommerce.payment.stripe provides  com.mycommerce.payment.PaymentService used by  com.mycommerce.main
复制代码


有了这些知识,你现在就可以创建自定义镜像并打包运行应用程序和加载所需服务提供程序所需的所有模块。


小结


本文描述了 Java 服务加载机制以及为了支持原生 Java 模块系统而对其进行的更改,介绍了名为 Susel 的试验性库,它可以帮助开发人员利用原生 Java 模块系统构建模块化、可扩展的应用程序。该库消除了定位和加载服务所需的样板代码。此外,它允许服务提供程序编写者依赖于其他可以自动定位并注入给定程序服务。


关于作者


Uday Tatiraju是 Oracle 首席工程师,有十多年电子商务平台、搜索引擎、后端系统、Web 和移动编程经验。


查看英文原文:Super Charge the Module Aware Service Loader in Java 11


2019 年 1 月 10 日 12:007365
用户头像

发布了 323 篇内容, 共 140.6 次阅读, 收获喜欢 661 次。

关注

评论

发布
暂无评论
发现更多内容

涨薪神作!华为内部操作系统与网络协议笔记爆火,Java程序员有福了

Java架构之路

Java 程序员 面试 编程语言

数字人民币都来了 黄金还有什么用?

CECBC区块链专委会

数字货币

简要分析近几年商业软件开发平台的现状

Philips

敏捷开发 快速开发 企业应用

Docker

Pulsar Summit Asia 2020 中文专场议题出炉!

Apache Pulsar

大数据 开源 Apache Pulsar

从一场“众盟科技云滇之播”,我们发现了美食直播的商业与公益价值

人称T客

阿里云视频云实时字幕技术,助力英雄联盟S10全球总决赛

阿里云视频云

游戏开发 直播 语音识别 字幕

阿里顶级DBA专家团又一力作——MySQL8的150高效技巧

周老师

Java 编程 程序员 架构 面试

云计算简史(完整版)

明道云

Java垃圾回收GC概览

Java JVM GC

把最新JAVA面试真题(阿里/字节跳动/美团)整理出来,却被自己菜哭了,赶紧去刷题了

Java架构追梦

Java 阿里巴巴 架构 面试 面试题

【T1543.003】利用 ACL 隐藏恶意 Windows 服务

比伯

Java 大数据 编程 架构 计算机

从零到千万用户,我是如何一步步优化MySQL数据库的?

冰河

数据库 架构 性能优化 分布式数据库 分布式存储

【算法题目解析】杨氏矩阵数字查找

程序员架构进阶

算法 二分查找 杨氏矩阵

Java程序员必备,Github上星标55.9k的微服务神级笔记简直太香了,学完感觉自己又行了!

Java架构之路

Java 程序员 架构 面试 编程语言

TCP性能分析与调优策略

云流

程序员 计算机网络 网络协议

马士兵老师首推Java七条自学路线,自学到底能不能行?自学也能拿到40W年薪?

Java架构追梦

Java 架构 面试 马士兵 项目实战

cglib入门后篇

Rayjun

Java cglib

JMeter100个线程竟然只模拟出1个并发

dongfanger

软件测试 Jmeter 性能测试 压力测试 测试工具

京东技术中台Flutter实践之路(二)

京东智联云开发者

开源 中台 大前端 Web UI

数字货币交易所开发源码,场外币币交易平台搭建

WX13823153201

数字货币交易所开发

非线性声学回声如何破解?华为云硬核技术为你解决

华为云开发者社区

算法 音视频 音视频算法

完美!阿里P8都赞不绝口的世界独一份489页SQL优化笔记

马士兵老师

Java 数据库 程序员 架构师 SQL优化

IPFS云算力挖矿系统开发技术

薇電13242772558

区块链 IPFS

当代开发者的六大真实现状,你被哪一个场景“戳中”了?

华为云开发者社区

开发者 调研 报告

《迅雷链精品课》第二课:区块链核心技术框架

迅雷链

区块链

LeetCode题解:剑指 Offer 22. 链表中倒数第k个节点,使用数组,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

可以解除程序员中年危机的职业规划

Java架构师迁哥

anyRTC Flutter SDK :全面实现跨平台音视频互动

anyRTC开发者

音视频 WebRTC RTC sdk 安卓

Redis基础—了解Redis是如何做数据持久化的

云流

数据库 redis 编程 计算机

JVM真香系列:轻松掌握JVM运行时数据区

田维常

JVM

解读:Java 11中的模块感知服务加载器 -InfoQ