Jade Dungeon

Java模块与Jigsaw

模块化(Module)

模块化就是增加了更高级别的聚合,是Package的封装体。Package是一些类路径名字的约定 ,而模块是一个或多个Package组成的封装体。

  • java9以前 :package => class/interface。
  • java9以后 :module => package => class/interface。

类比的话相当是Maven中项目可以有子项目,子项目之间还有依赖关系。 但是Maven中子项目的依赖是由pom.xml来声明的,独立于jvm体系之外。 所以Java要在模块下加上一个module-info.java文件来声明各个模块中的依赖关系 与访问隔离,编译打包后,就成为一个模块的实体。起到类似于pom.xml的作用。

访问级别也从老的JDK1到JDK8的:

  • public
  • pritected
  • package
  • private

细化到JDK9开始的6级:

  • public to everyone
  • public but only to friend moudles
  • public only within a module
  • protected
  • package
  • private

JDK中自己的模块

那么JDK被拆为了哪些模块呢?打开终端执行java --list-modules查看。

$ java --list-modules
java.base@11.0.2
java.compiler@11.0.2
java.datatransfer@11.0.2
java.desktop@11.0.2
java.instrument@11.0.2
java.logging@11.0.2
java.management@11.0.2
java.management.rmi@11.0.2
java.naming@11.0.2
java.net.http@11.0.2
java.prefs@11.0.2
java.rmi@11.0.2
java.scripting@11.0.2
java.se@11.0.2
java.security.jgss@11.0.2
java.security.sasl@11.0.2
java.smartcardio@11.0.2
java.sql@11.0.2
java.sql.rowset@11.0.2
java.transaction.xa@11.0.2
java.xml@11.0.2
java.xml.crypto@11.0.2
jdk.accessibility@11.0.2
jdk.aot@11.0.2
jdk.attach@11.0.2
jdk.charsets@11.0.2
jdk.compiler@11.0.2
jdk.crypto.cryptoki@11.0.2
jdk.crypto.ec@11.0.2
jdk.dynalink@11.0.2
jdk.editpad@11.0.2
jdk.hotspot.agent@11.0.2
jdk.httpserver@11.0.2
jdk.internal.ed@11.0.2
jdk.internal.jvmstat@11.0.2
jdk.internal.le@11.0.2
jdk.internal.opt@11.0.2
jdk.internal.vm.ci@11.0.2
jdk.internal.vm.compiler@11.0.2
jdk.internal.vm.compiler.management@11.0.2
jdk.jartool@11.0.2
jdk.javadoc@11.0.2
jdk.jcmd@11.0.2
jdk.jconsole@11.0.2
jdk.jdeps@11.0.2
jdk.jdi@11.0.2
jdk.jdwp.agent@11.0.2
jdk.jfr@11.0.2
jdk.jlink@11.0.2
jdk.jshell@11.0.2
jdk.jsobject@11.0.2
jdk.jstatd@11.0.2
jdk.localedata@11.0.2
jdk.management@11.0.2
jdk.management.agent@11.0.2
jdk.management.jfr@11.0.2
jdk.naming.dns@11.0.2
jdk.naming.rmi@11.0.2
jdk.net@11.0.2
jdk.pack@11.0.2
jdk.rmic@11.0.2
jdk.scripting.nashorn@11.0.2
jdk.scripting.nashorn.shell@11.0.2
jdk.sctp@11.0.2
jdk.security.auth@11.0.2
jdk.security.jgss@11.0.2
jdk.unsupported@11.0.2
jdk.unsupported.desktop@11.0.2
jdk.xml.dom@11.0.2
jdk.zipfs@11.0.2

基本模块java.se的依赖关系:

