Java11 & JavaFX 初體驗 - Java 代碼生成 Markdown 的小工具

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 指向到了該版本。

開發

總結一下在整個過程中遇到的問題

  1. 項目搭建 - 10%
  2. 應用開發 - 20%
  3. 生成 JDK - 非模塊依賴轉模塊依賴 - 50%
  4. 應用打包 - 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 定義中可直接引用控制類上的方法,或者將頁面元素直接關聯到控制類。

綁定元素
image.png

元素關聯
image.png

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,唯一能比較好的處理方式

  1. 生成 module-info.java
  2. 解包
  3. 編譯 module-info.java
  4. 更新 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 開發也異常的簡單,整個過程還是很爽的!
點擊閱讀更多,查看更多詳情

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章