2019新春支付寶紅包技術大揭秘在線峰會將於03-07日開始,點擊這里報名屆時即可參與大牛互動。
概述
Java 11 自 2018.9.25 發布以來,已經好幾個月了,在還沒正式 GA 之前都習慣性的去 java-countdown.xyz check 發布倒計時。Java 11 有比較多的新功能,而其中最吸引我的
- Java 11 是 LTS 版本
- 這意味着體驗 Java9 帶來的模塊特性變得更有意義
- JavaFX 從 JDK 中移除,作為獨立模塊
在 11 發布時,JavaFX 也發布了 11 的 GA 版本。JavaFX 本身並不新奇,但自 Java9 模塊化后,JavaFX 得益於 jlink 的能力,能夠將 JavaFX 封裝為獨立的 GUI 應用,不要求安裝JDK 。這使得在桌面應用開發的場景,除了 Electron、Mono、QT 等跨平台開發框架,Java 也能作為其中的一項選擇了。在 Swing 時代,Java的桌面應用開發體驗也不差(曾經做過的小游戲 wenerme/GTetris),但由於累贅的 JDK (大約 150m)使得開發一個小應用變得不切實際。
JLink 可以將項目依賴的模塊加上基礎VM來生成一個新的 JDK,應用的體積能夠大大減小,如果還能再配合 progard,那體積還能再縮小一圈。
Motivation
基於體驗 Java11 和 JavaFX 的前提(每個Java程序員都會寫界面是常識?),將生成 奧格人群服務化接口文檔 的生成器做成了一個 GUI 工具,源碼在 wener.cyw/tools。
工具下載地址見附件 - 只打包了 Mac 版應用,因為沒有 Windows。
安裝
從 Java 11 開始,Oracle 的 JDK 便不再建議使用了,因此首選 OpenJDK,而 OpenJDK 的二進制提供方也有不少,在這里推薦使用 adoptopenjdk,與 Oracle 不同的是,在這里下載的 JDK 都是壓縮包,無須安裝,解壓就能使用,當然也不會有自動更新的能力。
下載后我解壓到了 ~/jdk
目錄,然后建立軟連接 ~/jdk/11
指向到了該版本。
開發
總結一下在整個過程中遇到的問題
- 項目搭建 - 10%
- 應用開發 - 20%
- 生成 JDK - 非模塊依賴轉模塊依賴 - 50%
- 應用打包 - 20%
項目搭建
搭建一個 Java 11 的 Maven 項目與搭建一個普通的項目區別並不大,只是會多一些配置,並且所有的依賴都需要使用最新的。
父 POM 的 build/plugins 配置說明
<!-- 對 Java 11 持有基本的尊敬 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<!-- 打包時打包到 modules 目錄 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
</configuration>
</plugin>
<!-- 將依賴拷貝到 modules 目錄 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/modules
</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<!-- 因為並不是所有依賴都是模塊化的,所以可能會出現 illegal-access 的問題 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<argLine>
--illegal-access=permit
</argLine>
<forkCount>0</forkCount>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.0</version>
<configuration>
<argLine>
--illegal-access=permit
</argLine>
</configuration>
</plugin>
應用項目的 build 配置
<build>
<!-- 因為用到了 fxml,且 fxml 是放在類旁邊的,所以需要手動指定該類資源 -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.fxml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<!-- 確保 jar 中生成正確的信息 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<id>module-main-class</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<!-- 因為 PATH 中的 jar 是 Java8,所以這里指定的絕對路徑 -->
<executable>/Users/wener/jdk/11/Contents/Home/bin/jar</executable>
<arguments>
<argument>
--update
</argument>
<argument> --file=${project.build.directory}/modules/${project.build.finalName}.jar
</argument>
<!-- 啟動類 -->
<argument> --main-class=me.wener.tools.app.AppMain
</argument>
<argument> --module-version=${project.version}
</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
最終的配置在 mvn package
后,會在 target/modules
目錄下看到所有的 jar 包。這里的 jar 在生成 JDK 時會用到。
在項目搭建好后,建立出對應的子模塊,且在子模塊中 src/main/java
設置好 module-info.java
應用開發
JavaFX 的開發非常有意思,因為可以使用 FXML,開發的過程體驗與 React/Vue/Angular 這樣的前端開發體驗非常相似,只需要在 FXML 做好布局,在 css 中定義好樣式,然后綁定好交互處理方法即可。
應用的啟動類
public class AppMain extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("scene/Main.fxml"));
Scene scene = new Scene(root, 640, 480);
stage.setTitle("@文邇 的小工具");
stage.setScene(scene);
stage.show();
}
}
因為是基於 fxml,啟動類只需要將該場景初始化展示即可。
一個 fxml 的基本框架
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.stage.Screen?>
<AnchorPane fx:id="masterPane"
xmlns="http://javafx.com/javafx/8.0.121"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="me.wener.tools.app.scene.MainScene">
</AnchorPane>
其中比較關鍵的是 fx:controller
綁定了控制類 me.wener.tools.app.scene.MainScene
。
因此在后續的 action
定義中可直接引用控制類上的方法,或者將頁面元素直接關聯到控制類。
綁定元素
元素關聯
Intellij 比較智能,可直接在這兩個地方互相跳轉。
<!-- 按鈕點擊關聯控制類上的方法 doConvert -->
<Button mnemonicParsing="false" onAction="#doConvert" text="生成文檔"/>
生成 JDK
在 APP 開發完成后,即可為該 APP 生成一個定制的 JDK,該 JDK 只需要包含 APP 所需依賴,生成的 JDK 可重復使用,除非 APP 的依賴變更。
# 確保下面的 Java 命令是 Java 11 的
export PATH=$JAVA_11_HOME/bin:$PATH
# 查看打包拷貝的模塊
# 其中會發現很多 automatic 的模塊
java --list-modules -p target/modules/
# 查看主應用 jar 的依賴請求
jdeps --module-path target/modules/ target/modules/tools-app-1.0-SNAPSHOT.jar
# 生成 JDK 到該目錄 jdk/Contents/Home/jre
# add-modules 的列表來自於 module-info 的定義
jlink --strip-debug --compress 2 \
--no-header-files --no-man-pages \
--output jdk/Contents/Home/jre \
-p $PWD/target/modules \
--add-modules javafx.controls,javafx.fxml,com.google.common,com.github.javaparser.core,com.github.javaparser.symbolsolver.logic,com.github.javaparser.symbolsolver.model,me.wener.tools.core
但在生成 JDK 時會發現異常
Error: automatic module cannot be used with jlink: com.github.javaparser.symbolsolver.logic from xxx.jar
異常的原因是 jlink 不支持 automatic 的模塊,所謂 automatic 模塊,指的是沒有 module-info 的模塊,但在 jar 的 META-INF/MANIFEST.MF
中定義了 Automatic-Module-Name
信息。
針對這類 jar,唯一能比較好的處理方式
- 生成 module-info.java
- 解包
- 編譯 module-info.java
- 更新 jar
一下以 javax.inject 為案例
wget http://central.maven.org/maven2/javax/inject/javax.inject/1/javax.inject-1.jar
# 查看依賴情況,非模塊化的 jar 依賴和模塊化 jar 的依賴現實不同
# 輸出: javax.inject-1.jar -> java.base
# 模塊化的 jar 輸出: javax.inject -> java.base
jdeps javax.inject-1.jar
# 生成 module-info.java
jdeps --generate-open-module info javax.inject-1.jar
# 解壓 jar
unzip javax.inject-1.jar -d classes/
# 編譯 module-info.java
javac -p javax.inject -d classes/ info/javax.inject/module-info.java
# 更新 jar
jar uf javax.inject-1.jar -C classes/ module-info.class
# 再次查看依賴
jdeps javax.inject-1.jar
其中 info/javax.inject/module-info.java
的內容為
open module javax.inject {
}
接下來的一段時間便是將所有用到的依賴進行這樣的轉換,其中需要注意的是 間接依賴也需要模塊處理。其中最難處理的是 guava,因為需要將 guava 模塊化,也需要它依賴的所有模塊都存在。
open module com.google.common {
requires j2objc.annotations;
requires java.logging;
requires jdk.unsupported;
requires jsr305;
requires transitive error.prone.annotations;
}
因此為了將 guava 模塊化,需要從 maven 上下載所有的這些 jar 並進行模塊化。
完成所有的模塊化后,再次通過 jlink 生成 jdk 到 jdk/Contents/Home/jre
,之所以生成到這樣的一個目錄,是因為在應用打包時能符合默認的 Java 目錄結構。
# 使用生成的 JDK 來運行應用
./jdk/Contents/Home/jre/bin/java -Xmx64m --upgrade-module-path target/modules -m me.wener.tools.app
# 生成的 JDK 大約 50m - 對此已經非常滿意了,Electron 一般都是 100m 左右
du -s jdk/
應用打包
應用打包主要是將現在已經能運行的 jdk 環境打包為一個 macOs 的 app。打包器有不同的選擇,但用下來還是 jar2app 比較好用。如果需要打包其它平台應用,需要選擇其它平台的打包器。
git clone https://github.com/Jorl17/jar2app
# jar2app 是 Python 腳本,因此需要 Python 環境
# 打包,使用自定義 jdk target/jdk
./jar2app/jar2app ./target/modules/tools-app-1.0-SNAPSHOT.jar -r target/jdk/ -b me.wener.tools -n WenerTools -j "-Xmx64M --upgrade-module-path $APP_ROOT/Contents/PlugIns/jdk/modules"
# 最終打包后的應用約 50m
# 50M WenerTools.app
du -s WenerTools.app
# 雙擊啟動或命令行啟動
open WenerTools.app
一切大功告成,一個 APP 就此誕生了!如果還想要提交到 AppStore,這個過程還會需要其他的不少步驟,在這就不詳細說明啦。
總結
應用開發過程,打包過程都還是比較愉快的,最困難的是模塊化 jar 的處理,因為很多模塊都還沒有 module-info.java,導致大部分的 jar 都得先處理一遍,不過這個過程是可以累計的,被處理過的 jar 可以被重復利用。如果不需要配合 jlink,那么是不需要處理的。
Java 11 意味着 Java 9、10、11 的所有新特性,JavaFX 開發也異常的簡單,整個過程還是很爽的!
點擊閱讀更多,查看更多詳情