Java9新特性——module模块系统

官方文档:https://docs.oracle.com/javase/9/index.html

关于 java9的新特性,官方原文:https://docs.oracle.com/javase/9/whatsnew/toc.htm

这玩意就是一个列表,具体的技术细节需要根据官方文档挖一挖。

modular-模块系统

java9的模块化,从一个独立的开源项目而来,名为Jigsaw。

项目官网:http://openjdk.java.net/projects/jigsaw/

为什么要使用模块化

java开发者都知道,使用java开发应用程序都会遇到一个问题,Jar hell,他就像windows里的dll hell。

比如我们启动一个不算大的应用,但一来了很多的jar,如下图:

摘自:Mark Reinhold的演讲 https://www.youtube.com/watch?v=l1s7R85GF1A

这是很常见的。虽然你可以使用 “java -Djava.ext.dirs=lib xxx” 让命令行小一些,但不可否认,他的classpath就是那么长。顺便说一句,java9中不允许使用extdirs了。

另一方面,jdk本身有很多的api:

对于一些小设备,它太庞大了。

helloworld

还是习惯先来一个helloworld。在此之前,需要先检查一下你的java版本:

java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)

如果不是java9,而是 1.8、1.7,那么慢走不送。

创建主类

首先创建一个java类,就叫Demo吧。

文件保存为:src/com/pollyduan/modular/Demo.java

package com.pollyduan.modular;

public class Demo{
    public static void main(String[] args){
        System.out.println("hello modular.");
    }
}

编译:

