ServiceLoader在包含模块未在其module-info.java描述符中声明provides ... with指令时无法找到提供者。Java平台模块系统(JPMS)默认执行严格封装,阻止ServiceLoader(位于java.base)访问未导出或未打开的包中的类。provides指令作为合同声明,授予ServiceLoader特权反射访问权限,以实例化指定的提供者类,绕过正常的包访问限制,而不需要将包导出给所有模块。
背景:一个遗留的企业CRM系统正在从Java 8迁移到Java 17。目标是将单体架构模块化为不同的领域:crm-core、crm-email和crm-api。crm-email模块包含crm-api中定义的NotificationService接口的实现。
迁移后,应用在启动时引发了ServiceConfigurationError。尽管EmailNotificationService类是公共的,且JAR文件已存在于模块路径上,问题依然存在。堆栈跟踪表明未找到服务类型的提供者,导致通知子系统初始化失败。
问题:开发团队假设实现类的公共可见性足够。这反映了类路径时代的假设,即公共类是全局可见的。然而,JPMS阻止ServiceLoader访问其他模块中未导出包中的类。crm-email模块未导出com.crm.email.internal包。关键是,module-info.java缺少provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService声明。因此,ServiceLoader无法找到或实例化提供者,因为模块系统将实现视为封装的内部细节。
考虑的解决方案:
导出包:在crm-email模块描述符中添加exports com.crm.email.internal;。这种方法被拒绝,因为它会将内部实现细节暴露给所有其他模块。这违反了封装,并产生了模块系统旨在防止的紧密耦合。
打开包以供反射:使用opens com.crm.email.internal;或特别的opens com.crm.email.internal to java.base;。虽然这允许反射访问,但被认为过于宽松且语义上不正确。它表示该包一般接受深度反射,而不是通过受控机制特定提供服务。
使用provides ... with指令:在module-info.java中添加声明provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService;。这是符合JPMS惯例的解决方案。它明确声明了服务关系,并授予ServiceLoader必要的访问权限来实例化类,同时保持严格的封装。
选择的解决方案:团队选择了第三种方案。这种方法不需要对实现代码本身进行更改。它保留了包的内部可见性,同时在模块元数据中明确了服务依赖关系。
结果:应用成功地在运行时加载了EmailNotificationService。模块边界保持完整,防止其他模块直接实例化或依赖内部实现类。ServiceLoader能够通过声明的合同正确发现和提供服务。
为什么ServiceLoader要求提供者类必须具备公共的零参数构造函数,如果违反了这个约束,会出现什么特定的异常?
ServiceLoader通过反射使用Class.getConstructor().newInstance()来实例化提供者类。这严格要求有一个公共无参数构造函数。如果这个构造函数缺失或不是公共的,ServiceLoader将抛出ServiceConfigurationError。这个错误通常被包装在NoSuchMethodException或IllegalAccessException中,在迭代器遍历期间显现。候选人通常忽视必须显式提供这个构造函数,如果定义了其他构造函数。他们还错过了实例化是懒加载的,发生在调用Iterator.next()时,而不是在初始的ServiceLoader.load()调用期间。
当服务接口位于命名模块中而实现位于未命名模块中时,ServiceLoader机制如何处理提供者类?
当服务接口位于命名模块中但实现位于未命名模块(类路径)时,ServiceLoader仍然可以找到提供者。未命名模块隐式读取所有命名模块,而所有命名模块隐式读取未命名模块。然而,提供者类仍然必须是公共的,并且具有公共的无参数构造函数。一个常见的误解是,强封装完全阻止这种情况。实际上,未命名模块充当兼容性层。未命名模块中的提供者不能被未显式读取未命名模块的命名模块中的代码访问。这创建了一个方向性可访问性约束,候选人常常未能考虑。
ServiceLoader.loadInstalled()方法与ServiceLoader.load()方法在类加载器委托和提供者可见性方面有什么区别?
ServiceLoader.loadInstalled()使用系统类加载器(或现代JVM版本中的平台类加载器)搜索提供者。它将发现限制在已安装扩展目录或平台模块。它明确忽略应用模块路径或类路径上的提供者。相反,ServiceLoader.load()通常使用线程上下文类加载器或指定的类加载器。这使得它能够发现应用级提供者。候选人常常将这两个方法混淆,导致默默失败,应用提供者未被找到。这是因为使用了不正确的loadInstalled(),希望它像标准加载方法一样运行,但可见性更广。