51CTO.com 原创稿件本文是 Java9 系列文章的第一篇, 我会通过几篇文章系统性地介绍 Java9 的新特性 Java9 的发布对于 Java 语言来说是新的开始, 希望 Java 能够一直走下去, 因为它是太多程序员赖以为生的编程工具
一模块化问题
我一直认为, Java 这门编程语言已经不再仅仅是一门语言, 它是一个由使用者根据自身工程需要所构建起来的生态环境既然是生态环境, 它必然需要根据外部环境的变化不断调整自己, 不断地吸收外部优良的设计方案, 以用来不断地加强自身, 也必然需要不断地改变扩大自己的范围, 这样也就不再仅仅局限于语言本身
我们学习模块化编程之前, 应该想想为什么 Java 需要引入模块化设计思维? 首先让我们看看自己打包的 jar 文件我们每天在构建的应用程序, 也许大家编码是基于某种框架, 例如 Spring Cloud, 基于 Spring Cloud 可以很方便地启动微服务应用, 但是 Spring Cloud 背后引用了大量的 Java 生态环境里的第三方库
长期来看, 应用程序如果缺乏结构化设计思维, 最终一定会付出代价回到刚才的问题, 模块化编程为什么会出现? 因为它的出现可以让我们更为简便有效地管理库之间的依赖关系, 进而减少了工程的复杂度大家要问了, 不是有 Maven 可以实现这样的功能吗? 对的, Maven 确实可以, 现在 Java 在 JDK 库内设计思维上吸收了 Maven 的设计思维优点, 这样 JDK 内部也有了模块化设计
需要注意的是, 模块化编程并不是一下子出现的, 它会出现首先是基于 Java 本身就是面向抽象编程的, 也就是说, 模块化编程是构建在抽象层之上的相较于 Java8 引入的 lambda 表达式, 模块化编程关注的是整个应用程序的结构扩展问题, 而 lambda 表达式更多的是提供了在一个类里面切换到 lambda 的方式模块化带来的影响涉及到设计编译打包部署等等, 所以我前面讲了, 它不仅仅是一个语言级的特性变化, 它的意义比 lambda 表达式的引入大很多
开始全文前最后一个问题, 为什么 JDK9 一再推迟发布时间? JDK9 的模块化工程为了达到预期目标, 即功能之间的边界清晰目标, 同时又要保持向后兼容模块内定义良好且可读, 这些多重要求导致了 JDK9 的长时间难产这就好比我们编写的应用程序工程, 通过 20 年的积累, 成为了一个巨大无比的工程, 这时候你再想把它按照模块切分, 难度就非常高了
Java 语言已经积累了 20 年, 现在才开始做模块化设计, 其实是有点晚了, 但是一旦做成了这件事情(JDK9 的模块化), 后续的模块化进程就会变得快速起来, 这也就是为什么可能半年后你就会发现 JDK10 发布了
二模块化编程
1. 什么是模块化编程
什么是模块化编程? 模块化编程是将原有的系统分解为若干个自己管理的模块, 但是这些模块之间又是互相通信 (连接) 的模块, 或者可以称之为组件, 也就成了一个个可以识别的独立物件, 它们可以包括代码元数据描述, 以及和其他模块之间的关系等理想地看, 这些物件从编译时期开始就是可以被识别的, 生命周期贯穿整个运行时这样也就可以想象了, 我们的应用程序在运行时应该是由多个模块组成的
作为一个 Java 模块, 必须满足三个基本要求:
(1) 强封装性
对于封装的重要性应该不用和大家解释了, 两个模块之间仅需要知道对方的封装接口参数返回值, 而对于它内部的实现细节, 其他调用方并不关心, 内部怎么变化都没关系, 只要能够继续调用并返回正确的值就行
(2) 定义良好的接口
这里包含两层意思一是模块之间的边界要划分清楚, 不能存在重复的部分, 二是对于无法封装的公开代码, 如果进行了破坏性的修改, 那么对其他调用方来说也是破坏性的, 因此需要提供定义良好并且稳定的接口给其他调用模块调用
(3) 显式依赖
这是点和面的关系每一个点代表一个模块, 两点之间的线代表模块之间的依赖关系, 所有点就组成了模块调用关系图只有拥有清晰的模块调用关系图, 我们才能确保调用关系的正确性和模块配置的可用性 Java9 之前, 我们可以采用 maven 来帮助管理外部依赖关系
模块化带来的是灵活可理解可重用这三大优点模块化编程和当今很多软件架构概念是类同的, 都是为了解决相似的抽象层问题, 例如基于组件的开发面向服务系统架构, 或者更新的微服务架构
前面提到了三个基本要求, 强封装性定义良好的接口显式依赖, 其实在 Java9 之前就已经支持了比如封装, 类型的封装可以通过使用包和访问修饰符 (例如 publicprotectedprivate) 的组合方式完成例如 protected, 只有在同一个包内的类才能访问 protected 类里面的方法这里你就可以提出一个问题来了
如果我们想要让一些包外的类可以访问 protected 类, 又不想让另外一些包外的类可以访问, 这时候应该怎么处理呢? Java9 之前没有很好的解决方案对于第二个要求, 定义良好的接口, 这一点 Java 语言一直做得不错, 从一开始就做得不错你会发现接口方式在整个模块化编程中扮演了中心角色
对于显式依赖, 由于 Java 提供的 import 关键字所引入的 jar 包需要在编译时才会真正加载, 当你把代码打入 jar 包的时候, 你并不知道哪一个 jar 文件包含了你的 jar 包需要运行的类型为了解决这个问题, 我们可以利用一些外部工具, 例如 MavenOSGiJava9 虽然从 jvm 核心层和语言层解决了依赖控制问题, 但是 MavenOSGi 还是有用武之地的, 它们可以基于 Java 模块化编程平台之上继续持续自己的依赖管理工作
图 1: 应用程序的 jar 包关系图
上面这张图包含了两个部分, 一部分是应用程序, 包含 Application.jar 的应用程序 jar 包该 jar 包的两个依赖库(Google Guava 和 Hibernate Validator), 以及三个外部依赖 jar 包我们可以通过 maven 工具完成库之间的依赖关系绑定功能
Java9 出现之前, Java 运行时还需要包含 rt.jar, 如上图所示从这张图上至少可以看出没有强有力的封装概念, 为什么这么说? 以 Guava 库为例, 它内部的一些类是真的需要被 Application.jar 工程使用的, 但是有一些类是不需要被使用的, 但是由于这些类的访问限制符也是 public 的, 所以外部包里的类是可以访问到的, 所以说没有履行 Java9 的封装要求
大家知道, 当 JVM 开始加载类时, 采用的方式是顺序读取 classpath 里面设置的类名并找到需要的类, 一旦找到了正确的类, 检索工作结束, 转入加载类过程那么如果 classpath 里面没有需要的类呢? 那就会抛出运行时错误 (run-time exception) 又由于 JVM 采用的延迟加载方式(lazy loading), 因此极有可能某个用户点了某个按钮, 然后就奔溃了, 这是因为 JVM 不会从一开始就有效地验证 classpath 的完整性那么, 如果 classpath 里面存在重复的类, 会出现什么情况呢? 可能会出现很多莫名其妙的错误, 例如类的方法找不到, 这有可能是因为配置了两个不同版本的 jar 包
2. 模块化系统目标
Java9 的模块化系统有两大目标:
模块化 JDK 本身;
为应用程序的使用提供模块化系统
模块化系统为 Java 语言和运行时环境引入了本地模块化概念, 提供了强有力的封装
如图 1 所示, 在 Java9 之后, 每一个 jar 包都变成了一个模块, 包括引用其他模块的显示依赖从图 1 可以知道, Application 调用了 JDK 的 Java.sql 包
图 2: 应用程序的模块化调用关系
图 3 描述的是 JDK 内部的模型化系统(JSR376 和 JEP261), 已经被合并到了 JDK9
图 3:JDK 内部的模型化系统图
从图 3 大家可以知道, 各个模块之间有着千丝万缕的引用关系, 但是要记住, JDK9 的模块化设计做得很精巧, 它仅仅允许单一方向 (向下) 引用, 不允许出现环形结构, 这样可以确保引用关系图的简单设计原则
3. 模块化的 JDK
在 Java 模块化系统引入之前, JDK 的运行时库包括了重量级的 rt.jar, 该 jar 文件的总大小超过 60M, 包含了大量的运行时类为了重构整个 Java 平台, 也为了 Java 能够在轻量级语言解决方案越来越占主导地位的情况下让 Java 语言继续保持旺盛的生命力, JDK 团队引入了 JDK 的模块化设计, 这个决定可能是关键性的
在过去的 20 年中, JDK 的若干次发布, 每一次都会包含许多新的特性, 因此也增加了大量的类以 CORBA 为例, 它在上世纪 90 年代的时候被认为是企业级计算的未来, 当然现在几乎没有人记得它了, 然而用于支持 CORBA 的类仍然被包含在 rt.jar 包里面, 也就是说, 无论你有没有用到这些类, 只要你的应用程序是分布式的, 你都不得不带着它们一起运行这样做的直接后果是浪费了磁盘空间内存空间, 以及 CPU 资源 (需要增大 CPU 运行耗时) 对于资源受限的硬件载体, 或者云端的资源, 这样就产生了浪费, 也增加了成本(云端资源是按需申请的, 能省就省)
那么我们可不可以直接移除这些不需要的类呢? 不能这么简单执行, 因为我们需要考虑每次发布之后的向前兼容, 直接删除 API 会导致 JDK 升级后一些老的应用程序不可用 JDK 引入模块化管理方式后, 我们只需要忽略包含 CORBA 的模块就可以了
当然, 分解单体型 (monolithic) 的 JDK 并不仅仅是移除过时 (例如 CORBA) 的类 JDK 包含的很多技术都是这样的, 对于一些人有用, 对于另一些人则是无用的, 但是并不是说它们过时了, 仅仅是应用程序不需要使用 Java 语言一直以来就存在安全性漏洞, 通过模块化设计可以减少类的应用, 自然也就降低了漏洞发生的几率
截止目前, JDK9 大约有超过 90 个平台模块, 这种方式取代了以往的单一型大库形态平台模块是 JDK 的一部分, 它和应用程序模块是不一样的, 应用程序模块是由程序员自己创建的但是从技术层面来看, 平台模块和应用程序模块又没有什么区别每一个平台模块构造了一个 JDK 功能, 从日志到 XML 的支持, 等等, 覆盖了原有单一型 JDK 的功能
在 JDK9 里, 所有的模块都需要在外部显示地定义与其他模块之间的依赖关系, 这就好比我们买可拆装家具时的各模块之间的榫头, 你一看就知道需要和哪些其他模块进行拼接, 而一些其他模块都可以拿来公用的模块, 比如 java.logging, 你就会发现很多模块都会应用它也正是由于引入了模块化, JDK 内部终于在各个模块之间有了清晰的界限, 互相的引用关系终于清晰了
注意, 按照 JDK9 目前的模块化设计理念, 所有的依赖关系都是指向向下方向的, 不会出现编译时的各模块间环形依赖情况, 你自己编写的应用程序模块也需要避免这种情况发生
4. 模块资源介绍
一个模块包含模块名称相关的代码和资源, 这些都被保存在称为 module-info.java 的模块描述文件里, 以下面这个文件为例, 描述 java.prefs 平台模块
(1) 清单 1 module-info.java
- module java.prefs{
- requires java.xml;
- exports java.util.prefs;
- }
代码清单 1 内包含了 requires 和 exports 两个关键字, 逐一解释:
requires 关键字表示了依赖关系, 这里明确模块需要依赖 java.xml 模块, 如果没有依赖生命, java.prefs 模块在编译时会拒绝执行编译命令这一点是向 Maven 借鉴的, 使用前必须声明才能使用
exports 关键字表示了其他模块如何可以引用 java.prefs 包, 由于模块化编程已经把强封装性设置成了默认选项, 因此只有当包被显式地声明导出 (就是这里的 exported), 导出为本例的 java.util.prefs 包 Exports 是针对原有的访问方式(publicprotectedprivate) 的一个补充, 是针对强一致性的补充, Java9 之后, public 仅仅是针对模块内部类之间的访问权限, 如果你想要从外部能够应用模块内部类, 你必须要 exports
注意, 模块名由于是全局变量, 所以需要是全局唯一的
5. HelloWorld 案例
接下来简单介绍一个 HelloWorld 示例如清单 2 所示, HelloModularWorld 类的 main 函数负责打印字符串 Hello World, new modular World!
(2) 清单 2 HelloModularWorld 类
- package org.michael.demo.jpms;
- public class HelloModularWorld {
- public static void main(String[] args) {
- System.out.println("Hello World, new modular World!");
- }
- }
为了实现模块化, 需要在工程的根目录下创建一个名为 module-info.Java 的类, 内容如清单 3 所示:
(3) 清单 3 module-info.Java 源码
- module org. michael.demo.jpms_hello_world {
- // this module only needs types from the base module 'Java.base';
- // because every Java module needs 'Java.base', it is not necessary
- // to explicitly require it - I do it nonetheless for demo purposes
- requires Java.base;
- // this export makes little sense for the application,
- // but once again, I do this for demo purposes
- exports org.michael.demo.jpms;
- }
如代码清单 3 所示, 引用了 Java.base, 输出至 org.michael.demo.jpms 包接下来开始编译, 如清单 4 所示
(4) 清单 4 编译模块化系统
- $ Javac
- -d target/classes
- ${source-files}
- $ jar --create
- --file target/jpms-hello-world.jar
- --main-class org.michael.demo.jpms.HelloModularWorld
- -C target/classes .
- $ Java
- --module-path target/jpms-hello-world.jar
- --module org. michael.demo.jpms_hello_world
就这个简单的示例来看, 除了增加了一个文件编译时的差别替换为使用模块路径方式(module path), 以及工程没有了 manifest 文件以外, 其他和 Java9 之前的编程 / 编译方式是一样
三结束语
本文主要介绍了什么是 Java9 模块化编程首先从 Java9 为什么迟迟不能发布说起, 然后引申出什么是模块化编程, 接着系统性地介绍模块化编程的系统目标特点要求, 再通过 JDK 的模块化案例介绍, 让读者能够了解 JDK 的发展趋势最后, 通过一个 HelloWorld 实例让读者能够深入浅出地了解 Java 模块化编程下一篇文章我会介绍模块化对应的服务和模式
来源: http://zhuanlan.51cto.com/art/201803/568615.htm