【Java 模块系统】module-info 模块描述符
• 本文介绍了Java 9新增的模块系统中 module-info 【模块描述符】文件的格式,不涉及对模块系统的完整解析。读者应了解模块基本知识。• 本文核心参考【OpenJDK教程】《模块系统入门》。「壹」 网络上已经有很多完整的Java模块教程了,那为什么要写一篇 只介绍 module-info 部分 的文章呢?
• 本文介绍了Java 9新增的模块系统中 module-info 【模块描述符】文件的格式,不涉及对模块系统的完整解析。读者应了解模块基本知识。
• 本文核心参考【OpenJDK教程】《模块系统入门》
「壹」
网络上已经有很多完整的Java模块教程了,为什么要写一篇 只介绍 module-info 部分 的文章呢?
下面是一个空的module-info.java
文件:
module me.media {
}
它必须放在模块目录的根目录下:.../me.media/module-info.java
其中me.media就是模块根目录
。
假设me.media模块里有一个包 com.me.media.package,包中有一个类Reader,那么它的路径应该是.../me.media/com/me/media/package/Reader.java
。这就是$模块$/包/#类
的目录结构。
如果在这个模块中,我要使用java.desktop
模块下的 某个包/某个类 ,则必须先在模块描述符module-info.java
里导入该模块。
使用requires
关键字导入另一个模块:
module me.media {
requires java.desktop;
}
(这时我们说mcsw.media 依赖于
java.desktop)
然后,我们就可以在Reader.java里放肆地导入java.desktop已公开的包的任何一个类。
package com.me.media.package;
import javax.sound.sampled.spi.AudioFileReader;
public class Reader extends AudioFileReader {
...
}
现在,假设在com.me.media模块里有一个成熟的做好了的包com.me.media.ogg
。我想让它能被另一个模块 me.video
使用,于是在me.video
的.../me.video/module-info.java
里写:
module me.video {
requires me.media;
}
然而报错了:me.game里的类不能导入com.me.media.ogg包的类,报错提示:“com.me.media.ogg没有被导出”!
于是乎,我们知道:
一个类如果没有模块,则编译器会给它分配一个未命名模块
(类似未命名包
),这个类可以肆意导入任何模块任何包导出的public类,,代价是此类本身不能被任何有模块的类访问。
一旦这个类有了模块(有没有包倒无所谓),它要访问某一个模块的包必须满足
(1)模块 依赖(requires) 目标模块
(2)目标模块 导出(exports) 或 开放(opens)目标包
(3)目标类 公开(public)
不满足以上条件,编译时会抛出IllegalAccessError
错误;如果使用反射,运行时抛出IllegalAccessException
异常。
java.base
模块不需要显式依赖(编译器自动依赖),且java.base所有的包都已导出。
所以使用关键字exports
导出包:
module me.media {
requires java.desktop;
exports com.me.media.ogg;
}
我们现在学了两个关键字,大多数教程也就到此为止了。原因大概是:剩下关键字在企业开发中没用(事实上整个模块系统对于企业用处都不大)。
但也有人不是为企业而编程。
「贰」
学习其他关键字之前,先补充一些模块知识:
所有 Java SE 标准API 的模块前缀 java.
,取决于具体jdk(即取决于系统)的模块前缀 jdk.
。
模块的命名规则和包是一样的:即 域名倒置 + 实际名。下面实例中的模块名是不合规范的,仅作例子。
已知 模块B
依赖于模块C、D,而模块A
依赖于B、C、D。应该这么写:
module A {
requires B;
requires C;
requires D;
}
module B {
requires C;
requires D;
}
有点麻烦了。Java规范提供了更方便的写法:关键字transitive
:
module A {
requires B;
}
module B {
requires transitive C;
requires transitive D;
}
transitive
意为“可迁移的”,搭配requires
使用,表示依赖 目标模块 的同时,任何依赖 本模块 的模块,同时自动依赖于 目标模块。“依赖我的,要依赖我所依赖的”。requires transitive
合称「传递依赖」。
Q:如果 A 迁移依赖B,B又迁移依赖A,那么JVM加载模块时,会不会无限循环到死机?
A:你想得到,Java想不到吗?JVM加载模块时如果发现两个模块形成了“环”(loop),就会忽略transitive
修饰符。
对于第三方库来说,另一个有用的关键字是static
。requires static
叫做静态依赖
,表示编译时目标模块必须存在,但运行时不一定存在,JVM解析模块时不会加载静态依赖的模块(即使不存在,也不会报错)。这有什么用呢?
例如模块 A 可以访问 模块 B 和 C,但模块B 适用于Windows系统,模块C适用于Linux系统,用户使用模块A时显然只会搭配B和C中的一个,那就可以这样写:
module A {
requires static B;
requires static C;
}
在编译模块 A 时,Windows和Linux模块 B 、C 都在开发者的电脑里可供编译和调试;用户方运行时,JVM不会强制寻找、加载B和C;用户只要根据需要选择一个模块下载即可,模块 A 运行正常。
requires
有搭配的关键字,exports
也有。
模块之间的访问建立在 依赖关系 的基础上,还可以再套一层保护壳:「定向导出」。
使用关键字to
搭配exports
,可以限定将包导出到某些特定模块,其它模块不可访问:
module A {
exports A.a to B;
exports A.b to B, C; //逗号分隔
}
「叁」
你知道SPI吗?Service Provide Interface
【服务提供接口】是Java一项独门秘笈。Java提供的某些模块服务(比如图片、音频、网络)常常随时代变迁(网络协议、图片/音频格式等等都在不断更新),不可能让Java原生库永远紧跟时代步伐,SPI就是让开发者和第三方可以自己提供这类服务,并且内嵌在原生库中。最常见的步骤是:
- 服务提供者编写一个Java类,继承自标注SPI的类(例如
javax.sound.sampled.spi
包的所有类都是SPI类。SPI都是抽象类,必须重写其抽象方法)。 - 此类重写SPI类的抽象方法,实现了某些服务功能(当然可以有非SPI类作为辅助)。
- 将此类(以及它需要的工具类)编译成字节码class文件,打包成Jar包。
- 在Jar包自带的
META-INF/
目录下创建目录META-INF/services/
。加一个文件,文件名就是所继承的SPI类的全限定类名(比如javax.sound.sampled.spi.AudioFileReader
),不带拓展名。 - 文件里只写实现类的全限定类名(比如
com.mcsw.media.ogg.OggFileReader
)。(用ASCII) - 第三方Jar包可以分发在网络上,需要该服务功能的用户下载Jar包并放在classpath下。
- 从此,用户调用Java原生库的某个SPI方法(API文档会注明调用SPI接口)时,此方法调用了
java.util
包的ServiceLoader
类,这个类负责加载一切SPI提供类(程序员也可以手动调用)。它的方法findFirst() iterator() stream()
都可以发现并通过ClassLoader加载SPI提供类,通过 反射 把这些类的实例对象提供给原生库调用。 - SPI调用、加载、反射的整个过程是对用户隐蔽的,用户一无所知,只能感觉到“我调用了原生库的方法”,不能知道“这个方法加载了我下载的某个SPI类”。
- 提供者也可以写多个提供类,打包在同一Jar包中,在
META-INF/services/
目录里每个提供类都要写在相应的文件里;如果多个提供类继承自同一SPI类,每个提供类的类名都写同一文件里,用换行符分隔。 - 程序员也可以自己创造新SPI接口,用
ServiceLoader
的静态方法`load(Class) load(Class, ClassLoader) 可以得到ServiceLoader的对应实例。这个比较复杂,本篇不提。
以上都是无模块系统下的SPI。ServiceLoader后来添加了一个load(ModuleLayer, Class)
方法以搭配模块系统;但另一方面,模块系统提供了三个关键字来搭配SPI:provides
with
和 uses
。
我们假设,在原生库(当然也可以是你自己的SPI接口)的A模块里有一个SPI类A.spi.AProvider
。那么它的module-info.java应当这样写:
module A {
exports A.spi; // 首先要导出spi包,让SPI类可以被提供者访问
uses A.spi.AProvider; // 关键字 uses + 全限定类名 表示这是一个SPI类
}
在第三方库的模块B里有一个类B.test.BProvider
继承自AProvider
,模块B确认此类为AProvider的提供类(实现类),那么B的module-info.java:
module B {
requires A; // 使用模块A的类,首先导入A
provides A.spi.AProvider // provides + spi类全限定类名
with B.test.BProvider; // with + 提供类全限定类名
}
当然多个提供类继承自同一SPI类也可以,如下:
module B {
requires A;
provides A.spi.AProvider // provides后只能跟一个SPI类
with B.test.BProvider, // 逗号分隔
B.good.Provider;
}
模块也可以自己提供自己的SPI:
module A {
exports A.spi;
uses A.spi.AProvider;
provides A.spi.AProvider
with A.inner.Provider; //自给自足式
}
模块系统的SPI 与 Jar包的SPI 是不冲突的(向前兼容)。而且模块本身也可以被打成Jar包,此时module-info里的provides … with … 和 Jar包META-INF/services里的文件 必须同时存在;两者也同时用于ServiceLoader的查找。请注意,一个Jar包里只能存在一个模块。
「肆」
最后一个关键字是opens
/ open
。类似exports
,都用于导出类,但权限不同。
模块本身可以用open修饰:
open module A { }
一个开放(open)的模块,编译时一切照常。但运行时,任何一个类都可以通过反射API随意访问模块中成员,不设限制。
也可以选择开放某些特定的包:
module A {
opens A.test;
}
开放的包同开放的模块一样,编译时遵守访问限制,运行时可被任何类通过反射访问。类似exports
,opens
也可以配合to
进行「定向开放」。
最后,把Java SE标准的模块关系图奉上:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)