java.base
 ├-> java.compiler ---------------------------------------------┐
 ├-> java.instrument -------------------------------------------┤
 ├-> java.prefs ------------------------------------------------┤
 ├-> java.scripting --------------------------------------------┤
 ├-> java.naming --------------------------┐                    |
 ├-> java.transaction.xa -┐                |-> java.sql.rowset -┤
 ├-> java.logging --------+-> java.sql ----┘                    |
 |            ┌-----------┘                                     |
 ├-> java.xml +-------------> java.xml.crypto ------------------+--> java.se
 |            └-----------┐                                     |
 |                        ├-> java.desktop ---------------------┤
 ├-> java.datatransfer ---┘                                     |
 ├-> java.rmi ------------┐                                     |
 |                        ├-> java.management.rmi --------------┤
 ├-> java.management -----┘                                     |
 ├-> java.net.http ---------------------------------------------┤
 ├-> java.security.jgss ----------------------------------------┤
 `-> java.security.sasl ----------------------------------------┘

大家都知道JRE中有一个超级大的rt.jar(60多M),tools.jar也有几十兆, 以前运行一个hello world也需要上百兆的环境。

让Java SE程序更加容易轻量级部署。强大的封装能力。改进组件间的依赖管理, 引入比jar粒度更大的Module。改进性能和安全性。

例:分模块

其实只要在原来Maven的子项目的源代码根目录加上一个module-info.java, 就可以把项目定义为模块。

Java 可以根据 module descriptor计算出各个模块间的依赖关系, 一旦发现循环依赖,启动就会终止。同时, 由于模块系统不允许不同模块导出相同的包(即 split package,分裂包), 所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。

指定哪些类是对外的,要依赖哪些类等信息:

  • 模块名称
  • 依赖哪些模块
  • 导出模块内的哪些包(允许直接 import 使用)
  • 开放模块内的哪些包(允许通过 Java 反射访问)
  • 提供哪些服务
  • 依赖哪些服务
  • 该模块下的服务提供者和服务消费者信息(SPI)

例:

example-module/
|-> pom.xml
|
|-> example-common/
|   |-> src/main/java/
|   |   |-> study/module/common/base/
|   |   |   `-> SimpleRenderer.java
|   |   |-> study/module/common/helper/
|   |   |   `-> RendererSupport.java
|   |   `-> module-info.java
|   `-> pom.xml
|
`-> example-ui/
    |-> src/main/java/
    |   |-> study/module/ui/item/
    |   |   `-> Component.java
    |   `-> module-info.java
    `-> pom.xml

总项目pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>study.example</groupId>
	<artifactId>study-module</artifactId>
	<version>1.0.0</version>
	<packaging>pom</packaging>

	<modules>
		<module>example-common</module>
		<module>example-ui</module>
	</modules>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.version>1.0.0</project.version>
	</properties>

	<dependencyManagement>
		<dependencies>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>11</source>
					<target>11</target>
					<encoding>${project.build.sourceEncoding}</encoding>
				</configuration>
				<version>3.8.1</version>
			</plugin>
		</plugins>
	</build>

</project>

子项目两个包:

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>study.example</groupId>
		<artifactId>study-module</artifactId>
		<version>1.0.0</version>
	</parent>

	<artifactId>example-common</artifactId>

	<packaging>jar</packaging>

</project>

工具类模块两个包,一个对外一个对内:

对内:

package study.module.common.base;

public class SimpleRenderer {

	public void renderAsString(Object object) {
		System.out.println(object);
	}

}

对外:

package study.module.common.helper;

import study.module.common.base.SimpleRenderer;

public class RendererSupport {

	public void render(Object object) {
		new SimpleRenderer().renderAsString(object);
	}

}

指定这个模块对外的包:

module example.common {
	requires java.base;
	
	exports study.module.common.helper;
}

界面模块会引用工具类的模块:

POM文件指定MAVEN项目的引用:

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>study.example</groupId>
		<artifactId>study-module</artifactId>
		<version>1.0.0</version>
	</parent>

	<artifactId>example-ui</artifactId>
	<packaging>jar</packaging>

	<dependencies>
		<dependency>
			<groupId>study.example</groupId>
			<artifactId>example-common</artifactId>
			<version>${project.version}</version>
		</dependency>
	</dependencies>

</project>

模块的声明文件中指定对工具模块的引用:

module example.ui {
	requires java.base;
	
	requires example.common;
}

具体的类中使用工具模块中的类:

package study.module.ui.item;

import study.module.common.helper.RendererSupport;

public class Component {

	public static void main(String[] args) {
		RendererSupport support = new RendererSupport();
		support.render("test object");
	}

}

查看jar包依赖哪些模块:

$ jdeps --list-deps  example-common/target/example-common-1.0.0.jar
   java.base

模块的语法

module-info.java文件的格式:

[open] module <module> {
	exports <package> [to <module1>[, <module2> ...]];
	opens   <package> [to <module1>[, <module2> ...]];
	
	provides <interface | abstract-class> with <class1>[, <class2> ...];
	uses     <interface | abstract-class>;
	
}

语法详解

  • Java9模块化代码编写的核心类是module-info.java,该类必须在模块的根路径上定义 (例如:maven项目中可以将module-info.java放置在main\src\java下);
  • 当前模块的所有定义都在module-info.java文件中,主要关键字包括: exports, module, open, opens, provides, requires, uses (with, to, transitive)
  • module-info.java中 模块名称必须定义且moduleName必须保证唯一 (模块名可自定义,建议直接用模块包名做模块名),body{}中的内容)可以为空;
  • 当一个工程中有module-info.java时,会被当做一个模块来看待, 访问外部模块中的类时会受外部模块定义的约束限制(例如: 只能访问到其他模块exports的内容,更强的封装性), 当工程中没有module-info.java时,则当成普通的jar访问;
  • 默认情况下,在模块中的public类需要exports才能被外部其他模块访问到, exports的类中用public/protected修饰的嵌套类也可被外部模块访问到;

各关键字使用说明如下:

  • [open] module:声明一个模块,模块名称应全局唯一, 不可重复。加上open关键词表示模块内的所有包都允许通过 Java 反射访问, 模块声明体内不再允许使用opens语句。
  • requires [transitive]:声明模块依赖,一次只能声明一个依赖,如果依赖多个模块, 需要多次声明。加上transitive关键词表示传递依赖, 比如模块A依赖模块B,模块B传递依赖模块C, 那么模块A就会自动依赖模块C,类似于 Maven。
  • exports [to [, ...]]:导出模块内的包(允许直接import使用), 一次导出一个包,如果需要导出多个包,需要多次声明。如果需要定向导出, 可以使用to关键词,后面加上模块列表(逗号分隔)。
  • opens [to [, ...]]:开放模块内的包(允许通过Java反射访问), 一次开放一个包,如果需要开放多个包,需要多次声明。如果需要定向开放, 可以使用to关键词,后面加上模块列表(逗号分隔)。
  • provides with [, ...]:声明模块提供的 Java SPI 服务, 一次可以声明多个服务实现类(逗号分隔)。
  • uses:声明模块依赖的 Java SPI 服务, 加上之后模块内的代码就可以通过ServiceLoader.load(Class) 一次性加载所声明的 SPI 服务的所有实现类。

例子:

/**
 * 声明模块名称:algorithm.api,open:可选项;
 * 当用open修饰module时表示模块中的任何类都可以被外部访问到;
 * all packages in a given module should be accessible at runtime and via
 * reflection to all other modules
 */
[open] module algorithm.api {   

  // 声明当前模块依赖于另一模块java.base
  // java.base是默认requires,不用在module-info.java中明确声明;
  requires java.base;

  // 依赖的传递性,与maven的依赖继承性相似,任何依赖algorithm.api的模块同时也依赖java.desktop;
  // 去掉transitive则必须在依赖模块中明确依赖,即声明requires java.desktop;
  // JavaSE内部module间的依赖都已用transitive修饰,这使得我们对JavaSE module声明依赖更加简洁;
  requires transitive java.desktop;

  // 对java.xml的依赖在编译期必须,运行期非必须,
  // 类似maven中的<scope>provided<scope>,使用Jlink打包的jimage不包含java.xml的文件
  requires static java.xml;

  // 将当前模块指定包中的public类(包含用public/protected修饰的嵌套类)exports(公布),供给外部模块访问;
  // 只exports当前声明的package中的类,子package中的内容不被导出,需另声明;
  exports nl.frisodobber.java9.jigsaw.calculator.algorithm.api;

  // 将包中的类导出给指定的Modules,只能在限定的Module内使用;
  exports nl.frisodobber.java9.jigsaw.calculator.algorithm.api.extension to java.desktop, java.sql, calculator.gui;

   // 引入通过 provides…with 提供的service类,一般 provides…with 的定义在其他模块中
   // 代码中可以通过 ServiceLoader.load(Algorithm.class); 获取可用的service类;
  uses nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm;


   // 通过 provides…with 指令,提供一个实现类;外部模块使用时可用过 uses 引入;
  provides nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm with nl.frisodobber.java9.jigsaw.calculator.algorithm.add.Add;

  // 所有包内的public类(包含public/protected嵌套类)只能在运行中被访问
  // 包内的所有类以及所有类内部成员都可以通过反射访问到
  opens nl.frisodobber.java9.jigsaw.calculator.algorithm.api;

  // 限定访问包的模块
  opens nl.frisodobber.java9.jigsaw.calculator.algorithm.api.scala to java.desktop, java.sql;

  /**
   * By default, a module with runtime reflective access to a package can see the package’s public types
   * (and their nested public and protected types). However, code in other modules can access all types in
   * the exposed package and all members within those types, including private members via setAccessible,
   * as in earlier Java versions.
   */

}

注解

为了让注解能作用于 module 之上,Java 9 特地为 ElementType 扩展了一个 MODULE 类型

/**
 * 一个可以修饰 module 的注解
 */

@Target({ElementType.MODULE})

@Retention(RetentionPolicy.RUNTIME)

@interface ModuleAnnotation {
}

open

用来指定开放模块,开放模块的所有包都是公开的,public的可以直接引用使用, 其他类型可以通过反射得到。

open module module.one {
 //导入日志包
 requires java.logging;
}

将被 open 修饰的模块定义为 开放模块,否则就是 标准模块。

  • 开放模块:其他组件在编译时只能访问该模块明确 exports 的包下的类,但是在运行时可以访问该模块下所有的类
  • 标准模块:其他组件在编译时和运行时只能访问该模块明确 exoprts 的包下的类

opens

opens 用来指定开放的包,其中public类型是可以直接访问的,其他类型可以通过反射得到。

module module.one {
 opens <package>;
}

opens 是只针对于运行时访问能力的一个指令。

opens 后面仍然是指定包名,该包下的类第三方组件是不可以直接引用的 (即无法通过 import 使用),但是却可以通过反射进行调用。

opens 也可以通过 to 关键字指定具体的模块。

需要注意一点就是,如果你的模块是开放模块,就不需要再使用 opens 了,用了也会产生编译错误的。

exports

exports用于指定模块下的哪些包可以被其他模块访问。

module module.one {
 
 exports <package>;
 
 exports <package> to <module1>, <module2>...;
}

有时候我们还想更细粒度的 exports,就可以通过exports...to... 明确指定允许访问的第三方组件, 相当于从群聊变成了私聊。

如下,com.sample下的类只能被模块 B 访问。

module A {
  exports com.sample to B;
}

requires

该关键字声明当前模块与另一个模块的依赖关系。

module module.one {
 requires <package>;
}

requires 后面还可以跟 transitive 或 static 关键字

  • transitive 代表依赖可以被传递(默认依赖是不传递的, 官方也不推荐使用 transitive)
  • static 代表依赖的模块在编译期是必须的,在运行时是可选的 ( 比如 lombok )

下面用一段具体的实例来展示依赖传递

  • A 依赖 B
  • B 依赖 C,并且依赖关系是 transitive,这样的话 A 就隐式依赖了 C
  • C 依赖 lombok,并且依赖关系是 static 的
module A {
  requires B;
}

module B {
  requires transitive C;
  exports B.sample
}

mobule C {
  requires static lombok;
  opens C.sample
}

如果没有使用 transitive,但是模块 A 又需要 模块 C 的话,就必须要再模块 A 中显示的指定依赖关系,否则的话会有编译错误

module A {
  requires B;
  required C;
}

uses、provides … with …

uses语句使用服务接口的名字,当前模块就会发现它,使用java.util.ServiceLoader 类进行加载,必须是本模块中的,不能是其他模块中的.其实现类可以由其他模块提供。

module module.one {
 //对外提供的接口服务 ,下面指定的接口以及提供服务的impl,如果有多个实现类,用用逗号隔开
 uses <接口名>;
 provides <接口名> with <接口实现类>,<接口实现类>;
}

SPI 本质上就是运行时动态的在 class-path 路径下加载对应的实现类的一种技术, 它可以解耦服务消费者和服务提供者

provides A with B可以这样去理解, 为接口A提供实现类B

例:在组件 A 中定义了一个接口Serializer, 并创建了一个工厂SerializerFactory通过 SPI 机制加载接口的实现类

public interface Serializer {
	byte[] serialize(Object target);
}
public class SerializerFactory {

	public static List<Serializer> getSerializers() {
	
		final ServiceLoader<Serializer> loader = 
									ServiceLoader.load(Serializer.class);
		
		return loader.stream()
			.map(ServiceLoader.Provider::get)
			.collect(Collectors.toList());
	}

}

在组件 B 中定义了定义了实现类JdkSerializer,并根据 SPI 的规则创建配置文件 com.sample.Serializer并写入JdkSerializer的全限定名

- resources
	- META-INF
		- services
			com.sample.Serializer

这样就完成了 SPI 的整个配置, 但是在模块化系统中接口和实现的配置全部转到了 module-info.java文件中。

首先在组件 A 中要用uses明确指定Serializer为服务提供者

module A {
  // Serializer 和 SerializerFactory 都在该包下
	exports com.sample;
  uses com.sample.Serializer;
}

如果不适用uses指定的话,SerializerFactory.getSerializers()就会抛出异常

java.util.ServiceConfigurationError: com.sample.SerializerFactory: 
	module A does not declare `uses`

模块 B 依赖模块 A,并为 Serializer 提供实现类,同样需要通过provides ... with 来指定服务的实现类(不再需要创建com.sample.Serializer文件了)

module B {
  requires A;
  provides Serializer with HessianSerializer, JdkSerializer;
}

provides A with B 可以这样去理解, 为接口 A 提供实现类 B。

类加载器

双亲委派模型没有变, 但是ExtClassLoader 更名为 PlatformClassLoader,还有 ApplicationClassLoader 除了可以加载 class-path 的类以外,还支持 module-path 的类路径加载。

老模式

在只有 class-path 的时代,我们用 javac 命令编译时会通过 classpath 参数指定依赖类路径, 使用 java 命令运行时也会通过 cp 参数指定类路径

javac --classpath

java -cp

下面通过一个简单的示例来展示这两个命令在模块化系统中的应用

在项目 module-sample 下,module-A 和 module-B 分别代表两个模块,mods 是用来存放编译输出的目录。

module-sample/module-A/src
module-sample/module-B/src
module-sample/mods

模块 A 的目录结构如下

module-A/src/com/sample/Test.java
module-A/src/module-info.java
pakcage com.sample;

public class Test {
	public void say(String word) {
    	System.out.println(word);
    }  
}
module A {
  exports com.sample;
}

编译模块 A

javac -d mods/A module-a/src/module-info.java module-a/src/com/sample/Test.java

模块 B 的解构如下

module-B/src/com/test/MyTest.java
module-B/src/module-info.java
package com.test;

public class MyTest {
  
  public static void main(String[] args) {
    System.out.println("hello world");
  }
  
}
module B {
  requires A;
}

编译模块 B ,由于当前模块依赖了模块 A,所以需要指定 module-path,从而使得编译期可以找到 模块A。

  • -d参数指定编译文件的输出目录
  • -p指定 module-path
javac -p mods -d mods/B module-b/src/module-info.java module-b/src/com/b/MyTest.java

运行 main 方法

java --module-path mods -m B/com.b.MyTest

Jigsaw

Jigsaw 项目中还有 jlink, jmod,jdeps 等工具,它们都是应用于模块之上的工具。

尤其是 Jlink, 通过该工具你可以对 JDK 进行裁剪,从而构建出拥有最小依赖集合的运行时环境。

如果你的项目中有 automatic-module 的话,是无法通过 jlink 进行裁剪的

项目例子

例子:~/workspace/study/java/maven-java9-jigsaw

fd-java9-jigsaw/
  |-> fd-java9-jigsaw-algorithm-api/
  |     `-> nl.frisodobber.java9.jigsaw.calculator.algorithm.api
  |          `-> Algorithm.java
  |
  |-> fd-java9-jigsaw-algorithm-add/
  |     |-> nl.frisodobber.java9.jigsaw.calculator.algorithm.add
  |     |    `-> Add.java
  |     |
  |     `-> nl.frisodobber.java9.jigsaw.calculator.algorithm.extension
  |          `-> AddTwice.java
  |
  |-> fd-java9-jigsaw-algorithm-substract/
  |     `-> nl.frisodobber.java9.jigsaw.calculator.algorithm.substract
  |          `-> Substract.java
  |
  |-> fd-java9-jigsaw-gui/
  |     `-> nl.frisodobber.java9.jigsaw.calculator.cli
  |          `-> Main.java
  |
  `-> fd-java9-jigsaw-cli/
        `-> nl.frisodobber.java9.jigsaw.calculator.gui
             `-> Main.java

Algorithm.java接口指定运算操作:

package nl.frisodobber.java9.jigsaw.calculator.algorithm.api;

public interface Algorithm {

    Integer calculate(Integer input, Integer input2);

}

开放接口所在的包给调用方和实现方:

module calculator.algorithm.api {

    exports nl.frisodobber.java9.jigsaw.calculator.algorithm.api;

}

Substract实现减法操作:

package nl.frisodobber.java9.jigsaw.calculator.algorithm.substract;

import nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm;

public class Substract implements Algorithm {

    @Override
    public Integer calculate(Integer input, Integer input2) {
        return input - input2;
    }

}

开放接口所在的包给调用方和实现方:

module calculator.algorithm.api {

    exports nl.frisodobber.java9.jigsaw.calculator.algorithm.api;

}

指定为接口Algorithm提供实现类Substract

module calculator.algorithm.substract {

    requires calculator.algorithm.api;

    provides 
			nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm
      with 
			nl.frisodobber.java9.jigsaw.calculator.algorithm.substract.Substract;
 
}

再实现两个加法:

package nl.frisodobber.java9.jigsaw.calculator.algorithm.add;

import nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm;

public class Add implements Algorithm {

    @Override
    public Integer calculate(Integer input, Integer input2) {
        return input + input2;
    }
    
}

package nl.frisodobber.java9.jigsaw.calculator.algorithm.add.extension;

import nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm;

public class AddTwice implements Algorithm {

    @Override
    public Integer calculate(Integer input, Integer input2) {
        return 2 * (input + input2);
    }
 
}

  • 模块中的Add只声明实现了接口,
  • nl.frisodobber.java9.jigsaw.calculator.algorithm.add.extension 包全部开放给外部,其中public类型是可以直接访问的,其他类型可以通过反射得到。
module calculator.algorithm.add {

    requires calculator.algorithm.api;

    opens nl.frisodobber.java9.jigsaw.calculator.algorithm.add.extension;

    provides 
			nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm
      with 
			nl.frisodobber.java9.jigsaw.calculator.algorithm.add.Add;

}

命令行界面nl.frisodobber.java9.jigsaw.calculator.cli.Main, 模块中,requires指定依赖的包;use指定要用的接口:

module calculator.cli {

    requires calculator.algorithm.api;
    
    uses nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm;

}

图形界面nl.frisodobber.java9.jigsaw.calculator.gui.Main, 模块中:

  • exports ... to ...指定可以被些些包访问;
  • requires指定依赖的包。
  • requires transitive代表依赖可以被传递
  • use指定要用的接口:
module calculator.gui {

    exports nl.frisodobber.java9.jigsaw.calculator.gui to javafx.graphics;

    requires calculator.algorithm.add;
    requires calculator.algorithm.substract;
    requires calculator.algorithm.api;
    requires javafx.base;
    requires javafx.graphics;
    requires transitive javafx.controls;

    uses nl.frisodobber.java9.jigsaw.calculator.algorithm.api.Algorithm;

}

编译

Java 9 引入了一系列新的参数用于编译和运行模块, 其中最重要的两个参数是-p-m

  • -p参数指定模块路径,多个模块之间用:(Mac, Linux)或者;(Windows)分隔, 同时适用于javac命令和java命令,用法和Java 8 中的-cp非常类似。
  • -m参数指定待运行的模块主函数,输入格式为模块名/主函数所在的类名, 仅适用于java命令。两个参数的基本用法如下:
javac -p <module_path> <source>
java  -p <module_path> -m <module>/<main_class>

可以通过java -h查看所有与模块化相关的命令;

  • java --list-modules:列出可见模块;
  • java -d <moduleName>orjava --describe-module <moduleName>: 描述模块,moddule-info.java中定义的信息

打包

手动打包:

首先编译源码

> cd maven-java9-jigsaw
> javac
  --module-path fd-java9-jigsaw-algorithm-api/src/main/java
  -d classes/api
  fd-java9-jigsaw-algorithm-api/src/main/java/nl/frisodobber/java9/jigsaw/calculator/algorithm/api/Algorithm.java

其次打包jar文件,需将下述cmd替换上一步打编译的内容

> jar --create
    --file target/jpms-hello-world.jar
    --main-class org.codefx.demo.jpms.HelloModularWorld
    -C target/classes      

自己手动打包方式比较麻烦,实际项目中直接用 maven or gradle. 如果用maven就可以直接:

mvn clean package

执行

执行一个模块 java --module-path <path1>[;<path2>...] -m <moduleName>/<mainClass>

  • --module-path: 执行模块所处的路径,多个路径用;分割;
  • -m--module:指定要执行的模块 & 要执行的main函数所在的类;

例:执行界面:

java --module-path libs -m calculator.gui/nl.frisodobber.java9.jigsaw.calculator.gui.Main

例:执行命令行:

java --module-path libs -m calculator.cli/nl.frisodobber.java9.jigsaw.calculator.cli.Main

jlink

Jlink通过该工具你可以对 JDK 进行裁剪,从而构建出拥有最小依赖集合的运行时环境。

public static HelloWorld {
	
	public static void main(String [] args) {
		System.out.println("Hello, world");
	}

}

编译:

javac HelloWorld.java

创建镜像

创建镜像:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output HelloWorldImage

以上命令会生成一个包含了java.base的运行时镜像,并输出到了目录HelloWorldImage这个目录中。

执行:

cd HelloWorldImage
./bin/java -m HelloWorld

如果要添加更多的模块:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.logging,java.sql --output HelloWorldImage

自定义运行时镜像,通过--launcher定制一个启动器:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output HelloWorldImage --launcher start=java.base/java

启动器

通过启动器运行:

./start -m HelloWorld

压缩运行时镜像

--compress压缩:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output HelloWorldImage --compress 2

参数值2代表用zip算法压缩。

自定义运行时参数

--vm指定运行环境的参数:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output HelloWorldImage --vm "args=-Xmx256m"

配置文件

配置文件jlink.conf

--module-path $JAVA_HOME/jmods
--add-modules java.base
--output HelloWorldImage
--launcher start=java.base/java
--compress 2
--vm "args=-Xmx256m"

运行时:

jlink @jlink.conf

例子

应用到之前的例子中:

cd maven-java9-jigsaw

mvn clean package -DskipTests

jlink --module-path libs
  --add-modules calculator.gui,calculator.cli
  --compress 2 --output jimage

Jmod

// TODO diff between jmod and jar, how to use jmod;

生成jmod文件,将jar转换成jmod文件:

cd libs

jmod create calculator.gui
  --class-path fd-java9-jigsaw-gui-1.0-SNAPSHOT.jar
  --module-version 1.0

Jdeps

依赖对象分析工具,可分析模块相关依赖信息,具体命令可通过jdeps -h查看,简单示例:

cd libs

jdeps --module-path libs -m calculator.gui --list-deps

Maven Plugins

相关的maven插件都在早期,谨慎使用:

  • jlink: https://maven.apache.org/plugins/maven-jlink-plugin/usage.html
  • jmod: https://maven.apache.org/plugins/maven-jmod-plugin/

与老Java的兼容

老的java程序没有模块的概念,为了兼容引入了未命名模块(unnamed module) 和自动模块(automatic module)的概念。

一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块, 取决于这个 jar 文件出现的路径:

  • 如果是类路径(-cp-classpath--class-path), 那么就会转为「未命名模块」:
    • exports所有的未命名模块中的包
    • requires所有模块
    • 如果遇到分裂包(split package),则命名模块优先使用。
  • 如果是模块路径(-p--module-path),那么就转为「自动模块」:
    • exports所有的包
    • requires所有模块
    • 允许访问所有未命名模块的类。

注意,自动模块也属于命名模块。其名称是模块系统基于jar文件名或manifest自动推导得出的。 比如com.foo.bar-1.0.0.jar文件推导得出的自动模块名是com.foo.bar

除此之外,两者还有一个关键区别,分裂包规则适用于自动模块,但对未命名模块无效, 也即多个未命名模块可以导出同一个包,但自动模块不允许。

未命名模块和自动模块存在的意义在于, 无论传入的 jar 文件是否一个合法的模块(包含 module descriptor), Java 内部都可以统一的以模块的方式进行处理, 这也是 Java 9 兼容老版本应用的架构原理。 运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块, 对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。 进一步的解读可以参阅官方白皮书的相关章节。

把没有模块的程序改造为有模块的程序

基于未命名模块和自动模块,相应的就产生了两种老版本应用的迁移策略,或者说模块化策略。

Bottom-up 自底向上策略

第一种策略,叫做自底向上策略,即根据 jar 包依赖关系(如果依赖关系比较复杂, 可以使用 jdeps 工具进行分析),沿着依赖树自底向上对 jar 包进行模块化改造。 一开始应用以传统方式启动。然后,开始自底向上对 jar 包进行模块化改造, 改造完的 jar 包就移到模块路径下,这期间应用仍以传统方式启动。 最后,等所有 jar 包都完成模块化改造,应用改为-m式启动, 这也标志着应用已经迁移为真正的 Java 9 应用。

desc

假设初始时,所有 jar 包都是非模块化的,此时应用运行命令为:

java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar --add-modules mod3,mod4 mod1.EventCenter

对比上一步的命令,首先 mod3.jar 和 mod4.jar 从类路径移到了模块路径, 这个很好理解,因为这两个 jar 包已经改造成了真正的模块。

其次,多了一个额外的参数--add-modules mod3,mod4,这是为什么呢?这就要谈到模块系统的模块发现机制了。

不管是编译时,还是运行时,模块系统首先都要确定一个或者多个根模块(root module) ,然后从这些根模块开始根据模块依赖关系在模块路径中循环找出所有可观察到的模块 (observable module),这些可观察到的模块加上类路径下的 jar 文件最终构成了编译时环境和运行时环境。那么根模块是如何确定的呢?

对于运行时而言,如果应用是通过-m方式启动的,那么根模块就是-m指定的主模块; 如果应用是通过传统方式启动的,那么根模块就是所有的java.*模块即 JRE。 回到前面的例子,如果不加--add-modules参数,那么运行时环境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,没有 mod3、mod4 模块, 就会报java.lang.ClassNotFoundException异常。 如你所想,--add-modules参数的作用就是手动指定额外的根模块, 这样应用就可以正常运行了。

3.接着完成mod2amod2b的模块化改造,此时运行命令为:

java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar --add-modules mod2a,mod2b,mod4 mod1.EventCenter

由于 mod2a、mod2b 都依赖 mod3,所以 mod3 就不用加到--add-modules参数里了。

4.最后完成 mod1 的模块化改造,最终运行命令就简化为:

java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter

注意此时应用是以-m方式启动,并且指定了 mod1 为主模块(也是根模块), 因此所有其他模块根据依赖关系都会被识别为可观察到的模块并加入到运行时环境,应用可以正常运行。

Top-down 自上而下策略

自底向上策略很容易理解,实施路径也很清晰,但它有一个隐含的假设, 即所有 jar 包都是可以模块化的,那如果其中有 jar 包无法进行模块化改造 (比如 jar 包是一个第三方类库),怎么办? 别慌,我们再来看第二种策略,叫做自上而下(top-down)策略。

它的基本思路是,根据 jar 包依赖关系,从主应用开始, 沿着依赖树自上而下分析各个 jar 包模块化改造的可能性,将 jar 包分为两类, 一类是可以改造的,一类是无法改造的。

对于第一类,我们仍然采用自底向上策略进行改造,直至主应用完成改造

对于第二类,需要从一开始就放入模块路径,即转为自动模块。

这里就要谈一下自动模块设计的精妙之处,首先,自动模块会导出所有包, 这样就保证第一类 jar 包可以照常访问自动模块,其次,自动模块依赖所有命名模块, 并且允许访问所有未命名模块的类(这一点很重要,因为除自动模块之外, 其它命名模块是不允许访问未命名模块的类), 这样就保证自动模块自身可以照常访问其他类。

等到主应用完成模块化改造,应用的启动方式就可以改为-m方式。

还是以示例工程为例,假设 mod4 是一个第三方 jar 包,无法进行模块化改造, 那么最终改造完之后,虽然应用运行命令和之前一样还是

java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter

但其中只有 mod1、mod2a、mod2b、mod3 是真正的模块,mod4 未做任何改造, 借由模块系统转为自动模块。

desc

看上去很完美,不过等一下,如果有多个自动模块,并且它们之间存在分裂包呢?

前面提到,自动模块和其它命名模块一样,需要遵循分裂包规则。对于这种情况, 如果模块化改造势在必行,要么忍痛割爱精简依赖只保留其中的一个自动模块, 要么自己动手丰衣足食 Hack 一个版本。当然, 你也可以试试找到这些自动模块的维护者们, 让他们 PK 一下决定谁才是这个分裂包的主人。