Java 9 内部探索——版本架构,多版本 jar 包及其他

pijp9091 8年前
   <h2><strong>JShell</strong></h2>    <p>因为已经有很多人在谈论 Jigsaw,因此在第一部分我们先跳过不去讲它。在这一部分我们将会照本宣科地拿 JShell 做些事情, 这是 Java 的一个全新的 REPL (说到它能做的事情,例如你在一个地方敲入了 Java 代码,有了它就可以马上把代码的运行结果计算出来)。如果你还并不(特别地)了解这个东西但又感觉有点兴趣的话,可以看看 Robert Field 在 去年的 Devoxx Belgium 上提供的 这份不错的介绍 。</p>    <h2><strong>新的版本字符串</strong></h2>    <p>让我们先来个简单的入门介绍: 版本名称。</p>    <p>我尝试过去理解 Java的版本命名模式 ,直到这样做的时候才觉得值得去深究一番。它是从 1.0 和 1.1 版本的 JDK 开始的 – 这俩版本还算是那么回事儿,但这俩版本以后就越来越不那么像话了。版本 1.2 到 1.5 对商标进行了重命名,如 Java 2 这样的,改变比较明显(还记得J2SE 吗 ? 其实指的就是 2 这个版本) 。到了  JDK 1.5 就很明显可以看出上述的命令模式没有真正起作用,因此 Sun就开始将它叫做 Java 5 了。围绕 Java 6,整个自 Java 2 开始的命名创意被悄无声息的埋没了,不过这样反而更让人明白——我们简单的叫它“Java X”就可以了。 (你是否知道 Java 版本,包含 Java 7 其实都有一个像  Tiger和Mustang这样 很酷的工程名字?)</p>    <p>JVM 所报告的版本字符串并没有做出修改——它们总会是 1.x.... 这样的形式,不过现在有了 JEP 223 , 版本字串和命令模式做了对齐。如果检查相关的系统属性(见这里的 demo ), 输出会是下面这样的内容:</p>    <pre>  <code class="language-java">java.version: 9-ea  java.runtime.version: 9-ea+138-jigsaw-nightly-h5561-20161003  java.vm.version: 9-ea+138-jigsaw-nightly-h5561-20161003  java.specification.version: 9  java.vm.specification.version: 9</code></pre>    <p>这并非过分显示的信息,因为它是跑在一个早期可访问的构建本上的。在将来 java.version 会报告像 9.1.2 这样的字符串, 所遵循的是 $MAJOR.$MINOR.$SECURITY 模式:</p>    <ul>     <li> <p>$MAJOR 所标识的是 Oracle 计划每两到三年发布的主版本号。</p> </li>     <li> <p>$MINOR 所标识的是针对Bug修复以及一些其它需要定期跟进的更小一点的版本号——当主版本号发布时,这个标识就会被重置为零。</p> </li>     <li> <p>$SECURITY 则相当有意思 – 这个标识在每次发布中“含有包括为提升安全性而进行的重要修复”时就会要用到,并且在 $MINOR 变大的时候它并不会被 <strong>重置。</strong></p> </li>    </ul>    <p>现在我们已经没必要对这些字符串进行转换了,因为有了 Version , 这是一个能为我们做这些事情的小类,很不错。</p>    <pre>  <code class="language-java">Version version = Runtime.version();  System.out.println("Reported by runtime: " + version);  switch (version.major()) {      case 9:          System.out.println("Modularity!");          break;      case 10:          System.out.println("Value Types!");          break;</code></pre>    <h2><strong>GNU 风格的命令行选项</strong></h2>    <p>当涉及到命令选项的语法时,Java 的各种工具就有点太过于形式各异了:</p>    <ul>     <li> <p>有些会在 长版本选项前面 使用一个短横线(-classpath),其它则会使用两个(--gzip)</p> </li>     <li> <p>有些用短横线分隔单词(--no-gzip),其它则没有这样做(-classpath)</p> </li>     <li> <p>有些是单个字母形式的(-d), 其它则是两个字母形式的(-cp, 真不知道那样搞是为了啥?!)</p> </li>     <li> <p>有些用等号赋值(-D<name>=<value>), 其它则需要一个空格(我可不会这样做…)</p> </li>    </ul>    <p>相比之下,在 Linux 以及其它基于 GNU 的系统上几乎所有工具的选项都使用的是相同的语法:</p>    <ul>     <li> <p>长版本选项前面都是使用两个短横线</p> </li>     <li> <p>单词都用短横线分隔</p> </li>     <li> <p>选项缩写都会使用一个短横线并且只会包含一个字母</p> </li>    </ul>    <p>在一次不顾一切的鲁莽行动中,Java 9 快刀斩乱麻,修改了所有的命令行选项以匹配上述这些规则!好吧,这个只是跟你开个玩笑… 不过 JEP 293 已经确定了一个准则来反映和适配这些规则,并且新的选项预期也会遵循这个准则。老的选项可能会在某些时候向这种更加干净的语法迁移,但那并不是 Java 9 开发工作的一部分。JEP 包含了许多详细内容以及示例 – 值得一读。</p>    <h2><strong>扩展和更新</strong></h2>    <p>有很多 JEP(Java Expression Parser,Java表达式分析器)会随 Java 9 发布,它们会带来对已存在功能的提高扩展和更新。下面介绍它们,以主题为序,有些总结,有些细节。</p>    <h3><strong>Unicode</strong></h3>    <p>Java 自身可以用 UTF-16 (你的代码里可以用 emoji)[译者注:emoji 是 Unicode 的表情符号]来写代码,属性文件则必须使用 ISO-8859-1。来看看,如果有一个像这样的 config.properties 文件:</p>    <pre>  <code class="language-java">money = € / \u20AC</code></pre>    <p>在 Java 8 中访问这个文件:</p>    <pre>  <code class="language-java">money = â▯¬ / €</code></pre>    <p>JEP 226 终结了那个时代,不再需要进行  Unicode 转义。Java 9 中同样的代码访问这个文件会得到我们期望的结果:</p>    <pre>  <code class="language-java">money = € / €</code></pre>    <p>(有一个 完整示例 ,不过我们的代码高亮不太好用。)</p>    <p>值得注意的是,我们有很多种方法用于访问属性文件,但是只有通过 PropertyResourceBundle 访问的这种方法被更新了。 JavaDoc 中的 API 文档说明了如何精确的检测编码以及如何对其进行配置。默认配置是明智的,虽然它只是让 API 在一般情况下  “可以工作”:</p>    <pre>  <code class="language-java">try (InputStream propertyFile = new FileInputStream("config.properties")) {      PropertyResourceBundle properties = new PropertyResourceBundle(propertyFile);      properties.getKeys().asIterator().forEachRemaining(key -> {          String value = properties.getString(key);          System.out.println(key + " = " + value);      });} catch (IOException e) {      e.printStackTrace();}</code></pre>    <p>在 示例 中你可以找到使用了 Properties API 的代码。如果你想在 Java 8 中运行它来进行比较,你会发现 Java 9 的 API 中有一个漂亮的小修改。这个小修改是为仍然在使用古老的 Enumeration 的可怜开发者而做。</p>    <p>在其它 Unicode 相关的新闻里,Java 9 支付了 <strong>Unicode 8.0</strong> 。耶!</p>    <h3><strong>图形图像</strong></h3>    <p>图像 I/O 框架(在 javax . imageio 包中) 现在支持<strong>TIFF</strong> 了。Java 高级图像处理(Java Advanced Imaging,JAI)项目 实现了对 TIFF 的读写。它已经完成标准化并移到javax.imageio.plugins.tiff 包中。</p>    <p>视网膜 <strong>HiDPI</strong> 屏幕为桌面 UI 带来特有的挑战。Java已经在 Mac 上处理好了这个问题,现在紧跟着开始适配 Linux 和 Windows。“窗口和 GUI 组件会基于平台推荐而拥有一个合适的大小,在任何 HiDPI 设置下,默认的绽放都应该让文本应保持高清,图标和图像在合适的显示密度上应该光滑并尽可能地展现细节。”  </p>    <p>Linux 上 Java 桌面的三剑客(AWT、Swing 和 JavaFX)现在可以使用 <strong>GTK 3</strong> 。一开始JVM 会默认使用 GTK 2,只能通过 jdk.gtk.version 来配置使用  GTK 3。“交互性”需要 GTK 3,这个需求很早就能被探测到”。</p>    <p>JavaFX 使用过时的 <strong>GStreamer</strong> 版本,目前更新到了 1.4.4。新版本会提供 JavaFX 在播放媒体时的稳定性和性能。</p>    <p><strong>HarfBuzz</strong> 是新的  OpenType 布局引擎,取代了已被停用的  ICU 。</p>    <h3><strong>安全性</strong></h3>    <p>SHA-3哈希算法已经实现了 SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。可以通过  MessageDigestAPI 使用它们。</p>    <p>在使用 SecureRandom(在任何 Java 版本中)时,你可以获取操作系统的原生实现,或者 纯 Java 实现的版本 。后者是“旧的基于 SHA-1 的 RNG 实现,它的健壮性不如被认可的   <strong>DRBG</strong>  [Deterministic Random Bit Generator] 机制。 ”既然是旧的,特别是嵌入式系统,它依赖于 Java 变化,它的安全性随  NIST 800-90Ar1 所描述的 DRBG 机制得到了提升。SecureRandom API 已经被改进,可以传递参数以使用 DRBG 和将来的算法。 </p>    <pre>  <code class="language-java">Instantiation instantiation = DrbgParameters.instantiation(128, RESEED_ONLY, null);  SecureRandom random = SecureRandom.getInstance("DRBG", instantiation);    byte[] bytes = new byte[20];  random.nextBytes(bytes);</code></pre>    <h2><strong>新的 Java 虚拟机特性</strong></h2>    <p>机器可以代替我们工作——我想知道是否有一天会实现?它会获得一些新的特性,并被人们所喜爱,但一旦它在我们生活中出现,我们可能又要花上好几年去适应它。如果不能实现的话,我现在先介绍另一个“新机器”—— insect  机器。</p>    <h3><strong>包含多个发行版的 JAR 包</strong></h3>    <p>可能你有时候会想为不同的 Java 运行版本写代码——在 Java 8 上做 <em>一些事情</em> ,在 Java 9 上做 <em>另外一些事情</em> 。到现在为止,这处理起来都很棘手,不过 Java 9 解决了这个问题最重要的部分。现在所有不同的 Java 平台都能创建,也能认识包多个发行版的 JAR 包,它会包含同一类型的不同版本,这些版本对应不同的 Java 版本。</p>    <p>来看个示例:</p>    <ol>     <li> <p>先创建三个类:Main、VersionDependent用于 Java8,还有 VersionDependentfor 用于 Java 9。后续的代码提供一个用于打印的结果,内容是 “Java X version”,其中 <em>X</em> 是8 或 9。</p> </li>     <li> <p>然后编译 Main 和 VersionDependent(用于 Java 8 的),结果放在目录 out-mr/java8 上;编译VersionDependent(Java 9 的版本) 放在 out-mr/java9 上。</p> </li>     <li> <p>现在开始有趣了,来看看怎么打包。下面的命令会创建一个 mr.jar,包含两个 VersionDependent.class (分别来源于 out-mr/java-x 目录)。因为结构明确,所以 java 能选择正确的类:</p> <pre>  <code class="language-java">jar9 --create --file out-mr/mr.jar -C out-mr/java-8 . \    --release 9 -C out-mr/java-9 .</code></pre> </li>     <li> <p>确实,使用 Java 8 运行 java -cp out-mr/mr.jar ...Main 会输出 “Java 8 version” 而 使用 Java 9 运行会输出 “Java 9 version”。</p> </li>    </ol>    <p>JAR 内部结构看起来像这样:</p>    <pre>  <code class="language-java">└ org      └ codefx ... (moar folders)          ├ Main.class          └ VersionDependent.class  └ META-INF      └ versions          └ 9              └ org                  └ codefx ... (moar folders)                      └ VersionDependent.class</code></pre>    <p>Java 8 或更早的版本会在直接使用 org 下面的类,但新版本会使在 META-INFO/versions 子目录中去找合适的内容来代替默认的。干净利落。</p>    <h3><strong>统一的日志</strong></h3>    <p>调试 JVM,可能要找到应用崩溃或者性能低下的原因,这已经够复杂了。而不同的日志系统有着完全不同的选项,这并不能让事件变得简单。幸好有即将通过的 JEP <a href="/misc/goto?guid=4958856738920306786" rel="nofollow,noindex">158</a> 和<a href="/misc/goto?guid=4959725654861659911" rel="nofollow,noindex">271</a> ,它带来了新的命令行参数 -Xlog(现在不应该是 --log?)可以用来定义极尽详细的日志级别。这里有一些可用的设置:</p>    <ul>     <li> <p>每个消息可以有大量的标签,这依赖于创建和使用它的子系统,如 aregc、模块、oros等。可以选中单独的标签并对其应用其它设置。</p> </li>     <li> <p>当然消息有等级 (error,warning,info,debug,trace,develop) 而且可以按等级进行过滤。</p> </li>     <li> <p>可以为日志文件转换设置输出到 stdout、stderr 或者文件。</p> </li>     <li> <p>然后还有 <em>装饰</em> – 附加在消息上的有用信息(pid、uptime、…、技术标签和等级都是装饰)。它们都可以被输出。</p> </li>    </ul>    <p>这一些都可以由单独的 -Xlog 选项完成。让我们从简单的记录几个标签开始:</p>    <pre>  <code class="language-java">java9 -cp out-mr/mr.jar -Xlog:os,modules,gc ...Main    [0.002s][info][os] SafePoint Polling address: 0x00007feea4c96000  [0.002s][info][os] Memory Serialize Page address: 0x00007feea4c94000  [0.002s][info][os] HotSpot is running with glibc 2.22, NPTL 2.22  [0.009s][info][gc] Using G1  Java 9 version</code></pre>    <p>咦,难道没有模块信息?让我们把那个标签改为 debug(如果没有指定等级,默认是 info):</p>    <pre>  <code class="language-java">java9 -cp out-mr/mr.jar -Xlog:os,modules=debug,gc org.codefx.demo.java9.internal.multi_release.Main    [0.002s][info][os] SafePoint Polling address: 0x00007f3054a22000  [0.002s][info][os] Memory Serialize Page address: 0x00007f3054a20000  [0.002s][info][os] HotSpot is running with glibc 2.22, NPTL 2.22  [0.009s][info][gc] Using G1  [0.059s][debug][modules] set_bootloader_unnamed_module(): recording unnamed module for boot loader  [0.063s][debug][modules] define_javabase_module(): Definition of module: java.base, version: 9-ea, location: jrt:/java.base, package #: 159  [... snip ... many, many more module messages ... ]</code></pre>    <p>太多了,不过我们可以看到这样:</p>    <pre>  <code class="language-java">[0.079s][info][modules,startuptime] Phase2 initialization, 0.0366552 secs</code></pre>    <p>嘿,这是 info!为什么之前它没有显示出来?!Hey, that’sinfo! Why did it not show up before?! 令人惊异的是它有两个标签,在命令中只匹配其中 <em>一个</em> 标签是不够的——必须匹配 <em>所有</em> 标签。我们可以通过 modules+startuptime 或者使用通配符来扩展匹配:</p>    <pre>  <code class="language-java">java9 -cp out-mr/mr.jar -Xlog:os,modules*,gc* ...Main    [0.002s][info][os] SafePoint Polling address: 0x00007f9c7f307000  [0.002s][info][os] Memory Serialize Page address: 0x00007f9c7f305000  [0.003s][info][os] HotSpot is running with glibc 2.22, NPTL 2.22  [0.007s][info][gc,heap] Heap region size: 1M  [0.009s][info][gc     ] Using G1  [0.009s][info][gc,heap,coops] Heap address: 0x00000006c6200000, size: 3998 MB, Compressed Oops mode: Zero based, Oop shift amount: 3  [0.077s][info][modules,startuptime] Phase2 initialization, 0.0367418 secs  Java 9 version  [0.090s][info][gc,heap,exit       ] Heap  [0.090s][info][gc,heap,exit       ]  garbage-first heap   total 256000K, used 2048K [0x00000006c6200000, 0x00000006c63007d0, 0x00000007c0000000)  [0.090s][info][gc,heap,exit       ]   region size 1024K, 3 young (3072K), 0 survivors (0K)  [0.090s][info][gc,heap,exit       ]  Metaspace       used 4225K, capacity 4532K, committed 4864K, reserved 1056768K  [0.090s][info][gc,heap,exit       ]   class space    used 414K, capacity 428K, committed 512K, reserved 1048576K</code></pre>    <p>瞧,即使是垃圾收集器也有信息显示出来——在这个示例中,是关于退出和堆的。</p>    <p>这些都只是表面上的——还有更多选项可用于调整。</p>    <p>事实上,这个改进并不能解决 <em>一切问题</em> 。因为 JEP 关注于通过它提供基础设施和改变一些(很多?那一定是所有 GC 消息)已经存在的调用,而不是所有事情。虽然我找不到其它使用新机制的日志,但它可能就在那里。</p>    <h3>命令行参数验证</h3>    <p>分享一件有趣的事情:Java 8 虚拟机要在使用到命令行参数的值时才会验证它们。不幸的是,这在应用程序的生命周期中可能会显得有些迟了,我可以预见随着潜在的错误而来的崩溃。来看一个例子:</p>    <pre>  <code class="language-java">java -cp out-mr/mr.jar -XX:BlockLayoutMinDiamondPercentage=120 ...Main</code></pre>    <p>我不知道 BlockLayoutMinDiamondPercentage 有什么作用(也懒得查),看起来 120不是一个有效的百分比。但 Java 8 没有收到干扰,愉快地执行了 JAR 包中的代码——显然这并不是程序运行所需要的值。也许 120 就是一个有效的值呢?Java 9 并不这么看:</p>    <pre>  <code class="language-java">java9 -cp out-mr/mr.jar -XX:BlockLayoutMinDiamondPercentage=120 ...Main    intx BlockLayoutMinDiamondPercentage=120 is outside the allowed range [ 0 ... 100 ]  Improperly specified VM option 'BlockLayoutMinDiamondPercentage=120'  Error: Could not create the Java Virtual Machine.  Error: A fatal exception has occurred. Program will exit.</code></pre>    <p>不错 ... <a href="/misc/goto?guid=4959725654943066465" rel="nofollow,noindex">JEP 245</a> 为 Java 9 带来了在开始运行时验证所有输入参数的能力。从上面的例子可以看到,它也输出了解释性的错误消息。</p>    <p> </p>    <p>来自:https://www.oschina.net/translate/inside-java-9-part-i</p>    <p> </p>