$ javac -d classes src/**.java
$ tree .
.
├── classes
│   └── com
│       └── pollyduan
│           └── modular
│               └── Demo.class
└── src
    └── com
        └── pollyduan
            └── modular
                └── Demo.java

打包jar并执行

$ mkdir lib

$ jar cvf lib/demo.jar -C classes .

$ java --class-path lib/demo.jar com.pollyduan.modular.Demo

hello modular.

–class-path 开关可以简写:

$ java -cp lib/demo.jar com.pollyduan.modular.Demo

当然我们可以为jar指定主类,来简化运行:

Main-Class: com.pollyduan.modular.Demo

需要在MANIFEST.MF 中增加上面一行,即可直接运行:

$ java -jar lib/demo.jar

创建模块

src/module-info.java

module hello{}

我们写了一个空的模块,命名为hello。

编译模块

$ javac -d classes src/**.java

反编译看一下:

$ javap classes/module-info.class
Compiled from "module-info.java"
module hello {
  requires java.base;
}

为什么我们写了一个空的模块,反编译多了一行?先不用管,后面会说明为什么。

打包模块

$ jar cvf lib/hello.jar -C classes .
$ jar tf lib/hello.jar
META-INF/
META-INF/MANIFEST.MF
module-info.class
com/
com/pollyduan/
com/pollyduan/modular/
com/pollyduan/modular/Demo.class

运行模块

$ java --module-path lib -m hello/com.pollyduan.modular.Demo

hello modular.

这里和传统的执行jar不一样了,这里不需要classpath,而是module-path。

同样命令行可以简写成:

java -p lib -m hello/com.pollyduan.modular.Demo

模块可以增加Main-Class 吗?java9的jar提供了一个create开关,用这种方式打包,可以为module指定主类:

$ jar --create --file lib/lib.jar --main-class com.pollyduan.modular.Demo -C classes .

再次运行模块,命令行就会更简单了。

$ java -p lib -m hello

Jigsaw的设计目标

让开发者构建和维护一个大型的库或应用程序更容易;

提高javaSE平台及JDK实现的安全性和可维护性;

提升应用的性能;

在javase及JDK平台,让应用更小以便于部署于更小的计算单元及紧密的云部署系统。

什么是modules

为了解决这些问题,jdk在package上层,封装了一层。

module -> package -> class/interface

那到底 module 是什么?

module 是一些包的容器。

依赖它的应用称之为模块,模块是有名字的,其他模块使用该名字使用它。

module导出特定的包,仅供依赖它的包使用。

module是一个包的容器。module仅仅需要导出模块依赖的包。

创建一个module

声明一个module

cat module-info.java

module com.foo.bar{
  exports com.foo.bar.alpha;
  exports com.foo.bar.beta;
}

和package-info.java 类似,它也用一个独立的java文件保存,名为 module-info.java。

创建需要导出的类

暂时,类的内容不重要,可以先写一个空类,这里只列出目录结构:

$ tree .
.
├── com
│   └── foo
│       └── bar
│           ├── alpha
│           │   └── Alpha.java
│           └── beta
│               └── Beta.java
└── module-info.java

编译模块

$ javac module-info.java com/foo/bar/alpha/*java com/foo/bar/beta/*java
$ tree .
.
├── com
│   └── foo
│       └── bar
│           ├── alpha
│           │   ├── Alpha.class
│           │   └── Alpha.java
│           └── beta
│               ├── Beta.class
│               └── Beta.java
├── module-info.class
└── module-info.java

打包模块

jar cvf com.foo.bar-1.0.jar .

检查jar结构:

$ jar tf com.foo.bar-1.0.jar
META-INF/
META-INF/MANIFEST.MF
module-info.class
com/
com/foo/
com/foo/bar/
com/foo/bar/alpha/
com/foo/bar/alpha/Alpha.class
com/foo/bar/beta/
com/foo/bar/beta/Beta.class

引用模块

现在我们已经有了模块 com.foo.bar-1.0.jar,那么在定义其他模块,就可以使用requires关键字引用这个模块了。

module com.foo.app{
  requires co.foo.bar;
  requires java.sql;
}

module com.foo.bar{
  requires com.foo.baz;
  exports com.foo.bar.alpha;
  exports com.foo.bar.beta;
}

module com.foo.baz{
  exports com.foo.baz.mumble;
}

內建的module

jdk原生的包被归并到內建的module里,如java.base模块:

module java.base{
  exports java.io;
  exports java.lang;
  exports java.lang.annotation;
  exports java.lang.invoke;
  exports java.lang.module;
  exports java.lang.ref;
  exports java.lang.reflect;
  exports java.lang.math;
  exports java.lang.net;
  //...
}

所有的应用都会默认依赖 java.base,就像以前我们不用显式的 “import java.lang.*;” 一样。

这里验证了前面helloworld中,为什么反编译模块文件之后会多了一个:”requires java.base;”。

下面的 com.foo.app 模块,不需要显式地引入java.base:

如果此时com.foo.bar 增加了 com.foo.baz 模块的引用。

那么,我们知道 com.foo.bar 也隐式 引入了 java.base。

同样的道理,com.foo.baz 模块也隐式引用了 java.base:

可靠的配置

继续深入下去,我们知道 java.sql 引用了其他大量的api,那么下图就不难理解了。

目前的模块结构,称为可读的模块,提供了可靠的配置。

如果引用了不存在的module,和jar一样,你同样会触发 xx not found.

编译时:

运行时:

可访问的类型

如果引用的模块没有导出某个类,那么是不可访问的,这称为强封装。

比如 com.foo.bar 模块中有一个内部类BetaImpl:

那么在 com.foo.bar 模块的主动引用模块 com.foo.app 中如下使用 BeatImpl:

在编译时,会触发异常:

就是说:BetaImpl不可访问,因为包 com.foo.bar.beta.internal 包没有被导出。

同样,即便使用导出版本编辑成功,而运行时引用了未导出版本模块:

查看內建的模块

$ jmod list $JAVA_HOME/jmods/java.base.jmod
classes/module-info.class
classes/apple/security/AppleProvider$1.class
...
classes/java/lang/Object.class
...
bin/java
bin/keytool
...
conf/security/java.policy
...

查看更多内建模块:

$ java --list-modules

java.activation@9
java.base@9
java.compiler@9
java.corba@9
java.datatransfer@9
java.desktop@9
//...节省篇幅略

helloworld进阶

从helloworld的基础上,增加一个模块的依赖。

先来回顾一下helloworld的目录结构:

$ tree module
module
├── classes
│   ├── com
│   │   └── pollyduan
│   │       └── modular
│   │           └── Demo.class
│   └── module-info.class
├── lib
│   ├── demo.jar
│   ├── hello.jar
└── src
    ├── com
    │   └── pollyduan
    │       └── modular
    │           └── Demo.java
    └── module-info.java

增加一个模块service,其中service目录和module目录同级。

$ tree service
service
├── classes
├── lib
└── src
    └── com
        └── pollyduan
            └── service

创建服务类

service/src/com/pollyduan/service/HelloService.java

package com.pollyduan.service;

public class HelloService{
    public void sayHi(String name){
        System.out.println("Hello "+name);
    }
}

声明service模块

service/src/module-info.java

module service{
    exports com.pollyduan.service;
}

编译service模块

$ javac -d service/classes service/src/**java

$ tree service/classes/
service/classes/
├── com
│   └── pollyduan
│       └── service
│           └── HelloService.class
└── module-info.class

打包service模块

jar --create --file service/lib/service.jar -C service/classes/ .

修改helloworld模块

module/src/module-info.java

module hello{
    requires service;
}

修改helloworld主类使用service中的方法

module/src/com/pollyduan/modular/Demo.java

package com.pollyduan.modular;
import com.pollyduan.service.HelloService;

public class Demo{
    public static void main(String[] args){
        new HelloService().sayHi("java9 modular.");
    }
}

重新编译打包helloworld

$ javac -p service/lib -d module/classes module/src/**java

$ jar --create --file module/lib/hello.jar -p service/lib --main-class com.pollyduan.modular.Demo -C module/classes .

$ java -p module/lib:service/lib -m hello
Hello java9 modular.

打完收工。

模块相关的工具

原有的javac/javap等就不说了,这里只列举新增的几个。更多参考:https://docs.oracle.com/javase/9/tools/tools-and-command-reference.htm#JSWOR-GUID-55DE52DF-5774-4AAB-B334-E026FBAE6F34

jlink

模块整理工具,用于将一系列模块聚合、优化,打包到一个自定义的镜像中。这里说的是jre镜像,不是jar。

如果我们只引用了java.base 模块,那么可以打包时可以选择性地打包:

$ jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre

这时输出的jre就是一个完整可用的jre,他和原生jdk的大小相差很大:

$ du -sh $JAVA_HOME jre
493M    /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home
 35M    jre

这样,我们可以把自己的模块也打包进去。

$ mkdir jmods
$ jmod create --class-path service/lib/service.jar jmods/service.jmod
$ jmod create --class-path module/lib/hello.jar jmods/module.jmod
$ jlink -p $JAVA_HOME/jmods:jmods --add-modules java.base,hello --output jre

$ cat jre/release
JAVA_VERSION="9"
MODULES="java.base service hello"

./jre/bin/java --list-modules
hello
java.base@9
service

注意,module-path的值采用classpath同样的分隔符,如windows里的分号和linux里的冒号;而add-modules 开关的值是使用逗号分隔的。

这样,我们打包了一个只有30M的jre,而且,把自己的module也打包进去了。然后呢?直接执行模块看看:

$ ./jre/bin/java -m hello

Hello java9 modular.

jlink还提供了一个launcher开关,可以将我们的模块编译成和java命令一样的可执行文件,放在 jre/bin 中。

$ jlink -p $JAVA_HOME/jmods:jmods --add-modules java.base,hello --launcher Hello=hello --output jre

$ ls jre/bin
Hello   java    keytool

$ ./jre/bin/Hello
Hello java9 modular.

请留意launcher的格式——”[命令]=[模块]”,为了区分,命令使用了首字母大写。

jlink的开关很多,功能不仅于此,如下可以将已经很小的jre继续压缩:

$ jlink -p $JAVA_HOME/jmods:jmods --add-modules java.base,hello --launcher Hello=hello \
--compress 2 --strip-debug \
--output jre_mini

$ du -sh jre*
 35M    jre
 21M    jre_mini

jdeps

这是一个java类文件的依赖分析器。

$ jdeps --module-path service/lib module/lib/hello.jar
hello
 [file:///Users/pollyduan/tmp/java/java9/module/lib/hello.jar]
   requires mandated java.base (@9)
   requires service
hello -> java.base
hello -> service
   com.pollyduan.modular                              -> com.pollyduan.service                              service
   com.pollyduan.modular                              -> java.lang                                          java.base

jmod

用于创建jmod文件,以及查看已存在的jmod文件。

创建jmod文件:

$ jmod create --class-path . com.foo.bar.jmod
$ jmod list com.foo.bar.jmod
classes/module-info.class
classes/.com.foo.bar.jmod.tmp
classes/com/foo/bar/alpha/Alpha.class
classes/com/foo/bar/alpha/Alpha.java
classes/com/foo/bar/beta/Beta.class
classes/com/foo/bar/beta/Beta.java
classes/com.foo.bar-1.0.jar
classes/module-info.java

jdeprscan

这是一个针对jar的静态的分析工具,查找其依赖的api。

$ jdeprscan dom4j-1.6.1.jar
Jar 文件 dom4j-1.6.1.jar:
class org/dom4j/bean/BeanMetaData 使用已过时的方法 java/lang/Integer::<init>(I)V
错误: 找不到类 org/relaxng/datatype/DatatypeException
错误: 找不到类 com/sun/msv/datatype/xsd/XSDatatype
错误: 找不到类 com/sun/msv/datatype/DatabindableDatatype
错误: 找不到类 com/sun/msv/datatype/SerializationContext
错误: 找不到类 com/sun/msv/datatype/xsd/TypeIncubator
错误: 找不到类 com/sun/msv/datatype/xsd/DatatypeFactory
class org/dom4j/io/SAXEventRecorder 使用已过时的方法 java/lang/Integer::<init>(I)V
class org/dom4j/io/SAXHelper 使用已过时的类 org/xml/sax/helpers/XMLReaderFactory
class org/dom4j/io/SAXReader 使用已过时的类 org/xml/sax/helpers/XMLReaderFactory
错误: 找不到类 org/xmlpull/v1/XmlPullParserFactory
错误: 找不到类 org/xmlpull/v1/XmlPullParser
错误: 找不到类 org/gjt/xpp/XmlPullParserFactory
错误: 找不到类 org/gjt/xpp/XmlPullParser
错误: 找不到类 org/jaxen/XPath
错误: 找不到类 org/jaxen/VariableContext
class org/dom4j/tree/NamespaceCache 使用已过时的方法 java/lang/Integer::<init>(I)V
class org/dom4j/tree/NamespaceCache 使用已过时的方法 java/lang/Float::<init>(F)V
错误: 找不到类 org/jaxen/NamespaceContext
错误: 找不到类 org/jaxen/SimpleNamespaceContext
错误: 找不到类 org/jaxen/dom4j/Dom4jXPath
错误: 找不到类 org/jaxen/JaxenException
错误: 找不到类 org/jaxen/pattern/Pattern
错误: 找不到类 org/jaxen/Context
错误: 找不到类 org/jaxen/pattern/PatternParser
错误: 找不到类 org/jaxen/saxpath/SAXPathException
错误: 找不到类 org/jaxen/ContextSupport
错误: 找不到类 org/jaxen/XPathFunctionContext
错误: 找不到类 org/jaxen/SimpleVariableContext
错误: 找不到类 org/jaxen/dom4j/DocumentNavigator
错误: 找不到类 org/gjt/xpp/XmlStartTag

模块小结

关键词

模块定义 module-info.java
模块描述符 module-info.class
modular jar files 模块jar文件
jmod files 模块清单文件
observable modules
readable modules => reliable configuration 可靠配置
accessible types => strong encapsulation 强封装

module和jar的区别

jar 实际上就是一个类文件的集合,就像一个zip文档;而module是一个规范的java组件,除了jar还有更多的工具支持。

jar中的资源可以任意使用;而module中的资源只有导出的才可以使用。

module仍然以jar为载体。

物理层面上,module在一定意义上可以理解为jar中的一个module-info.class。

目录结构的变化,以前一个jar项目是:
project
├── bin
├── classes
└── src
而module项目则是:
project
├── module1
│   ├── classes
│   ├── lib
│   └── src
└── module2
    ├── classes
    ├── lib
    └── src

模块需要注意的问题

module 的依赖,同样存在循环依赖问题,需要注意。如:模块A,requires B; 模块B有 requires A。

IDE是否支持?传统的IDE都是基于classpath管理项目,现在需要支持基于module-path

module打包的jar,你仍然可以当做普通jar来用,没有人阻止你,至少目前是这样的。不过这并不是说module完全没有意义,就像class文件中的成员设置为私有,不允许外部访问,你完全可以通过反射去访问它,一个道理。

模块的应用场景

首先,最突出的用法,就是使用jlink打包自定义的镜像,分发到小计算单元中运行,如docker,嵌入式设备。

其次,将来必定会有越来越多的容器来支持直接运行模块。

然后,他对于应用的热插拔的插件场景中,会有一席之地。

最后,就是代替jar方式运行的模块运行方式。

拭目以待。

时间: 2024-08-31 23:27:22

Java9新特性——module模块系统的相关文章

即将改变软件开发的5个Java9新特性

预计发布的Java 9中,最令人兴奋的特性是什么? 有关Java9的消息最近显得有些沉寂,不要被它迷惑了.JDK开发者正在努力朝着下一个版本迈进,计划2015年12月前完成所有功能开发.之后,它会经历严格测试和bug修复以准备它的全面上市,按计划会在2016年9月发布. 今天我们已经对Java 9中所期待的特性有了一个很清晰的图景.如果Java 8可以被描述为主要是lambdas表达式.streams和API变化的话,那么Java 9就是关于Jigsaw.额外的实用工具和内部的变化.在这篇文章中

Java9的一些新特性介绍_java

被接受的特性1. Jigsaw 项目;模块化源码 Jigsaw项目是为了模块化Java代码.将JRE分成可相互协作的组件,这也是Java 9 众多特色种的一个.JEP是迈向Jigsaw四步中的第一步,它不会改变JRE和JDK的真实结构.JEP是为了模块化JDK源代码,让编译系统能够模块编译并在构建时检查模块边界.这个项目原本是随Java 8发布的,但由于推迟,所以将把它加到Java 9. 一旦它完成,它可能允许根据一个项目需求自定义组件从而减少rt.jar的大小.在JDK 7 和JDK 8的rt

Java 9的14个新特性总结

Java 9 包含了丰富的特性集.虽然Java 9没有新的语言概念,但是有开发者感兴趣的新的API和诊断命令. 我们将快速的,着重的浏览其中的几个新特性:  模块化系统–Jigsaw 项目 模块化是一个很通用的概念.在软件中,模块化可以运用到编写和实现一个程序和计算系统,他们都是作为独立的模块,而不是作为一个单一的和完整的设计. Java 9中主要的变化是已经实现的模块化系统.模块化的引入使得JDK可以在更小的设备中使用.采用模块化系统的应用程序只需要这些应用程序所需的那部分JDK模块,而非是整

Java 8新特性方法引用详细介绍_java

Java 8新特性方法引用 对于引用来说我们一般都是用在对象,而对象引用的特点是:不同的引用对象可以操作同一块内容! Java 8的方法引用定义了四种格式: 引用静态方法     ClassName :: staticMethodName 引用对象方法:  Object:: methodName 引用特定类型方法: ClassName :: methodName 引用构造方法: ClassName  :: new  静态方法引用示例 /** * 静态方法引用 * @param <P> 引用方法

iOS 新特性分列式 之 iOS 7.x - 主要内容:扁平 UI、64位支持、多任务加强、Objective-C 模块支持

iOS 新特性分列式 之 iOS 7.x - 主要内容:扁平 UI.64位支持.多任务加强.Objective-C 模块支持 太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS.Android.Html5.Arduino.pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作. iOS7.1 支持外

Mac OS X Mavericks系统新特性综述

Mac OS X Mavericks系统主要有十大新特性 近日,苹果公司正式发布了全新的桌面操作系统Mac OS X Mavericks,并且完全免费,令人十分惊喜.苹果公司表示,Mac OS X Mavericks拥有更强的性能.更好的电池表现,同时还增强了一些软件功能,如新版Safari浏览器.标签搜索窗口.文件标记和iCloud钥匙串.事实上,这款操作系统还拥有更多的新特性,下面让我们一起来了解一下. 1.分页搜索窗口 在Mac OS X Mavericks中,Finder文件管理器开始支

Mac OS X Mavericks系统的新特性

  苹果公司正式发布了全新的桌面操作系统Mac OS X Mavericks,并且完全免费,令人十分惊喜.苹果公司表示,Mac OS X Mavericks拥有更强的性能.更好的电池表现,同时还增强了一些软件功能,如新版Safari浏览器.标签搜索窗口.文件标记和iCloud钥匙串.事实上,这款操作系统还拥有更多的新特性,下面让我们一起来了解一下. 1.分页搜索窗口 在Mac OS X Mavericks中,Finder文件管理器开始支持分页浏览,就像各种流行的Web浏览器一样,可以打开一个新的

任务调度系统 xxl-job,新版本 V1.4.0 新特性速览

分布式任务调度系统 xxl-job新版本V1.4.0更新内容: 1.任务依赖: 通过事件触发方式实现, 任务执行成功并回调时会主动触发一次子任务的调度; 2.执行器底层实现代码进行重度重构, 优化底层建表脚本; 3.执行器中任务线程分组逻辑优化: 之前根据执行器JobHandler进行线程分组,当多个任务复用Jobhanlder会导致相互阻塞.现改为根据调度中心任务进行任务线程分组,任务与任务执行相互隔离; 4.执行器调度通讯方案优化, 通过Hex + HC实现建议RPC通讯协议, 优化了通讯参

Docker 1.13 新特性 —— Docker系统相关

Docker随着1.13版本的发布,Docker对Daemon的API和命令行做了一系列的改造,除了对容器,镜像等命令做了二级的命令(containers,images)的改造完,还增加了很多的系统或集群的管理的接口和命令,包含system,plugin,secret,分别用于管理Docker Daemon的系统状态,Docker插件的管理,以及集群中的敏感信息的管理. docker system 管理Docker的命令,其中包括 docker system df 显示docker 磁盘使用情况