• 本文介绍了Java 9新增的模块系统中 module-info 【模块描述符】文件的格式,不涉及对模块系统的完整解析。读者应了解模块基本知识。
• 本文核心参考【OpenJDK教程】《模块系统入门》
「壹」
网络上已经有很多完整的Java模块教程了,那为什么要写一篇 只介绍 module-info 部分 的文章呢?
下面是一个空的module-info.java
文件:
module cmcsw.media {
}
如果要想让它起作用,它必须放在模块目录的根目录下:.../mcsw.media/module-info.java
其中mcsw.media就是模块根目录
。mcsw.media模块里有一个包 com.mcsw.media.ogg,包中有一个类OggFileReader,那么它应该放在.../mcsw.media/com/mcsw/media/ogg/OggFileReader.java
。也就是$模块$/包/#类
的目录结构。
很好,如果在这个模块中,我要使用java.desktop
模块下的 某个包/某个类 ,则必须在模块描述符里进行导入,使用require
关键字导入另一个模块:(这时说mcsw.media 依赖于
java.desktop)
module mcsw.media {
requires java.desktop;
}
然后,我们就可以在OggFileReader里放肆地导入java.desktop已公开的包的任何一个类。
package com.mcsw.media.ogg;
import javax.sound.sampled.spi.AudioFileReader;
public class OggFileReader extends AudioFileReader {
...
}
现在,假设在com.mcsw.media模块里有一个成熟完整的包com.mcsw.media.ogg
。我想让它能被另一个模块 mcsw.game
使用,于是我在mcsw.game
的里写:
module mcsw.game {
requires mcsw.media;
}
然而这是一厢情愿:mcsw.game里的类不能导入com.mcsw.media.ogg包的类,报错提示:“com.mcsw.media.ogg没有被导出”!
于是乎,我们知道:一个类如果没有放在模块内,则编译器会给它分配一个未命名模块
(类似未命名包
),这个类可以肆意导入任何模块任何包的public类(代价是此类本身不能被任何包的类访问);一旦这个类有了模块(有没有包倒无所谓),它要访问某一个模块的包必须满足
(1)模块 依赖(requires) 目标模块
(2)目标模块 导出(exports) 或 开放(opens)目标包
(3)目标类 公开(public)
不满足以上条件,编译时会抛出IllegalAccessError
错误;如果用反射,运行时抛出IllegalAccessException
异常。java.base
模块不需要显式依赖(编译器自动依赖),且java.base所有的包都已导出。
在module-info.java使用关键字exports
导出包:
module mcsw.media {
requires java.desktop;
exports com.mcsw.media.ogg;
}
我们现在学了两个关键字,大多数教程也就到此为止了。原因大概是:剩下关键字在企业开发中没用(事实上整个模块系统对于企业用处不大,更何况企业JDK死死拖在Java 8)。
「贰」
在学习其他关键字之前,先补充一下模块知识:所有 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;
}
而我们已知B依赖于C、D,在这里Java规范提供了更方便的写法:新关键字transitive
:
module A {
requires B;
}
module B {
requires transitive C;
requires transitive D;
}
transitive
意为“可迁移的”,搭配requires
使用,表示任何依赖本模块的模块,同时自动依赖于本模块requires transitive
的模块。“需要我的,需要我所需要的”。requires transitive
称「传递依赖」。
Q:如果 A 迁移依赖B,B又迁移依赖A,那么JVM加载模块时,会不会无限循环到死机?
A:你想得到,Java想不到吗?JVM加载模块时如果发现两个模块形成了“环”,就会忽略transitive
修饰符。
对于第三方库来说,一个有用的关键字是static
。requires static
叫做静态依赖
,表示编译时必须存在,但运行时不一定需要,JVM解析模块时不会加载静态依赖的模块(即使不存在,也不报错)。例如模块 A 可以访问 模块 B 和 C,但模块B 适用于Windows系统,模块C适用于Linux系统,用户使用模块A时只会搭配B和C中的一个,就可以这样写:
module A {
requires static B;
requires static C;
}
在编译模块 A 时,模块 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
),文件不带拓展名。文件内容是(ASCII)实现类的全限定类名(比如com.mcsw.media.ogg.OggFileReader
)。 - Jar包可以分发在网络上,需要服务功能的用户下载Jar包并放在classpath某个路径下。
- 用户调用Java原生库的某个SPI方法(API文档注明调用SPI接口)时,此方法调用了
java.util
包的ServiceLoader
类,这个类负责加载一切SPI(程序员也可以手动调用)提供类,它的实例方法findFirst() iterator() stream()
都可以发现并通过ClassLoader加载SPI提供类,通过反射把这些类的实例对象提供给原生库调用。 - 在SPI调用、加载、反射的整个过程中,用户一无所知,用户只能感觉到“我调用了方法”,而不能知道“这个方法加载了我下载的某个SPI提供类”。
- 提供者也可以写多个提供类,打包在同一Jar包中,
META-INF/services/
目录里每个提供类都要写在相应的文件里;如果多个提供类继承自同一SPI类,每个提供类的类名都写在该SPI的文件里,换行符分隔。 - 程序员也可以自己创造SPI类,用
ServiceLoader
的静态方法`load(Class) load(Class, ClassLoader) 可以得到ServiceLoader的对应实例。
以上都是无模块系统的SPI。ServiceLoader后来添加了一个load(ModuleLayer, Class)
方法补充模块系统;但模块系统提供了三个关键字来管理SPI:provides
with
uses
。
让我们假设,在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标准的模块关系图奉上: