|
|
|
|
移动端
创建专栏

初识Java 9模块化编程

本文主要介绍了什么是Java9模块化编程。首先从Java9为什么迟迟不能发布说起,然后引申出什么是模块化编程,接着系统性地介绍模块化编程的系统目标、特点、要求,再通过JDK的模块化案例介绍,让读者能够了解JDK的发展趋势。

作者:周明耀|2018-03-21 21:31

人工智能+区块链的发展趋势及应用调研报告


【51CTO.com原创稿件】本文是Java9系列文章的第一篇,我会通过几篇文章系统性地介绍Java9的新特性。Java9的发布对于Java语言来说是新的开始,希望Java能够一直走下去,因为它是太多程序员赖以为生的编程工具。

Java9

一、模块化问题

我一直认为,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之前就已经支持了。比如封装,类型的封装可以通过使用包和访问修饰符(例如public、protected、private)的组合方式完成。例如protected,只有在同一个包内的类才能访问protected类里面的方法。这里你就可以提出一个问题来了。

如果我们想要让一些包外的类可以访问protected类,又不想让另外一些包外的类可以访问,这时候应该怎么处理呢?Java9之前没有很好的解决方案。对于第二个要求,定义良好的接口,这一点Java语言一直做得不错,从一开始就做得不错。你会发现接口方式在整个模块化编程中扮演了中心角色。

对于显式依赖,由于Java提供的import关键字所引入的jar包需要在编译时才会真正加载,当你把代码打入jar包的时候,你并不知道哪一个jar文件包含了你的jar包需要运行的类型。为了解决这个问题,我们可以利用一些外部工具,例如Maven、OSGi。Java9虽然从jvm核心层和语言层解决了依赖控制问题,但是Maven、OSGi还是有用武之地的,它们可以基于Java模块化编程平台之上继续持续自己的依赖管理工作。

应用程序的jar包关系图

图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。

JDK内部的模型化系统图

图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

  1. module java.prefs{ 
  2.      requires java.xml; 
  3.      exports java.util.prefs; 

代码清单1内包含了requires和exports两个关键字,逐一解释:

  • requires关键字表示了依赖关系,这里明确模块需要依赖java.xml模块,如果没有依赖生命,java.prefs模块在编译时会拒绝执行编译命令。这一点是向Maven借鉴的,使用前必须声明才能使用。
  • exports关键字表示了其他模块如何可以引用java.prefs包,由于模块化编程已经把强封装性设置成了默认选项,因此只有当包被显式地声明导出(就是这里的exported),导出为本例的java.util.prefs包。Exports是针对原有的访问方式(public、protected、private)的一个补充,是针对强一致性的补充,Java9之后,public仅仅是针对模块内部类之间的访问权限,如果你想要从外部能够应用模块内部类,你必须要exports。

注意,模块名由于是全局变量,所以需要是全局唯一的。

5. HelloWorld案例

接下来简单介绍一个HelloWorld示例。如清单2所示,HelloModularWorld类的main函数负责打印字符串“Hello World, new modular World!”。

(2) 清单2 HelloModularWorld类

  1. package org.michael.demo.jpms;   
  2. public class HelloModularWorld {   
  3.     public static void main(String[] args) { 
  4.         System.out.println("Hello World, new modular World!"); 
  5.     }   

为了实现模块化,需要在工程的根目录下创建一个名为module-info.Java的类,内容如清单3所示:

(3) 清单3 module-info.Java源码

  1. module org. michael.demo.jpms_hello_world { 
  2.     // this module only needs types from the base module 'Java.base'; 
  3.     // because every Java module needs 'Java.base', it is not necessary 
  4.     // to explicitly require it - I do it nonetheless for demo purposes 
  5.     requires Java.base; 
  6.     // this export makes little sense for the application, 
  7.     // but once again, I do this for demo purposes 
  8.     exports org.michael.demo.jpms; 

如代码清单3所示,引用了Java.base,输出至org.michael.demo.jpms包。接下来开始编译,如清单4所示。

(4) 清单4 编译模块化系统

  1. $ Javac 
  2.     -d target/classes 
  3.     ${source-files} 
  4. $ jar --create 
  5.     --file target/jpms-hello-world.jar 
  6.     --main-class org.michael.demo.jpms.HelloModularWorld 
  7.     -C target/classes . 
  8. $ Java 
  9.     --module-path target/jpms-hello-world.jar 
  10. --module org. michael.demo.jpms_hello_world 

就这个简单的示例来看,除了增加了一个文件、编译时的差别替换为使用模块路径方式(module path),以及工程没有了manifest文件以外,其他和Java9之前的编程/编译方式是一样。

三、结束语

本文主要介绍了什么是Java9模块化编程。首先从Java9为什么迟迟不能发布说起,然后引申出什么是模块化编程,接着系统性地介绍模块化编程的系统目标、特点、要求,再通过JDK的模块化案例介绍,让读者能够了解JDK的发展趋势。最后,通过一个HelloWorld实例让读者能够深入浅出地了解Java模块化编程。下一篇文章我会介绍模块化对应的服务和模式。

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

【本文为51CTO专栏作者“周明耀”原创稿件,转载请联系原作者】

戳这里,看该作者更多好文

【编辑推荐】

  1. 外媒速递:关于C++中的元编程机制
  2. 深入理解多线程(三)—— Java的对象头
  3. Java开发必须掌握的8种网站攻防技术
  4. Java对象的序列化与反序列化
  5. 你真的以为你了解Java的序列化了吗?
【责任编辑:赵宁宁 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

热门职位+更多