读后笔记 -- Java核心技术(第11版 卷 II) Chapter9 Java 平台模块系统
9.1 模块的概念
一个平台模块包含:
- 一个包集合;
- 可选地包含资源文件和像本地库这样的其他文件;
- 一个有关模块中可访问的包的列表;
- 一个有关这个模块依赖的所有其他模块的列表;
平台模块系统的优点:
- 1)强封装:可控制可访问的包,无须操心维护不想开放的代码;
- 2)可靠的配置:可避免 类路径中常见的类重复或丢失问题;
模块化的相关文章参照:https://www.cnblogs.com/bruce-he/p/17443594.html
9.2 对模块命名
1)模块名和包名可以完全相同,如 java.sql 模块包含 java.sql、javax.sql 和 javax.transaction.xa 这三个包;
2)模块之间无任何层次关系,如 com.horstmann 和 com.horstmann.corejava 没有关系;
9.3 模块化的 hello, world 程序
// HelloWorld.java public class HelloWorld { public static void main(String[] args) { System.out.println("Hello Modular World!"); } } // module-info.java @SuppressWarnings("module") module v2ch09.hellomod { }
编译:
* 1. method 1: 生成一个新的文件夹 mods 保存相关的 class 文件 * 1.1. switch to path v2ch09.hellomod\src * 1.2. javac -d mods\v2ch09.hellomod module-info.java com\bruce\hello\HelloWorld.java * 1.3. java -p mods -m v2ch09.hellomod/com.bruce.hello.HelloWorld * * 2. method 2: 直接在原相关路径上产生 class 文件 * 2.1 switch to path root path lessonlearn_coreJava * 2.2 run "javac v2ch09.hellomod\src\module-info.java v2ch09.hellomod\src\com\bruce\HelloWorld.java" * 2.3 run "java -p v2ch09.hellomod -m v2ch09.hellomod/com.bruce.hello.HelloWorld" * * Note: Must use "/" in step3, not "\" */
9.4 对模块的需求
结构:
代码:
// HelloWorld.java public class HelloWorld { public static void main(String[] args) { JOptionPane.showMessageDialog(null, "Hello Modular World!"); } } // module-info.java module v2ch09.requiremod { }
编译:
// 编译: // switch to path v2ch09.requiremod\src > javac -d mods\v2ch09.requiremod module-info.java com\bruce\hello\HelloWorld.java // error message: error: package javax.swing is not visible (package javax.swing is declared in module java.desktop, but module v2ch09.requiremod does not read it)
// root cause:
code 需要加载 "java.desktop",如果在 module-info.java 中加上 "requires java.desktop" 编译即可通过。section 9.3 hellomod 的 module-info.java 没有 require 任何内容,是其默认加载了 "reuqires java.base"
Swing 应用程序的模块图:
(通过查看 java.desktop,可以看出 其requires 3个包,并 exports 哪些包可用)
9.5 导出包
module java.xml {
exports javax.xml;
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}
包在模块中被导出(exports)的规则:
- 1)包被导出:public 和 protected 的类和接口,以及 public 和 protected 的成员,在模块外部可以访问(protected 的类型和成员只有在子类中是可访问的);
- 2)没有导出的包:模块外不可访问(这与模块化之前很不相同);
案例:
// Greeter.java package com.horstmann.greet.internal; import com.horstmann.greet.Greeter; public interface Greeter { static Greeter newInstance() { return new com.horstmann.greet.internal.GreeterImpl(); } String greet(String subject); } // GreeterImpl.java package com.horstmann.greet; public class GreeterImpl implements Greeter { @Override public String greet(String subject) { return "Hello, " + subject + "!"; } } // com.horstmann.greet\src\module-info.java module com.horstmann.greet { exports com.horstmann.greet; } // HelloWorld.java package com.horstmann.hello; import com.horstmann.greet.Greeter; public class HelloWorld { public static void main(String[] args) { Greeter greeter = Greeter.newInstance(); System.out.println(greeter.greet("Modular World")); } } // v2ch09.exportedpkg\src\module-info.java,根据提示,需要将 com.horstmann.greet 作为依赖加载 module v2ch09.exportedpkg { requires com.horstmann.greet; }
编译过程:
step1: compile com.horstmann.greet module 1) switch to C:\Users\xxx\IdeaProjects\trunk\lessonlearn_coreJava 2) run "javac -d .\com.horstmann.greet\mods com.horstmann.greet\src\module-info.java \ com.horstmann.greet\src\com\horstmann\greet\Greeter.java \ com.horstmann.greet\src\com\horstmann\greet\internal\GreeterImpl.java" // 此步是指定 com.horstmann.greet 的类文件输出,在 step2 时,它需要从该目录获取类文件结构 step2: compile HelloWorld.java with last module 1) run with "javac -encoding UTF-8 -p com.horstmann.greet\mods v2ch09.exportedpkg\src\module-info.java v2ch09.exportedpkg\src\com\horstmann\hello\HelloWorld.java" step3: run this program with last modules。注意 红色的符合部分 1) run with "java -p v2ch09.exportedpkg;com.horstmann.greet\mods -m v2ch09.exportedpkg/com.horstmann.hello.HelloWorld"
9.6 模块化的 JAR
将两个模块都编译成 jar 包,然后运行
1)按照 https://www.cnblogs.com/bruce-he/p/17219451.html 生成 com.horstmann.greet.jar
2)在 v2ch09.exportedpkg 中,就可以将生成的 com.horstmann.greet.jar 作为依赖
3)按照步骤1)的方式,生成 v2ch09.exportedpkg.jar
4)cmd 下运行下面命令
java -p C:\Users\xxx\IdeaProjects\trunk\lessonlearn_coreJava\out\artifacts\com_horstmann_greet\com.horstmann.greet.jar;C:\Users\xxx\IdeaProjects\trunk\lessonlearn_coreJava\out\artifacts\v2ch09_exportedpkg\v2ch09.exportedpkg.jar -m v2ch09.exportedpkg/com.horstmann.hello.HelloWorld
注意:
-m 后面必须指定包名下面的主函数,否则将报 “v2ch09.exportedpkg 不具有 ModuleMainClass 属性”
9.7 模块和反射式访问
1. 之前非模块结构,访问私有域的方式:
Field f = obj.getClass().getDeclaredField("salary"); f.setAccessible(true); double value = f.getDouble(obj); f.setDouble(obj, value * 1.1);
2. 模块化访问私有域,将有所不同
1)结构:
2)具体代码:
// Country.java package com.bruce.places; public class Country { private String name; private double area; public Country(String name, double area) { this.name = name; this.area = area; } public String getName() { return name; } public double getArea() { return area; } } // Demo.java package com.bruce.places; import com.bruce.util.ObjectAnalyzer; public class Demo { public static void main(String[] args) { var belgium = new Country("Belgium", 30510); var analyzer = new ObjectAnalyzer(); System.out.println(analyzer.toString(belgium)); } } // v2ch09.openpkg\module-info.java
@SuppressWarning("module") module v2ch09.openpkg { requires com.bruce.util; } // com.bruce.util\module-info.java module com.bruce.util { exports com.bruce.util; }
3)编译并运行
javac -d .\com.bruce.util\mods com.bruce.util\src\module-info.java com.bruce.util\src\com\bruce\util\ObjectAnalyzer.java javac -p com.bruce.util\mods v2ch09.openpkg\src\module-info.java v2ch09.openpkg\src\com\bruce\places\*.java java -p v2ch09.openpkg;com.bruce.util\mods -m v2ch09.openpkg/com.bruce.places.Demo
提示错误信息:
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String com.bruce.places.Country.name accessible: module v2ch09.openpkg does not
"opens com.bruce.places" to module com.bruce.util
4)解决方案:
// 修改 v2ch09.openpkg\module-info.java @SuppressWarnings("module") module v2ch09.openpkg { requires com.bruce.util;
opens com.bruce.places; } // 重新编译步骤3)的后2步。opens 放在具体的方法前面,意味着仅该包对外开放,如果放在 module 前,则该模块的所有包都对外开放。
9.8 自动模块
模块化兼容以前的 JAR 包 的方案:
- 1)自动模块;
- 2)不具名模块;
自动模块:模块路径上没有 module-info.class 文件的 JAR。其具有如下属性:
- 1)模块隐式地包含对其他所有模块的 requires 子句;
- 2)其所有包都被导出,且是开放的;
- 3)如果在 JAR 文件清单 META-INF/MANIFEST.MF 中具有键为 Automatic-Module-Name 的项,那么它的值就会变为模块名;
- 4)否则,模块名将从 JAR 文件中获得,将文件名中尾部的版本号删除,并将非字母数字的字符替换为句点;
结构:
用例:
// module-info.java @SuppressWarnings("module") module v2ch09.automod { requires org.apache.commons.csv; } // CSVDemo.java public class CSVDemo { public static void main(String[] args) throws IOException { var in = new FileReader("./v2ch09.automod/countries.csv"); // withDelimiter(';') 必须使用 单引号,并且 throws IOException Iterable<CSVRecord> records = CSVFormat.EXCEL.withDelimiter(';').withHeader().parse(in); for (CSVRecord record : records) { String name = record.get("Name"); double area = Double.parseDouble(record.get("Area")); System.out.println(name + " has area " + area); } } }
编译:
1. 解释: -- 1.1 模块路径:当前 v2ch09.automod\ 为模块路径 -- 1.2 类路径: 当前 v2ch09.automod\src\ 为类路径的一部分 (可以通过 System.out.println("System.getProperty("classpath")"); 打印当前的类路径) 2. 对比课本 section 9.8 自动模块的差异: -- 2.1 去掉 File -> Project Structure\Module\dependencies\ 下面的 jar (这样就去掉了 v2ch09.automod.iml 里面的 指定 jar 路径) -- 2.2 将 commons-csv-1.10.0.jar 放在 v2ch09.automod\ 根目录下面 -- 2.3 以下在 cmd 下面运行(在 IDE 里面看不到区别) -- 2.3.1 > 进入 目录 lessonlearn_corejava> -- 2.3.2 > javac -p v2ch09.automod;commons-csv-1.10.0.jar v2ch09.automod\src\com\bruce\places\CSVDemo.java v2ch09.automod\src\module-info.java -- 2.3.3 > java -p v2ch09.automod;commons-csv-1.10.0.jar -m v2ch09.automod/com.bruce.places.CSVDemo 对比: 将 commons-csv-1.10.0.jar 放在类路径 v2ch09.automod\src 下面时,再运行 2.3.2 将会提示 找不到模块 org.apache.commons.csv 另外: 1. IDE 中,因为不会指定 -p,所以必须通过添加外部依赖的方式。这样做,不论 jar 放在 模块路径或类路径,效果都一样 2. 区分模块路径和类路径的目的:将模块路径下面的第三方 jar 不用放在 编译后形成的 类一个目录下面。如果 .jar 包放在类路径,将和编译后的 .class 放置在一个目录,不容易管理
9.9 不具名模块
模块的几种类型:
- 1)自动模块(section 9.8):JAR 包在模块路径上,但没有 module-info.class 文件;
- 2)不具名模块(section 9.9):任何不在模块路径中的类都是不具名模块的一部分;
- 3)明确模块:JAR 包在模块路径,且有 module-info.class;
不具名模块的特性:
- 1)可以访问所有其他模块的模块;
- 2)所有包都会被导出,并且开放;
9.10 用于迁移的命令行标识
非法的模块访问的权限设置:
- --illegal-access=permit:Java 9 的默认行为,每一种非法访问第一次出现时打印一条消息;
- --illegal-access=warn:Java 11 的默认行为,对每次非法访问都打印一条消息;
- --illegal-access=debug:每次非法访问都打印一条消息和栈轨迹;
- --illegal-access=deny:未来的默认行为,直接拒绝所有非法访问;
案例:一个应用程序使用了不再能继续访问的内部 API,而且不能访问源代码
解决方案:用 --add-exports 标志启动该应用程序,指定希望导出的模块和包,以及将包导出的模块。
java --illegal-access=deny --add-exports java.sql.rowset/com.sun.rowset=ALL_UNNAMED -jar MyApp.jar // 该例中,将其导出到了不具名模块中,这样其他应用程序就可以访问
9.11 传递的需求和静态的需求
1)传递的需求:requires transitive
module javafx.controls { requires transitive javafx.base; ... } // 任何声明需要 javafx.controls 的模块都自动地需要 javafx.base
2)静态的需求:requires static
它声明一个模块必须在编译时出现,而在运行时是可选的。
(对比 requires:编译和运行时都需要的模块)
9.12 限定导出和开放
1)限定导出 (exports ... to):所列的模块可以访问这个包,其他的模块不行
exports com.sun.javafx.collections to javafx.controls, javafx.graphics, javafx.fxml, javafx.swing;
2)限定开放 (opens.. to):包仅对列出的模块开放
module v2ch09.openpkg {
requires com.horstmann.util;
opens com.horstmann.places to com.horstmann.util;
}
exports 和 open 的更详细的用途及区别:详见: https://www.cnblogs.com/bruce-he/p/17445845.html
9.13 服务加载
使用场景:
1). 模块中有一个接口; 2). 有一个或多个模块实现了接口; 需求:对外提供服务
实现方案:
1). 服务提供者 在 module-info.java 中,使用 "exports xxxserviceModule;" "provides xxxservice with xxImpl;" 向外提供服务; 2). 服务消费者 在 module-info.java 中,使用( provides 和 uses 的声明,使得消费该服务的模块允许访问私有实现类 ) "requires xxxserviceModule;" "uses xxxservice;" 3). 服务消费者使用 ServiceLoader 来加载服务类
结构:
代码实现:
// GreeterService package com.horstmann.greetsvc; import java.util.Locale; public interface GreeterService { String greet(String subject); Locale getLocale(); } // FrenchGreeter package com.horstmann.greetsvc.internal; import com.horstmann.greetsvc.GreeterService; import java.util.Locale; public class FrenchGreeter implements GreeterService { @Override public String greet(String subject) { return "Bonjour" + subject; } @Override public Locale getLocale() { return Locale.FRENCH; } } // com.horstmann.greetsvc/src/module-info.java module com.horstmann.greetsvc { exports com.horstmann.greetsvc; provides com.horstmann.greetsvc.GreeterService with com.horstmann.greetsvc.internal.FrenchGreeter; } // HelloWorld.java package com.horstmann.hello; import java.util.*; import com.horstmann.greetsvc.*; public class HelloWorld { public static void main(String[] args) { ServiceLoader<GreeterService> greeterLoader = ServiceLoader.load(GreeterService.class); String desiredLanguage = "fr"; GreeterService chosenGreeter = null; for (GreeterService greeter : greeterLoader) { if (greeter.getLocale().getLanguage().equals(desiredLanguage)) chosenGreeter = greeter; } if (chosenGreeter == null) System.out.println("No suitable greeter."); else System.out.println(chosenGreeter.greet("Modular World")); } } // v2ch09.useservice\src\module-info.java @SuppressWarnings("module") module v2ch09.useservice { requires com.horstmann.greetsvc; uses com.horstmann.greetsvc.GreeterService; }
编译运行:
javac -d com.horstmann.greetsvc\mods com.horstmann.greetsvc\src\module-info.java com.horstmann.greetsvc\src\com\horstmann\greetsvc\GreeterService.java \
com.horstmann.greetsvc\src\com\horstmann\greetsvc\internal\*.java javac -p com.horstmann.greetsvc\mods v2ch09.useservice\src\com\horstmann\hello\HelloWorld.java v2ch09.useservice\src\module-info.java java -p com.horstmann.greetsvc\mods;v2ch09.useservice -m v2ch09.useservice/com.horstmann.hello.HelloWorld
9.14 操作模块的工具
1. jdeps 工具可以分析给定的 JAR 文件集之间的依赖关系。
jdeps -s junit-4.13.jar hamcrest-core-1.3.jar
// --generate-module-info :对每个分析过的模块产生 module-info 文件 jdeps --generate-module-info d:\temp\junit junit-4.13.jar hamcrest-core-1.3.jar
2. jlink 工具可以产生执行时无须单独的 Java 运行时环境的应用程序。
> jlink --module-path com.horstmann.greet.jar;v2ch09s.exportedpkg.jar;$JAVA_HOME/jmods --add-modules v2ch09s.exportedpkg --output e:\temp\hello
3. JMOD 文件
与 JAR 文件不同,JMOD 文件(..\jdk-17\jmods)只有在链接时才有用,即只有在产生运行时镜像时才有用。