java 模块化
在过去的几年中,Java的模块化一直是一个活跃的讨论主题。 从(现在已经不复存在的) JSR 277到对JSR 291和正在进行的JSR 294的认可,模块化被视为Java演进的必要步骤。 甚至像Scala这样基于JVM的未来语言都在考虑模块化 。 本文是有关模块化Java的多部分系列文章的第一篇,讨论了模块化的含义以及为什么要关注它。
什么是模块化?
模块化是一个通用概念,它以允许开发单个模块的方式应用于软件开发,通常具有标准化的接口以允许模块进行通信。 实际上,OO语言中对象之间的关注点分离类型与模块的概念几乎相同,只是规模更大。 通常,将系统划分为模块有助于最大程度地减少耦合 ,这将导致维护代码更容易。
Java语言在设计时并未考虑模块(除了软件包,在介绍中类似于Modula-3模块),但Java社区中实际上有许多模块。 实际上,任何Java库都是一个模块,从Log4J到Hibernate再到Tomcat。 通常,开源应用程序和封闭源代码应用程序都将对外部库具有一个或多个依赖关系,而外部库又对其他应用程序具有传递依赖关系。
库也是模块
库是隐式的模块。 它们可能不全都有一个单独的接口与之通信,但是经常会有记录了用例的“ 公共 ” API(应使用)和“ 私有 ”包。 此外,它们本身具有依赖项(例如JMX或JMS )。 这可能导致自动依赖项管理器带来的收益远远超出严格的要求; 在Log4J-1.2.15的情况下,引入了10多个依赖项(包括javax.mail
和javax.jms
),即使使用Log4J的程序从不需要使用许多依赖项。
在某些情况下,模块的依赖关系可以是可选的 。 也就是说,模块可以提供缺少依赖项的功能子集。 在上面的示例中,如果运行时类路径中不存在JMS,则通过JMS进行日志记录将不可用,但其他机制将可用。 (Java通过使用延迟链接来实现这一点;通过在访问类之前不要求提供类,可以通过适当的ClassNotFoundException
处理缺少的依赖关系。其他平台具有弱链接的概念,该链接执行的运行时检查几乎相同)
通常,模块具有附加的版本号。 许多开源项目生成log4j-1.2.15.jar
都与log4j-1.2.15.jar
。 这使开发人员可以通过在运行时进行手动检查,来确定要使用的特定版本的开源库的版本,方法是查询类路径。 但是,该程序可能已针对该库的其他版本进行了编译。 隐含的假设是,对编制log4j-1.2.3.jar
和对运行log4j-1.2.15.jar
将是行为上兼容。 甚至升级到下一个次要版本也通常是兼容的(这就是为什么log4j 1.3中的问题导致新的分支2.0表示兼容性中断)。 所有这些通常都是基于约定,而不是运行时已知的约束。
模块化何时有用?
模块化可用作将应用程序分解为不同部分的通用概念,然后可以分别对其进行测试(和改进)。 如上所述,大多数库无论如何都是模块,因此对于那些生产供他人使用的库的人来说,模块化是一个重要的概念。 通常,依赖项信息在构建工具(maven pom或ivy-module)中进行编码,并明确记录在库的使用说明中。 上游库为低级库中的错误开发解决方法的情况并不少见,即使此后已修复了低级库的最新版本,也可以在高级库中提供无缝的体验。 (但是,有时它们可能会引起一些细微的问题 。)
如果要为他人使用而构建一个库,那么它已经隐式地是一个模块。 但是,与几乎没有“ Hello World”库的方式一样,也没有真正的“ Hello World”模块。 只有在应用程序变得足够大(或者使用足够模块化的构建系统进行构建)之后,才能将逻辑上将应用程序分解为不同部分的概念发挥作用。
测试的好处是模块化的一个方面。 较小的模块(具有定义良好的API)通常可以比整体应用程序进行更好的测试。 对于GUI应用程序尤其如此,在GUI应用程序中,GUI本身可能不容易测试,但其调用的代码却可能很容易测试。
另一个方面是进化。 尽管整个系统都有版本号,但实际上,它是多个模块和版本的幕后产物(无论是封闭源代码还是开放源代码,总会有某种库,甚至是Java版本)是系统的依赖项)。 结果,每个模块都可以按照适合该模块的方式自由发展。 一些模块的发展可能比其他模块快,而某些模块可能足够稳定,可以长期固定(例如,Eclipse 3.5具有org.eclipse.core.boot
,自2008年2月以来一直保持不变)。
项目管理也可以从模块化中受益。 鉴于某个模块最终将拥有已发布的API,其他人可以订阅该API,因此可以由单独的团队来实现单独的模块。 无论如何,这不可避免地会在大型项目中发生,但是可以让子团队负责交付不同的模块。
最后,对应用程序进行模块化可以帮助具体识别使用哪个版本的依赖库,以便在整个大型项目中协调依赖库。
运行时与编译时
Java通常具有平坦的类路径,无论是在编译时还是在运行时。 换句话说,应用程序通常对在类路径上找到的任何类具有完全可见性,而不管类路径中条目的顺序如何(假定至少没有重叠;否则,第一个获胜)。 这将启用Java中动态链接的功能。 从类路径的最前面加载的类无需解析对类的所有引用,这些引用可能朝向类路径的最后面,直到实际需要它们为止。
当处理一组接口(直到运行时才知道该实现)时,通常使用此方法。 例如,可以对通用JDBC包编译SQL实用程序,但是在运行时(以及附加的配置信息)可以实例化正确的JDBC驱动程序。 通常,这是通过在运行时提供给Class.forName
查找的类的名称来实现的(该类实现了预定义的工厂接口或抽象类)。 如果指定的类不存在(或由于任何其他原因而无法加载),则会生成错误。
因此,编译时类路径很可能(巧妙地)与模块的运行时类路径不同。 此外,每个模块通常都是独立编译的(模块A可以针对模块C 1.1进行编译,而模块B可以针对模块C 1.2进行编译),然后在运行时以单个路径进行组合(在这种情况下,可以任意选择版本模块C的1.1或1.2)。 这会Swift导致Dependency Hell ,尤其是当这些依赖项的传递性闭合形成运行时类路径时。 Maven和Ivy之类的构建系统使模块化(即使不是最终用户)对开发人员可见。
Java具有称为ClassLoader的未受赞赏的功能,该功能允许对运行时路径进行更多分段。 通常,所有类都是从系统ClassLoader加载的。 但是,某些系统使用不同的ClassLoader划分其运行时空间。 Tomcat(或其他Servlet引擎)就是一个很好的例子,它通常具有一个ClassLoader-per-WebApp。 这允许WebApp正常运行,但不能(偶然或其他方式)看到同一JVM中其他WebApp定义的类。
这种工作方式是每个WebApp都从其自己的ClassLoader加载类,以便(本地)WebApp的实现不会加载与另一个WebApp的实现冲突的类。 对于任何ClassLoader链,要求类空间必须一致。 这意味着您可以Util.class
从虚拟机中的两个单独的ClassLoader加载两个Util.class
,前提是这些ClassLoader彼此不可见。 (这也是使Servlet引擎无需重新启动即可重新部署更改的能力;通过丢弃ClassLoader,您也可以丢弃对其类的引用,使旧版本有资格进行垃圾回收–然后,Servlet引擎可以创建新的ClassLoader,然后在运行时重新加载新版本的类。)
一直向下的模块
构建模块化系统实际上是一种将应用程序划分为(潜在)可重用模块并使它们之间的耦合最小化的方法。 这也是分离模块需求的一种方式。 例如,Eclipse IDE通常具有对GUI和非GUI组件(例如jdt.ui
和jdt.core
)具有独立依赖性的插件。 这允许在IDE环境之外其他使用非GUI模块(无头构建,解析和错误检查等)。
除了单片rt.jar
,任何系统通常都可以分解为各种模块。 问题变成了; 这值得么? 毕竟,从模块化系统开始构建自己的方法要比将整体系统拆解成模块要容易得多。
之所以如此,通常是因为跨模块边界的类泄漏。 例如,从逻辑java.beans
, java.beans
包不应与任何GUI代码具有任何依赖关系。 但是, Beans.instantiate()
使用的java.beans.AppletInitializer
引用了Applet
, Applet
当然对整个AWT链具有连锁依赖关系。 因此,从常识上讲, java.beans
技术上对AWT具有可选的依赖关系。 如果最初采用了更加模块化的方法来构建核心Java库,那么早在API公开之前就已经捕获了此错误。
在某个时候,模块无法进一步细分为子模块。 但是,有时相关功能保留在同一模块中,以简化组织,并且仅在必要时进一步分解。 例如,最初是Eclipse JDT的一部分的重构支持被拉到了自己的模块中,以允许其他语言(如CDT)利用通用重构功能。
外挂程式
许多系统都可以通过插件的概念进行扩展。 在这些情况下,主机系统具有已定义的插件必须符合的API,以及将插件插入其中的方法。许多应用程序(例如Web浏览器,IDE和构建工具)提供了一种通过提供以下内容来自定义应用程序的方法:提供正确API的插件。
有时,这些插件受到限制或执行常规操作(解码音频或视频),但它们本身也可能很复杂(例如,IDE插件)。 有时,这些插件可以提供自己的插件以进一步自定义行为,这可以使系统具有高度可定制性。 (但是,增加间接级别的数量会使系统变得越来越难以理解。)
插件API构成了各个插件必须遵守的合同的一部分。 这些插件本身就是模块,它们会通过封闭系统提供的常规依赖项链和版本控制问题来解决。 随着(特定)插件API的复杂性发展,插件本身也必须(或必须保持向后兼容的行为)。
用于浏览器的Netscape插件API成功的原因之一是其简单性。 只需要少量功能,并且只要主机浏览器使用适当的MIME类型重定向输入,该插件就可以处理其余的内容。 但是,像IDE这样的更复杂的应用程序通常需要紧密集成得多的模块,因此需要更复杂的API来驱动它们。
Java模块化的当前状态
目前,Java中存在许多模块系统和插件基础结构。 IDE通常是众所周知的IDE,IntelliJ,NetBeans和Eclipse都提供自己的插件系统,作为定制体验的方法。 但是,构建系统(Ant,Maven)甚至最终用户应用程序(Lotus Notes,支持Mac AppleScript的应用程序)都具有能够扩展相关应用程序或系统的核心功能的概念。
可以说,Java中最成熟的模块系统是OSGi ,其运行时间几乎与Java一样长,最初出现为JSR 8 ,但最近被接受为JSR 291 。 OSGi在JAR的MANIFEST.MF中定义了其他元数据,以表示每个包所需的依赖关系。 这允许模块(在运行时)检查是否满足其依赖关系,此外,还允许每个模块具有自己的私有类路径(因为每个模块有一个ClassLoader)。 这有助于但不能完全避免前面提到的依赖地狱的概念。 与JDBC一样,OSGi是一个规范(当前为4.2版 ),具有多个开源(和商业)实现。 由于模块不需要依赖任何OSGi特定代码,因此许多开源库现在将其元信息嵌入清单中,以供OSGi运行时使用。 对于那些没有这样做的人,像bnd这样的工具可以对现有的JAR文件进行后处理,并生成合理的默认值。 Eclipse 3.0在2004年从专有的插件系统转换为OSGi ; 许多其他拥有专有内核的系统(JBoss,WebSphere,Weblogic)也纷纷效仿,并将其运行时也基于OSGi内核。
最近,为了对JDK本身进行模块化,创建了Project Jigsaw 。 尽管JDK是JDK的内部部分,并且有可能不被其他SE 7实现所支持,但不能阻止在JDK外部使用Jigsaw。 尽管工作仍在进行中,但拼图仍可能是上述JSR 294的参考实现。 对于最低版本SE 7的要求(加上目前没有Java 7的事实)意味着Jigsaw仍在开发中,并且它通常不适用于在Java 6或更低版本上运行的系统。
为了鼓励采用标准的模块化格式,JSR 294专家组目前正在讨论简单的模块系统建议。 Java库的生产者(例如在Maven存储库中找到的或来自Apache.org之类的库)可以提供Jigsaw和OSGi系统都可以使用的元信息。 结合对Java语言的微小更改(最值得注意的是增加了module
关键字),可以由足够高级的编译器在编译时生成此信息。 然后,运行时系统(例如Jigsaw或OSGi)可以使用此信息来验证已安装模块的集合及其依赖性。
摘要
本文讨论了模块化的一般概念以及如何在Java系统中实现模块化。 由于编译时间和运行时路径可能不同,所以可能有不一致的库要求,从而导致依赖地狱。 但是,插件API允许加载许多类型的代码,这些代码必须遵循主机的依赖项解析,这增加了发生这种不一致的可能性。 为了避免这种情况,像OSGi这样的运行时模块化系统可以提前验证一组要求,以确定应用程序是否可以正确启动,而不是在运行时以静默或不可检测的方式失败。
最后,JSR 294邮件列表的工作正在进行中,以创建Java语言的模块系统,可以在Java语言规范中对其进行完整定义,以允许Java开发人员生成带有编码的依赖项信息的版本化模块,从而可以随后可在任何模块系统中使用。
java 模块化