(全文阅读时间大概 20 分钟.)从 JDK9 开始, module 成为与 class,interface,package 等同等重要的一等公民, 是 (需要成为)Javaers 日常高频处理的词汇. Java 的 modularity 起源于 2008 年的 Jigsaw 项目, 并从 2014 年开始在 JDK9 的开发过程中设计并实现. 引入 module 不像引入像 lambada 表达式仅是语法的改变, 它涉及到了 JLS(java language specification),JVM,JDK,JAR, JNI, JVM TI(tool interface) 和 JDWP(javadebug wire protocol,java 调试通信协议)等多个模块的改动. 本文结合一个样例从 JDK 引入 module 机制的原因, 引入 module 涉及的改动点, module 目标, module 与 jar 的关系, module 与 reflection 的关系, automatic module 和 unnamed module 等方面介绍 modularity 相关机制, 希望给想在实际项目中使用 java module 的程序员提供参考.
一. Java 为什么引入 modularity 机制?
1) classpath 问题. Java 的 classloader 采用 parent 委托模式, 即 application 的 classloader 在加载一个 class 时首先委托给 parent 加载器, parent 加载器再向上委托其 parent 加载器, 依此类推, 直到查找到已加载的类或者未查找到, 此时再由 classloader 加载. 但对一些通用框架, 这种机制并不适合, 如 JNDI,JDBC,JAXB 等其接口定义在 Bootstrap classloader 中, 而其 SPI 是由其他厂商实现, 定义在 system classloader(Bootstrap classloader 的子 loader, 即 application classloader)中, parent 无法加载 child 的类, 这就需要打破 parent delegation model; 另外 web 容器为了隔离不同应用的类加载器, 也定制 classloader. 上述各种场景使得类加载, 类依赖关系比较复杂, 例如: 一个 Web 应用依赖的 hibernate 和 spring 都依赖了不同的 log4j, 很容易导致如下所列的一些运行时异常:
NoClassDefFoundError: 编译成功, 运行时异常.
- ClassNotFoundException
- NoSuchMethodError
ClassCastException: 不同的类加载器之间的对象进行类型强转.
2) 安全性问题. jdk 中包括很多 internal 类, 它们虽然设置为 public, 但 oracle 官方一直警告开发人员不要使用; 不过, 警告信息对开发人员来讲等同于说可以使用, 而且这些 API 又能很方便的满足开发需求, 从而导致被广泛地 "滥用". 例如: GC,Unsafe,BASE64Encoder 等.
3)性能问题. jdk 从 1.0 到 8.0, 为了保证兼容性, 基本都是增加 API, 从而导致整个 JDK 有几百 M, 非常的臃肿, 很多 API 实际上很少被用到, 导致程序启动时较慢. 在 cloud native 成为主流的今天, 非常需要一个能够弹性伸缩的 jdk image, 同时也能够满足 IoT 场景下嵌入式设备的使用需求.
二. modularity 涉及到的改动
上文已提到, JDK 的 modularity 机制是一次由底到上的全局性的大升级, 它涉及到 JVM,JDK,tools 等多层次的改动, 尤其是 JDK, 全部的 package 按照 module 重新划分, 移除了 java.xml.ws,java.corba,java.transaction 等包, 如下给出 JVM,JTS,JVM TI 的主要改动点.
1) JVM 改动点:
3.16 Modules: 介绍引入 module 后, module 的描述文件 module-info.java 对包和类的访问控制 (即 module 的 strong encapsulation 特性) 产生的影响.
4.2.3 Module and Package Names:module 命名规范.
4.4.11 The CONSTANT_Module_infoStructure:module 的常量池结构.
4.7.25 The Module Attribute: 类的 module 属性, 表明一个 module 所依赖的其他 modules,export 和 open 的 packages, 使用和提供的 services.
4.7.26 The ModulePackages Attribute: 表明一个 module export 和 open 的 packages.
4.7.27 The ModuleMainClass Attribute:module 主类.
5.3.6 Modules and Layers: 介绍 module 与 layer,classloader 之间的关系.
2) JLS 改动点:
6.5.3 Meaning of Module Names and Package Names:module 命名规范.
7.2 Host Support for Modules and Packages: 应用系统决定如果创建和存储 module 和 package.
7.7 Module Declarations:module 创建语法规范.
13.3 Evolution of Packages and Modules: 提供 package 和 module 访问控制机制.
3) JVM TI 改动点:
增加 "Bytecode Instrumentation of code inmodules",agent 通过 AddModuleReads,AddModuleExports, AddModuleOpens, AddModuleUses 和 AddModuleProvides 等 API 修改 module 的运行时行为.
三. modularity 目标
1) 明确的依赖配置(reliable configuration). 通过配置文件明确 module 之间的依赖关系, 不允许有循环依赖, 以此代替 classpath 记载类机制.
2) 封装增强(strong encapsulation). 增加 module 层的访问控制机制. 引入 module 后, 包和类之间的访问控制由 module 层的访问控制和原有的包和类的访问控制机制两级机制决定.
3) 可变的 JDK 文件大小. jdk 按照模块重新划分为 70 个(jdk11.0.1)modules, 开发者在发布应用时, 根据所需定制自己的 image, 进而带来性能和安全的提升.
四. Jar 与 module 的关系
Module 的 strong encapsulation 由已有类型 (package,class,method) 的访问控制机制和 moduel 的可读性 (readability), 可访问性(accessibility) 共同决定的. 例如: 一个 default 的 class 所在的 package 即使 exports, 也不能被 requires 的 module 访问到. 而如果没有 export 的 package, 即使是 public 的类型, 也不能被其他 module 访问. 见下表:
访问控制符
|
类自身访问
|
包内访问
|
子类访问
|
其他类(exported)
|
其他类(unexported)
|
public
|
Y
|
Y
|
Y
|
Y
|
N
|
private(不能限制 top-level 类)
|
Y
|
N
|
N
|
N
|
N
|
无控制符 (default)
|
Y
|
Y
|
N
|
N
|
N
|
protected(不能限制 top-level 类)
|
Y
|
Y
|
Y
|
N
|
N
|
表 1: 引入 module 后的访问控制机制
exports 后只能跟 package 全限定名, 不允许跟其他类型, 如 class,interface 等; 同时, export 的 package 的 subpackage 如果没有 export, 也不能访问到.
一个 module 可以对应一个 jar 文件, 也可以是 JMOD 文件(JDK 的 module 都封装为该格式, 参见 jdk-11.0.1.jdk/Contents/Home/jmods).jar 和 module 的区别如下:
module 通过 require 关键字声明其依赖的其他 modules, 而 jar 不具备该功能.
module 通过 export 关键字声明其对外暴露的 package, 而 jar 不具备该功能.
module 必须包含文件 module-info.java, 而 jar 不需要.
module 的名字必须与其定义文件 module-info.java 保持一致, 其他 module 通过 module 名字加载该 module.jar 的文件名比较随意, 没有语义上的约束.
总之, jar 是把代码和资源文件打包后的文件格式, 该格式被 JVM 用来加载类和配置文件; 而 module 是 package 的容器, 它有自己的名字, 声明自己所依赖的其他 module 和对外暴露的包名, 它可以是 jar 文件, 也可以是 jmod 文件; 一个 module 对应一个 jar 或 jmod 文件.
样例参见: https://github.com/matoujun/jdkmodularity, 环境: eclipse-2018-09(4.9),JDK11.0.1.
创建 module 工程:
图一 module 源码结构
module 描述文件需要放到与 package 外层平级的目录, 上层目录可以是 src 目录, 也可以是与 module name 相同的目录.
module 的命名可以采用倒序的 DNS, 也可以采用短 module 名, 只要保证工程下名称唯一即可.
打包一个 module:
$JDK11/jar -cfe mods/module1.jarorg.matoujun.module1.ModuleVerify -C out/module1 .
参见 $JDK11/jar -help 获取更多参数说明($JDK11 表示 JDK11 的 bin 目录), 展开的 module 的 jar 目录结构:
图二 module 文件结构
运行 module:
$JDK11/java --module-path ./mods --module module1/org.matoujun.module1.ModuleVerify
因为 main 类已经指定, 也可以这样运行 module:
$JDK11/java --module-path ./mods --module module1
显示 module 的依赖关系:
modules 具有传递性. 例如: module1 的 module-info.java 中, requires transitive java.logging; 使得依赖 module1 的其他 modules 自动依赖 logging. 这种特性又称为 implied readability.
$JDK11/java --show-module-resolution--limit-modules java.logging --module-path ./mods --module module1
show-module-resolution: 输出执行 module1 依赖的 modules;
limit-modules: 限制执行 module1 时依赖的 modules, 或使用 --describe-module.
JDeps: 分析 class 文件获取类依赖关系的一个命令行工具.
Graphviz 是一个图形化工具, 能够把特定的文本文件转为图形文件. Mac xos 下, brewinstall Graphviz
$JDK11/jdeps -R --dot-output dots./mods/module1.jar 生成 dot 文件
dot -Tpng -O dots/summary.dot--dot 文件转为 PNG 图形文件. 如图三所示:
图三 module1 的 modules 依赖
模块化的两个应用场景就是为了云端容器化和 IoT 领域的嵌入式设备, 这两个场景都需要一个最小集 JRE 环境. JDK9 引入 jlink 工具, 开发人员根据需要创建特定的 JRE image 文件.
$JDK11/jlink --module-pathmods/:/usr/local/jdk-11.0.1.jdk/Contents/Home/jmods --add-modules module1--launcher module1Start=module1 --output module1-image
查看 module1-image 目录, 总大小是 37M:
图三: module1 的 image 文件
图四: 运行结果
五. module 与 reflection:
1) 如果一个 package 是 exports 的, 我们可以在其他 modules 中通过反射创建对象, 执行方法, 但不能对非 public 的类型执行 setAccessible(true).
2) 如果一个 package 是 open 的, 我们不可以在其他 modules 直接声明对象, 但可以在其他 modules 中通过反射创建对象, 执行方法, 也可以对非 public 的类型执行 setAccessible(true).open 可以修辞 module, 也可以是 package, 并允许限定范围. 例如: opens org.matoujun.module2.open tomodule1; . 通过 open 关键字, 像 spring 这样的 DI 框架就可以很容易地注入实现类. 通过 uses 和 providers...with,ServiceLoader 可以实现相同的功能.
六. automatic module 与 unnamed module
所有通过 module-info.java 定义的 module 都是 named module, 而在 module path 下的 jar 自动封装为 automatic module, 在 classpath 上的 jar 为 unnamed module. 举例如下:
编译 module1, 其依赖 module2, 依赖 lib 下的 commons-lang-2.6.jar:
$JDK11/javac-d mods --module-source-path src $(find src -name "*.java")--module-path ../org.matoujun.module2/out/:./lib
执行 module1, 把 jar 包直接放在 module-path 下, jar 包被直接加载为 automatic modules,automatic module 的 name 由 jar 的文件名决定, 并去掉文件名中的版本号,"-" 转为 ".":
j11--module-path ./modss:../org.matoujun.module2/out/ -mmodule1/org.matoujun.module1.ModuleVerify
其中 j11 为 $JDK11/java 别名. 参见图五.
图五 automatic module 自动命名
unnamed module
通过 classpath 引用的 jar 包为 unname dmodule,JDK 允许 unnamed module 访问其他 unnamed module 和 named module, 但 named module 不允许访问 unnamed module. 因此如果运行一个 module 工程, 如果直接调用 classpath 上 jar 中的类会抛出 "java.lang.IllegalAccessError" 错误. 要解决这种问题, 只能把 classpath 上的 jar 包转为 automatic module 或者 named module.
总之, modularity 是 java 一次里程碑的重大升级, JDK 自身的 module 化已经证明其是一种稳定, 可靠, 灵活, 安全的技术, 因此在资源允许的情况下架构师和工程师们完全可以把它引入到具体的项目开发中.
来源: https://juejin.im/entry/5bf398d951882516df032894