JavaFX17 现代 Java 客户端权威指南(五)
原文:The Definitive Guide to Modern Java Clients with JavaFX 17
九、JavaFX、Web 和云基础设施
由约翰·沃斯、布鲁诺·博尔赫斯和何塞·佩雷达创作
正如这个术语本身所暗示的,客户端应用程序很少是自包含的。为了正常运行,它们需要访问其他组件,例如服务器应用程序。JavaFX 作为一个 Java 框架,允许应用程序开发人员利用广泛的 Java 生态系统。为与 REST APIs、web 组件、SOAP 端点和加密 API 等交互而创建的库可以在 JavaFX 应用程序中使用。因此,JavaFX 应用程序与非 JavaFX 应用程序没有什么特别之处。然而,JavaFX 中有许多特性使开发人员能够创建到其他(后端或服务器端)组件的简单、安全、可靠的连接。
在本章中,我们将讨论将后端组件与 JavaFX 应用程序集成的两种方法。首先,我们将讨论 WebView 组件。JavaFX WebView 控件的核心具有 web 浏览器的大部分功能,它允许网页上的脚本元素和 JavaFX 应用程序中的 Java 函数之间的双向交互。它通常被用作一种简单的方法,将网站的现有功能呈现到桌面应用程序中。
这种方法如图 9-1 所示。
图 9-1
使用 WebView 集成后端功能
接下来,讨论一种更灵活、更通用的方法,用于将 JavaFX 控件连接到云中或后端基础设施中的远程端点。在这种方法中,JavaFX 应用程序直接与后端 API 通信,如图 9-2 所示。
图 9-2
JavaFX 应用程序直接与后端 API 通信
与网络整合
使用 WebView 组件,您可以在 JavaFX 应用程序中显示 HTML5 内容。这适用于本地 HTML 文件或网页。该组件基于 WebKit,功能非常强大。可能的应用范围从文档的显示到完整 web 应用程序的集成。非常实用的是,通过 JavaScript 桥很容易与 WebView 组件的内容进行交互。这允许我们从 JavaFX 修改页面并对用户操作做出反应。
显示网页
对于简单的用例,WebView API 本身非常容易使用。WebView 和相关类是 javafx.web 模块的一部分。因此,如果我们想要使用它,我们应该确保将 javafx.web 模块添加到我们的模块路径中。使用命令行方法,这是通过添加
--add-modules javafx.web
将 Maven 与 javafx-maven-plugin 一起使用时,我们可以很容易地添加一个依赖项,如下所示:
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>17.0.1</version>
</dependency>
这也在样本存储库中的webviewdemo
样本的pom.xml
文件中进行了说明。
我们可以马上从一个代码示例开始。核心组件是WebView
节点。我们可以像其他节点一样将它添加到场景图中。我们请求它的WebEngine
,并传递一个 URL 给它,如下所示:
public class WebViewDemo extends Application {
@Override
public void start(Stage primaryStage) {
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
engine.load("https://openjfx.io");
Scene scene = new Scene(webView, 300, 250);
primaryStage.setTitle("JavaFX WebView Demo");
primaryStage.setScene(scene);
primaryStage.show();
}
}
图 9-3
在 JavaFX WebView 中加载网站
三行突出显示的代码足以加载如图 9-3 所示的网页。通过右键单击,您可以打开页面上的上下文菜单,并根据当前状态,停止或重新启动页面加载。默认情况下,此菜单可用。对于最常见的用例,您不需要它。让我们用下面的命令禁用它:
webView.setContextMenuEnabled(false);
添加导航和历史记录
要使用 WebEngine 的 load 命令在网站之间导航,可以向 JavaFX 应用程序添加按钮或菜单,以便在单击时显示特定页面。为了响应点击,只需调用 engine.load(" http://myurl.com
")来指示 web 引擎为您加载页面。让我们在我们的例子中用一个菜单栏来试试这个,如图 9-4 所示。为此,我们首先将 WebView 放入一个 BorderPane 中,这样我们可以很容易地在它上面插入一个 MenuBar。像这样更改开始方法:
图 9-4
为导航添加菜单栏
public void start(Stage primaryStage) {
WebView webView = new WebView();
webView.setContextMenuEnabled(false);
WebEngine engine = webView.getEngine();
engine.load("https://openjfx.io ");
BorderPane borderPane= new BorderPane(webView);
MenuBar menuBar = new MenuBar();
final Menu navigateMenu = new Menu("Navigate");
MenuItem home = new MenuItem("Home");
navigateMenu.getItems().addAll(home);
home.setOnAction(e -> engine.load("https://github.com/openjdk/jfx"));
menuBar.getMenus().add(navigateMenu);
borderPane.setTop(menuBar);
Scene scene = new Scene(borderPane, 640, 400);
primaryStage.setTitle("JavaFX WebView Demo");
primaryStage.setScene(scene);
primaryStage.show();
}
网络引擎也让我们可以使用导航历史。web 引擎的 getHistory 方法返回具有 getEntries 方法的 History 对象。这是一个可观察的列表,我们可以用它来跟踪变化。让我们用它来填充一个菜单:
Menu historyMenu = new Menu("History");
engine.getHistory().getEntries().addListener((ListChangeListener.Change<? extends Entry> c) -> {
c.next();
for (Entry e: c.getAddedSubList()) {
for(MenuItem i: historyMenu.getItems()){
if (i.getId().equals(e.getUrl())){
historyMenu.getItems().remove(i);
}
}
}
for (Entry e: c.getAddedSubList()) {
final MenuItem menuItem = new MenuItem(e.getUrl());
menuItem.setId(e.getUrl());
menuItem.setOnAction(a->engine.load(e.getUrl()));
historyMenu.getItems().add(menuItem);
}
});
menuBar.getMenus().addAll(navigateMenu, historyMenu);
图 9-5
访问浏览历史
结果如图 9-5 所示。对于构建一个好的基本浏览器来说,这并不需要太多的努力。现在是时候改善用户体验了。目前,我们没有得到加载过程的指示。
显示加载进度
进度条在这里会很有帮助。我们可以在加载新内容时将其显示在页面底部。幸运的是,WebEngine
正在使用 javafx 并发 API,因此我们可以使用Property
来轻松跟踪进度。因此,让我们在 BorderPane 中添加一个 ProgressBar 来显示进度:
ProgressBar progressBar = new ProgressBar();
progressBar.progressProperty().bind(
engine.getLoadWorker().progressProperty());
progressBar.visibleProperty().bind(engine.getLoadWorker().
stateProperty().isEqualTo(State.RUNNING));
borderPane.setBottom(progressBar);
我们还将 ProgressBar 的 visibleProperty 绑定到 Worker 的 stateProperty。一个非常小的代码示例完美地说明了属性和绑定与 JavaFX APIs 的其余部分是如何集成的。不需要监听器,这确实有助于提高代码的可读性。带有工作进度条的示例如图 9-6 所示。
图 9-6
加载页面时显示进度
从网站到 API
在本节中,我们将展示如何在 JavaFX 应用程序中轻松呈现现有的网站。通过将网站上的 JavaScript 功能与 JavaFX 应用程序中的 Java 功能相链接,可以增强网站功能并使其更具交互性,或者将其与网络浏览器不可用的功能相集成(例如,与设备的连接)。
虽然这通常是基于现有网站创建桌面应用程序的快速而简单的解决方案,但在大多数情况下,它不提供后端提供的丰富功能,也没有利用 JavaFX APIs 提供的丰富功能。
在本章的第二部分,我们将讨论如何访问后端功能,并以更细粒度的方式将其与 JavaFX 控件集成。
面向云的构建
大约在 2012 年,云最终成为主流,但只有极少数玩家仍然主导着市场。然而,竞争是巨大的,每个季度都会发布服务和 API。为云构建应用程序通常意味着利用这些高度可用和可扩展的基础设施即服务,以及高效、易用和易于集成的平台即服务。无论是为您快速调配的虚拟机或数据库,还是只需要订阅密钥的人脸检测 API,所有这些资源都有一个共同点:管理、维护和扩展它们的不是您,而是您的云提供商。
云通常还意味着构建基于 web 的应用程序或基于 web 的 REST APIs,它们将在云中运行,并与同样位于云中的资源进行对话。如何构建和部署这些应用和服务通常取决于微服务和云原生架构剧本。云资源的可伸缩性模式非常适合这些 web 应用程序和微服务。当一切都在互联网上,并且通过浏览器、一些基于 HTTP 的自动化客户端或消息系统访问应用程序时,桌面应用程序常见的许多挑战(如版本更新、数据缓存、服务重新路由等)根本不存在,或者更容易在云中解决。
那么当面向用户的应用程序是富客户端时会发生什么呢?开发人员如何利用云,为什么?这里的重点是通过不必在客户端本身开发某些算法和业务逻辑,而是将它们转移到开发人员可以更好地控制和更快地交付更新的地方,或者使用现成的服务 API(否则将花费大量开发时间在富客户端中构建),来为他们的现代客户端桌面应用程序提供价值。
与客户端简单地显示带有网站的 WebView 相比,这种方法要强大得多。部分功能可以在客户端实现,而其他部分可以卸载给云提供商。
JavaFX 云应用程序的架构
大多数现代企业系统都有多层架构,其中基于 web 的前端和业务逻辑是分离的。典型的 web 框架查询业务层,要么使用直接(Java)调用,要么使用 REST APIs。如下图所示。
业务逻辑不是在 web 应用程序内部实现的,而是通常在使用某种特定企业架构的后端组件中实现的,这提供了额外的功能,如安全性、可伸缩性、监控等。相同的概念可以应用于 JavaFX 客户端应用程序的情况,如下所示。
在这种情况下,JavaFX 应用程序调用的 API 层与典型的 web 应用程序调用的 API 层相同。这种方法的优点是只需要一个 API 层来服务不同类型的客户端。
通常,API 层会公开一个 REST 接口。许多 web 框架提供了对 REST 接口的访问,也有大量的 Java 库允许开发人员访问 REST 接口,并将 REST 接口的输出转换为 Java 对象。
作为一个例子,我们将使用 OpenWeather API,这是一个通过 REST 接口访问的 API 层。许多基于 web 的应用程序都使用这个 API,我们将展示如何在 Java 客户端应用程序中利用这个 API 的三种方法。
用例:查询 OpenWeather
我们现在将展示一个 JavaFX 应用程序,它查询 OpenWeather API ( https://openweathermap.org
)来检索给定位置的天气。我们可以简单地使用 WebView 并呈现现有的 OpenWeather 网站,但是我们希望利用 JavaFX 控件的强大功能,或者我们希望将天气数据无缝集成到现有的 JavaFX UI 应用程序中。
我们现在将解释如何编写一个从 OpenWeather 中检索查询的应用程序。为了做到这一点,第一件事就是在门户( https://home.openweathermap.org/users/sign_up
)注册并获得默认的 API 密匙或者生成一个新的( https://home.openweathermap.org/api_keys
)。
在下面的代码片段中,您会注意到 API 键有时是必需的。OpenWeather API 允许单个帐户每小时进行有限次数的呼叫。因此,最好创建并使用自己的 API 密钥,而不是使用共享的 API 密钥。这就是为什么我们通常用例如“XXXXXXXXXXX”来替换真正的密钥。确保用您从 OpenWeather 门户获得的真实 API 密钥替换这个“XXXXXXXXXXX”。
首先,让我们创建一个简单的应用程序来显示给定时间和城市的天气:
public class WeatherApp extends Application {
private static final String API_KEY = "XXXXXXXXXXX";
private static final String CITY = "London";
private ImageView imageView;
private Label weatherLabel;
private Label descriptionLabel;
private Label tempLabel;
@Override
public void start(Stage stage) {
imageView = new ImageView();
imageView.setFitHeight(100);
imageView.setPreserveRatio(true);
imageView.setEffect(new DropShadow());
Label label = new Label("The weather in " + CITY);
weatherLabel = new Label();
descriptionLabel = new Label();
descriptionLabel.getStyleClass().add("desc");
tempLabel = new Label();
tempLabel.getStyleClass().add("temp");
VBox root = new VBox(10,
label, imageView, weatherLabel, descriptionLabel, tempLabel);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 600, 400);
scene.getStylesheets().add(
WeatherApp.class.getResource("/styles.css").toExternalForm());
stage.setScene(scene);
stage.setTitle("The Weather App");
stage.show();
retrieveWeather();
}
private void retrieveWeather() {
// TODO
}
}
这段代码利用了我们在本书前面章节中讨论过的布局和控件。唯一的新部分是“retrieveWeather()”
调用that
目前还没有实现。在本章的剩余部分,我们将通过多种方式实现这一点。
除了代码之外,在应用程序中应用一些样式信息通常也是很好的。在此示例中,我们使用以下 CSS 内容:
.label {
-fx-font-size: 1.4em;
-fx-text-fill: blue;
}
.label.desc {
-fx-font-size: 1.2em;
-fx-text-fill: gray;
}
.label.temp {
-fx-font-size: 1.1em;
-fx-text-fill: green;
}
获取给定系统的天气数据所需的查询由 OpenWeather API 定义,并在 https://openweathermap.org/current
中描述。
注意,API 定义可能会改变,所以如果 OpenWeather 改变了它的 API,当前的方法可能需要做一些小的修改!
要使用我们的 API 键查询给定城市的 OpenWeather,基本上我们只需要创建以下查询:
"https://api.openweathermap.org/data/2.5/weather?appid="
+ API_KEY + "&q=" + CITY
当这样的查询被发送到 OpenWeather API 时,我们将得到 JSON 格式的响应,如下所示:
{"coord":{"lon":-0.13,"lat":51.51},
"weather":[{"id":500,"main":"Rain",
"description":"light rain","icon":"10d"}],"base":"stations",
"main":{"temp":290.14,"pressure":1012,"humidity":68,
"temp_min":288.15,"temp_max":292.59},"visibility":10000,
"wind":{"speed":4.1,"deg":180},"clouds":{"all":40},
"dt":1563527401,"sys":{"type":1,"id":1414,"message":0.0137,
"country":"GB","sunrise":1563509115,"sunset":1563566876},
"timezone":3600,"id":2643743,"name":"London","cod":200}
有多种方法可以处理这个响应。在 Java 生态系统中,存在许多处理 JSON 输入并将其绑定到 Java 对象的库。为了促进 JSON 数据和 Java 对象之间的映射,我们将使用模型实体将 JSON 字符串反序列化为如下内容:
public class Model {
private long id;
private long dt;
private Clouds clouds;
private Coord coord;
private Wind wind;
private String cod;
private String visibility;
private long timezone;
private Sys sys;
private String name;
private String base;
private List<Weather> weather = new ArrayList<>();
private Main main;
// Getters & setters
}
public class Clouds {
private String all;
// Getters & setters
}
public class Coord {
private float lon;
private float lat;
// Getters & setters
}
public class Main {
private float humidity;
private float pressure;
private float temp_max;
private float temp_min;
private float temp;
private float feels_like;
// Getters & setters
}
public class Sys {
private String message;
private String id;
private long sunset;
private long sunrise;
private String type;
private String country;
// Getters & setters
}
public class Weather {
private int id;
private String icon;
private String description;
private String main;
// Getters & setters
}
public class Wind {
private float speed;
private float deg;
private float gust;
// Getters & setters
}
如您所见,所描述的模型映射到 JSON 服务提供的输出。一些框架要求对象字段的名称与 JSON 字段的名称完全匹配。其他的允许注释来提供匹配。在某些情况下,字段是必需的,而在其他情况下,getters 和 setters 是必需的。
案例一:杰克逊
将 JSON 反序列化为 Java 对象的一个非常流行和灵活的框架是 Jackson 项目,它提供了一个 JSON Java 解析器( https://github.com/FasterXML/jackson
)。
我们可以简单地将依赖项添加到我们的项目中:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
(for Maven)
or
dependencies {
implementation "com.fasterxml.jackson.core:jackson-databind:2.12.3"
}
(for Gradle)
现在我们可以完成我们的retrieveWeather
通话了:
private void retrieveWeather() {
try {
String restUrl =
"https://api.openweathermap.org/data/2.5/weather?appid="
+ API_KEY + "&q=" + CITY;
ObjectMapper objectMapper = new ObjectMapper();
Model model = objectMapper.readValue(
new URL(restUrl), Model.class);
updateModel(model);
} catch (Throwable e) {
System.out.println("Error: " + e);
e.printStackTrace();
}
}
private void updateModel(Model model) throws MalformedURLException, URISyntaxException {
if (model != null) {
if (!model.getWeather().isEmpty()) {
Weather w = model.getWeather().get(0);
imageView.setImage(new Image(new URL("http://openweathermap.org/img/wn/" + w.getIcon() + "@2x.png").toURI().toString()));
weatherLabel.setText(w.getMain());
descriptionLabel.setText(w.getDescription());
}
tempLabel.setText(String.format("%.2f °C - %.1f%%", model.getMain().getTemp() - 273.15, model.getMain().getHumidity()));
}
}
如果您已经在使用 Jackson API,这段代码看起来会非常熟悉。从服务器端应用程序和从 JavaFX 应用程序使用 Jackson API 实际上没有区别。
我们不会详细讨论杰克逊,但你可以从代码中了解正在发生的事情。
首先,构建包含所需 API 请求的 URL:
String restUrl =
"https://api.openweathermap.org/data/2.5/weather?appid="
+ API_KEY + "&q=" + CITY;
URL 要求填写 API_KEY 和感兴趣的城市。
接下来,我们使用 Jackson 代码来检索信息,并将结果转换成 Java 对象:
ObjectMapper objectMapper = new ObjectMapper();
Model model = objectMapper.readValue(
new URL(restUrl), Model.class);
ObjectMapper API 是 Jackson 的一部分,它允许解析 REST 方法的输出,创建所提供类的新实例(在本例中为“Model”),并将该类的字段与返回的 JSON 数据中的字段进行映射。
最后,调用“updateModel”来确保检索到的信息在场景中可视化。为此,我们使用了本书前面讨论过的 JavaFX APIs。
现在我们运行如图 9-7 所示的应用程序。
图 9-7
运行的应用程序
这种方法非常容易实现,尤其是如果您熟悉 Jackson 或另一个 Java 中的 JSON 解析库。
虽然这种方法非常方便,但是有一些点是 JavaFX 特有的,应该加以考虑。
例如,解析是在同步调用中完成的,以完成查询给定 URL、等待结果、检索和解析响应的整个过程。如果出现任何问题,比如超时或网络错误,将只有一个异常,并且 UI 可能会冻结,因此我们应该将该调用包装在 JavaFX 服务中。
此外,虽然模型类非常通用,并且是面向对象开发中的良好实践,但它没有利用 JavaFX 中的“可观察”概念。因此,需要样板代码来更新 UI。
我们采用的方法可以想象如下:
情况二:连接
Gluon 的 Connect 库( https://github.com/gluonhq/connect
)不仅像 Jackson 一样反序列化 JSON 响应还以异步的方式进行;此外,它返回 JavaFX 可观察列表或对象,JavaFX UI 控件可以直接使用这些列表或对象。使用 Gluon Connect 可以省去将解析的对象转换成 JavaFX 可观察实例的额外步骤,因为它直接填充或操作这些实例。视觉上,这可以显示如下:
使用 Gluon Connect 需要向我们的项目添加以下依赖项:
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>connect</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>1.1.5</version>
<scope>runtime</scope>
</dependency>
(for Maven)
or
dependencies {
implementation "com.gluonhq:connect:2.0.1"
runtimeOnly 'org.glassfish:jakarta.json:1.1.5
}
(for Gradle)
And now our updateWeather call can be done as follows:
private void retrieveWeather() {
GluonObservableObject<Model> weather = getWeather();
weather.setOnFailed(e -> System.out.println("Error: " + e));
weather.setOnSucceeded(e -> updateModel(weather.get()));
}
private GluonObservableObject<Model> getWeather() {
RestClient client = RestClient.create()
.method("GET")
.host("http://api.openweathermap.org/")
.connectTimeout(10000)
.readTimeout(1000)
.path("data/2.5/weather")
.header("accept", "application/json")
.queryParam("appid", API_KEY)
.queryParam("q", CITY);
return DataProvider.retrieveObject(
client.createObjectDataReader(Model.class));
}
运行这个应用程序会产生和以前一样的输出,但是代码更加简洁。“retrieveWeather”方法首先创建一个“GluonObservable”对象的实例。这个对象的创建创建了一个“RestClient ”,它定义了我们需要查询的 REST 接口,然后它使用一个“DataProvider”来提供一个“GluonObservableObject ”,该对象包含来自结果 JSON 输出的解析信息。
我们可以在这个对象上设置回调方法。例如,当数据被成功检索时,我们用提供的天气信息更新模型。
我们创建的应用程序非常简单,但您会发现它比简单地在 WebView 控件中呈现网站更加灵活和动态。通过访问外部 API,JavaFX 应用程序中的代码看起来类似于访问这些外部 API 的后端代码,如图 9-8 所示。
图 9-8
JavaFX 应用程序看起来类似于访问外部 API 的后端代码
JavaFX 客户端或 web 客户端与 API 层建立连接,然后 API 层可能会使用更复杂的业务逻辑,与数据层对话,并以某种方式处理请求。
客户端代码驻留在云环境中和最终用户的桌面上有一个非常重要的区别。在第一种情况下,云环境在大多数情况下可以被视为可信环境。然而,在代码驻留在最终用户桌面上的情况下,这不再是真的。最终用户自己或设备上的其他恶意应用程序可能会获得 API 密钥。只要 API 密钥存储在设备上,任何能够访问设备的人都可能以某种方式使用或滥用它。如下图所示:
在 OpenWeather 查询的情况下,这可能不是最致命的问题,但是一般来说,提供对关键或敏感功能的访问的 API 密钥不应该存储在最终用户系统上。
为了解决这个问题,使用中间件组件作为可信云基础设施和用户的客户端设备(桌面/移动/嵌入式)之间的桥梁是有益的。
然后,API 密钥保存在中间件组件上,该组件托管在一个安全的环境中,如下所示:
如果设备现在被黑客攻击,或者客户端应用程序不知何故受到损害,黑客将无法访问 API 密钥。在最坏的情况下,黑客可以与中间层对话,假装他们是设备的原始所有者,然后用真正的 API 密钥执行调用,但他们自己不会访问这个密钥。
通过在设备和中间层之间提供最佳的安全性和认证,甚至可以避免这种情况。
这个中间件组件可以扩展并导出通用的云功能(例如,访问无服务器容器),如图 9-9 所示。
图 9-9
中间件组件可以被扩展以导出通用的云功能
在下一段中,我们将解释如何使用 Gluon CloudLink ( https://docs.gluonhq.com/cloudlink/
)编写 OpenWeather 应用程序,这是 Gluon 的一个中间件解决方案,为 JavaFX 应用程序提供特定的支持。
案例三:胶子云链
Gluon CloudLink 提供了比简单地代表设备存储 API 密钥更多的功能。我们不会详细讨论这个功能,但是我们将在下面的例子中使用一个方便的特性。我们不会在 JavaFX 客户机上创建 REST 请求,而是使用基于 web 的仪表板在 Gluon CloudLink 中编写请求。
在我们添加代码之前,我们需要访问 Gluon 仪表板( https://gluon.io
),使用我们的凭证访问,并转到 API 管理来添加一个远程函数。远程功能是一个可以从 Java 客户端调用的组件(我们将在下一个代码片段中展示),但它是在中间件上执行的(因此在 Gluon CloudLink 内部)。这有很多好处,包括灵活性:如果 REST API 以某种方式发生了变化(例如,添加或重命名了一个参数),我们可以在 Gluon Dashboard 控制台中重新定义远程函数,而不必更改客户端代码。特别是在您不能轻松地立即更新所有客户机实例的环境中,这可能会产生很大的不同。
在客户机仪表板中,当创建远程函数时,我们为请求添加两个查询参数:appid
和q
。我们用 API 键设置前者的值,所以客户端不需要这样做,而后者的值将在客户端设置。正如我们之前强调的,认识到 API 密匙存储在中间件中(托管在云中)并且只在中间件和远程服务之间发送是很重要的。客户端应用程序没有访问此 API 密钥;因此,密钥被黑客获取的风险被最小化。
我们可以在测试字段中设置一对值,这样我们就可以从图 9-10 所示的仪表板中直接测试远程功能。
图 9-10
从胶子仪表板测试功能
一旦测试成功(我们应该看到一个 200 响应和一个 JSON 字符串),我们就可以回到我们的项目,在 pom.xml 文件中添加 CloudLink 依赖项。这是通过将胶子库添加到库列表中来实现的:
repositories {
mavenCentral()
maven {
url 'https://nexus.gluonhq.com/nexus/content/repositories/releases/'
}
}
我们还必须向 pom.xml 添加一些依赖项,如下所示:
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>charm-cloudlink-client</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.4</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>1.1.5</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>storage</artifactId>
<version>4.0.11</version>
</dependency>
Gluon CloudLink 客户端代码设计用于桌面、移动和嵌入式设备。为了做到这一点,它抽象了一些在不同平台上有不同实现的功能。开源项目 Gluon Attach 提供了这种抽象,以及针对桌面、iOS 和 Android 平台的实现。例如,数据在平台上的存储方式在这些目标之间是不同的,这被 Gluon CloudLink 客户端代码所使用。因此,我们向 com.gluonhq.attach.storage 服务添加了一个依赖项。为了使应用程序平台独立,我们还添加了 gluonfx-maven-plugin,它确保使用正确的附加服务的本机实现:
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>1.0.2</version>
<configuration>
<attachList>
<list>storage</list>
</attachList>
<mainClass>org.modernclientjava.WeatherApp</mainClass>
</configuration>
</plugin>
最后,我们将代码修改为
private void retrieveWeather() {
GluonObservableObject<Model> weather = getWeather();
weather.setOnFailed(e -> System.out.println("Error: " + e));
weather.setOnSucceeded(e -> updateModel(weather.get()));
}
private GluonObservableObject<Model> getWeather() {
RemoteFunctionObject functionObject = RemoteFunctionBuilder
.create("weather")
.param("q", CITY)
.object();
return functionObject.call(Model.class);
}
正如您所看到的,在来自客户端的远程函数调用中只有一个查询参数。
客户端中唯一需要的凭证是那些访问 CloudLink 中间件的凭证。在src/main/resources
需要一个名为gluoncloudlink_config.json
的文件:
{
"gluonCredentials": {
"applicationKey" : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"applicationSecret": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
}
此文件的内容可以从仪表板➤凭据➤客户端检索。
因为我们希望依赖于平台的附加实现由 gluonfx 插件来处理,所以我们使用
mvn gluonfx:run
这将检测当前平台并使用正确的实现。
结论
JavaFX 应用程序是常规的 Java 应用程序,可以使用所有现有的 Java 功能。使用 JavaFX WebView 控件,可以在 JavaFX 应用程序中呈现网页,并在网页功能(在 JavaScript 中)和客户端上运行的 Java 引擎之间创建交互。
对于客户机应用程序与云中企业功能的细粒度、安全和灵活的集成,可以使用现有的 Java 框架。开发人员应该非常清楚客户端系统的具体特征,尤其是与安全性相关的特征。因此,中间件(例如,Gluon CloudLink)允许开发人员屏蔽客户端应用程序的安全敏感信息。
十、为桌面打包应用
由 José Pereda、Johan Vos 和 Gail Anderson 撰写
创建 Java 客户端应用程序只是应用程序生命周期的第一步。您的应用程序将由与之交互的最终用户使用。无论您的应用程序是供内部使用、仅供内部网使用还是供广大公众使用,您都希望使用您的应用程序的障碍尽可能低。最终用户在开始使用应用程序之前不喜欢复杂的过程。
在这一章中,我们将解释如何构建最终用户可以按照安装其他(非 Java)应用程序的相同方法来安装和使用的应用程序。我们将探索两个重要的现代工具,用于创建满足最终用户期望的捆绑 Java 应用程序:jpackage 和 GraalVM 的本机映像。
Web 与桌面应用程序
当您创建一个 Java 客户端应用程序,或者通过扩展创建任何客户端应用程序时,最终用户将直接访问您的应用程序。这与创建云或基于 web 的应用程序非常不同,在这种情况下,您的代码可能运行在云或服务器基础架构上,最终用户通常通过 web 浏览器访问您的代码。在这种情况下,您必须将您的应用程序供应到云或服务器,有一些特定的工具可以帮助您完成这项任务。
您或您的 IT 部门控制着应用程序的运行环境。您知道周围的环境,包括操作系统、资源限制和可用的软件。这种情况如图 10-1 所示。
图 10-1
应用程序运行的环境
对于客户端应用程序,您需要确保您的应用程序可以安装在所有客户端系统上。如果幸运的话,您的应用程序将被部署在完全由您或您的 IT 组织控制的 intranet 上。但是通常情况下,您无法控制应用程序应该运行的环境。通常假设您的应用程序应该可以在所有操作系统上运行,您不知道最终用户是否安装了 Java,如果安装了,是哪个或哪些版本。与云环境中的部署相反,您不能自己在最终用户的系统上安装应用程序。您依赖这个最终用户来执行所需的指令;因此,您希望这些说明尽可能简单和熟悉。客户端部署场景如图 10-2 所示。
图 10-2
桌面部署场景
应用程序部署的演变
在过去,大多数 Java 客户端应用程序的部署都需要在应用程序代码、其依赖项和运行应用程序所需的 Java 虚拟机(JVM)之间进行明确的分离。从 1995 年的 Java Applets 开始,到后来的 Java Web Start,Java 客户端应用程序的开发人员可以假设在最终用户系统上已经安装了一些 Java 虚拟机。如果不是这样,开发人员可以假设存在某种工具来帮助最终用户安装 JVM。
因此,开发人员必须使用构建工具将程序代码、依赖项和资源捆绑到一个应用程序中,然后允许最终用户在他们已经安装的 JVM 上安装该应用程序——参见图 10-3 。
图 10-3
遗留开发过程
这个概念背后的基本原理很清楚:当一个客户机上运行多个 Java 应用程序时,用一个 JVM 安装为不同的应用程序提供基础是有意义的。由于 Java 非常注重向后兼容性,所以假设您的应用程序仍然可以在 JVM 的更高版本上运行也是合理的。因此,最终用户系统上的单个 JVM 就足以满足所有 Java 客户端应用程序的需求。因此,这个概念需要更少的磁盘空间(JVM 在应用程序之间共享)和更少的带宽(JVM 代码不需要随应用程序一起传输;假设它是存在的)。
近年来,许多发展使得这种方法变得不那么引人注目:
-
典型的磁盘空间和带宽正在增加。今天的普通用户正在使用更高容量的存储容量和流容量。其中一个驱动因素是媒体流和存储的增加。与典型流服务的需求相比,JVM 代码的传输和存储相对较小。因此,JVM 代码不应该与应用程序一起发送或与应用程序一起存储,因为这需要太多的带宽或存储容量的论点是不相关的。
-
Java 开发工具包的新版本节奏给最终用户带来了更多的责任。许多用户发现在他们的 PC 上看到一个弹出窗口要求升级到 Java 的新版本是很烦人的。他们的应用程序可能使用 Java,但是这对最终用户应该是透明的。虽然不同的应用程序可能仍然运行在较旧的 JVM 上,但是开发人员通常希望将较新的 JVM 中的新特性用于他们的应用程序。这导致更多的弹出窗口让最终用户感到烦恼。
-
应用商店(app stores)的概念改变了移动领域,也在桌面领域获得了发展。使用应用程序商店,最终用户只需决定使用统一的“点击安装”概念来安装应用程序。执行应用程序所需的一切都被认为是该应用程序的一部分。因此,最终用户不会管理没有给用户增加功能的必需组件(例如,JVM)。
Java 生态系统见证了这一演变。JavaFX 库以前包含一个“javafxpackager”工具,该工具允许开发人员为不同的平台(Linux、Windows、macOS)打包他们的 JavaFX 应用程序,包括一个 Java 虚拟机和所有依赖项。
在 Java 平台模块系统中,JVM 本身被分解成许多模块,这种情况发生了变化。现在可以创建 JVM 的子版本,只包含特定应用程序需要的模块。由于打包与所有应用程序(不仅仅是需要用户界面的客户端应用程序)相关,一个新的 Java 打包工具是从 Java 14 开始的预览版和在 Java 16 中完成的 JDK 的一部分。我们将在下一节讨论这个叫做 jpackage 的工具。
除了 jpackage 之外,还有另一个工具允许您将应用程序捆绑到一个自包含的本机应用程序中,即 GraalVM 的本机映像。使用这个工具,应用程序及其依赖项可以提前编译成本地应用程序。我们将在本章的后面讨论这个工具。
图 10-4 显示了将应用程序与其依赖项和资源以及 JVM 捆绑在一起的概念。
图 10-4
现代发展进程
打包工具
什么是 jpackage?
从 JDK 14 的预览版开始,到 JDK 16 的最终版本,jpackage 工具是 JDK 发行版的一部分。jpackage 的规格是 JEP 392: https://openjdk.java.net/jeps/392
。(jpackage 的原始规格是 JEP 343。)
这个 JEP 的总结很清楚:
创建一个打包自包含 Java 应用程序的新工具。
这个总结符合我们在本章前面讨论的内容:我们希望创建包含 JVM 的自包含 Java 应用程序。
根据该网站,JEP 的目标如下:
基于 JavaFX javapackager 工具创建一个打包工具,该工具
-
支持本机打包格式,为最终用户提供自然的安装体验。这些格式包括 Windows 上的 msi 和 exe,macOS 上的 pkg 和 dmg,Linux 上的 deb 和 rpm。
-
允许在打包时指定启动时参数。
-
可以直接从命令行调用,也可以通过 ToolProvider API 以编程方式调用。
由此可见,jpackage 工具构建在 JavaFX 打包工具之上,JavaFX 打包工具以前是 JavaFX 8 的一部分。此外,jpackage 工具被设计为支持流行的桌面操作系统的现有的和通用的本机打包格式。因此,用 jpackage 创建的 Java 应用程序的安装方式类似于大多数其他本机应用程序的安装方式。
Java 客户端应用程序现在结合了两大优势:
1.由于一次编写,随处运行的范例,开发人员必须编写的代码实际上是独立于平台的。
2.多亏了 jpackage 工具,虽然代码本身是平台无关的,但是部署过程完全符合特定的平台。
因为 jpackage 是 JDK 发行版的一部分,所以它像普通的 java 或 javac 命令一样容易调用。jpackage 可执行文件与 java 和 javac 可执行文件位于同一目录下。
当被调用时,jpackage 基于提供的代码、依赖项和资源创建一个本地应用程序。它将 Java 运行时与本机应用程序捆绑在一起,当应用程序被执行时,打包的 Java 运行时将执行 Java 代码,类似于运行时被单独安装在系统上的情况。如图 10-5 所示。
图 10-5
带包部署
使用包
要开始使用 jpackage 工具,您需要使用 Java 16 或更高版本;但是,正如后面将要解释的,您可以将您的应用程序与其他版本的 Java 运行时捆绑在一起。
jpackage 工具支持三种应用程序类型:
-
在类路径上运行的非模块化应用程序,来自一个或多个 jar 文件
-
具有一个或多个模块化 jar 文件或 jmod 文件的模块化应用程序
-
链接到自定义运行时映像的模块化应用程序
注意,对于前两种情况,jpackage 运行 jlink 为应用程序创建一个 Java 运行时,并将其捆绑到最终映像中。在第三种情况下,您提供一个绑定到映像中的自定义运行时。
jpackage 的输出是一个自包含的应用程序映像。该图像可以包括以下内容:
-
本机应用程序启动器(由 jpackage 生成)
-
应用程序资源(如 jar、icns、ico、png)
-
配置文件(如 plist、cfg、properties)
-
启动器的助手库
-
Java 运行时映像,包括执行 Java 字节码所需的文件
虽然 jpackage 是包含独立于平台的 java 和 javac 命令的同一个目录的一部分,但是 jpackage 根据您使用的平台略有不同。这是有意义的,因为 jpackage 的输出是特定于平台的。因此,jpackage 可以被认为是平台无关的 Java 字节码和平台相关的本机可执行文件的中间部分。
因为 jpackage 能够为当前平台构建可执行文件,所以您必须在您想要支持的所有平台上运行 jpackage。不支持将一个平台的 Java 应用程序交叉编译为其他平台的本机可执行文件。
套装程式使用状况
jpackage 的用法如下:
jpackage <options>
软件包选项
表格 10-1 到 10-7 显示了 jpackage 可以使用的不同选项。
表 10-7
jpackage–用于创建应用程序包的平台相关选项
| -win-目录选择器 | 添加一个对话框,使用户能够选择产品的安装目录。此选项仅在 Windows 上运行时可用。 | | -赢菜单 | 将应用程序添加到系统菜单。此选项仅在 Windows 上运行时可用。 | | 赢菜单组 | 放置此应用程序的开始菜单组。此选项仅在 Windows 上运行时可用。 | | -每用户安装成功率 | 请求以每个用户为基础执行安装。此选项仅在 Windows 上运行时可用。 | | -赢-捷径 | 为应用程序创建桌面快捷方式。此选项仅在 Windows 上运行时可用。 | | - win-upgrade-uuid | 与此包的升级相关的 UUID。此选项仅在 Windows 上运行时可用。 | | - linux-package-name | Linux 包的名称。默认为应用程序名称。只有在 Linux 上运行时,此选项才可用。 | | - linux-deb-maintainer | 的维护者。deb 包。只有在 Linux 上运行时,此选项才可用。 | | - linux-menu-group | 此应用程序所在的菜单组。只有在 Linux 上运行时,此选项才可用。 | | - linux-package-deps | 应用程序所需的包或功能。只有在 Linux 上运行时,此选项才可用。 | | -Linux-rpm-许可证类型 | RPM 的许可类型(“许可:”。规格)。只有在 Linux 上运行时,此选项才可用。 | | - linux-app-release | 释放转速值。DEB 控制文件的规范文件或 Debian 版本值。只有在 Linux 上运行时,此选项才可用。 | | - linux-app-category | RPM 的组值。DEB 控制文件的规范文件或部分值。只有在 Linux 上运行时,此选项才可用。 | | -Linux-快捷键 | 为应用程序创建快捷方式。只有在 Linux 上运行时,此选项才可用。 |表 10-6
jpackage–用于创建应用程序包的选项
| - app-image | 用于构建可安装包的预定义应用程序映像的位置(绝对路径或相对于当前目录)。请参见创建-应用程序-图像模式选项来创建应用程序图像。 | | -文件关联 | 包含键值对列表的属性文件的路径(绝对路径或相对于当前目录的路径)。关键字“扩展”、“mime 类型”、“图标”和“描述”可用于描述关联。该选项可以多次使用。 | | -安装目录 | macOS 或 Linux 上应用程序安装目录的绝对路径。应用程序安装位置的相对子路径,例如 Windows 上的“Program Files”或“AppData”。 | | -许可文件 | 许可证文件的路径(绝对路径或相对于当前目录的路径)。 | | -资源目录 | 覆盖 jpackage 资源的路径(绝对路径或相对于当前目录的路径)。图标、模板文件和 jpackage 的其他资源可以通过向该目录添加替换资源来覆盖。 | | -运行时图像 | 要安装的预定义运行时映像的路径(绝对路径或相对于当前目录的路径)。创建运行时安装程序时需要选项。 |表 10-5
jpackage–用于创建应用程序启动器的平台相关选项
| -win-控制台 | 为应用程序创建控制台启动器。应为需要控制台交互的应用程序指定。此选项仅在 Windows 上运行时可用。 | | - mac 软件包标识符 | 唯一标识 macOS 应用程序的标识符。默认为主类名。只能使用字母数字(A–Z、A–Z、0–9)、连字符(-)和句点(。)字符。此选项仅在 macOS 上运行时可用。 | | - mac-package-name | 出现在菜单栏中的应用程序的名称。这可以不同于应用程序名称。此名称必须少于 16 个字符,并且适合在菜单栏和应用程序信息窗口中显示。默认为应用程序名称。此选项仅在 macOS 上运行时可用。 | | - mac 包签名前缀 | 在对应用程序包进行签名时,该值会作为所有需要签名的组件的前缀,这些组件没有现有的包标识符。此选项仅在 macOS 上运行时可用。 | | - mac 符号 | 请求对包进行签名。此选项仅在 macOS 上运行时可用。 | | - mac 签名钥匙串 | 要使用的钥匙串的路径(绝对路径或相对于当前目录的路径)。如果没有指定,则使用标准的钥匙串。此选项仅在 macOS 上运行时可用。 | | - mac 签名密钥用户名 | Apple 签名身份名称中的团队名称部分。比如“开发者 ID 应用:”。此选项仅在 macOS 上运行时可用。 |表 10-4
jpackage–用于创建应用程序启动器的选项
| -添加-启动器 = | 启动程序的名称和包含键值对列表的属性文件的路径(绝对路径或相对于当前目录的路径)。可以使用键" module "、" add-modules "、" main-jar "、" main-class "、" arguments "、" java-options "、" app-version "、" icon "、" win-console "。这些选项被添加到或用于覆盖原始命令行选项,以构建一个额外的替代启动器。主应用程序启动器将通过命令行选项构建。可以使用此选项构建其他备选启动器,并且可以多次使用此选项来构建多个其他启动器。 | | -参数| 如果没有给启动程序提供命令行参数,则传递给主类的命令行参数。该选项可以多次使用。 | | - java 选项 | 传递给 Java 运行时的选项。该选项可以多次使用。 | | -主级 | 要执行的应用程序主类的限定名。只有在指定了- main-jar 的情况下,才能使用此选项。 | | -主罐
| 应用程序的主 JAR,包含主类(指定为相对于输入路径的路径)。可以指定- module 或- main-jar 选项,但不能同时指定两者。 | | -模块或-m [/
]| 应用程序的主模块(以及可选的主类)。该模块必须位于模块路径上。指定此选项时,主模块将在 Java 运行时映像中链接。可以指定- module 或- main-jar 选项,但不能同时指定两者。 |
表 10-3
包–用于创建应用程序映像的选项
| -图标 | 应用程序包图标的路径(绝对路径或相对于当前目录的路径)。 | | -输入或-i | 包含要打包的文件的输入目录的路径(绝对路径或相对于当前目录)。输入目录中的所有文件都将打包到应用程序映像中。 |表 10-2
包–用于创建运行时映像的选项
| -添加模块 [,...] | 要添加的以逗号(",")分隔的模块列表。这个模块列表连同主模块(如果指定的话)将作为- add-module 参数传递给 jlink。如果未指定,则只使用主模块(如果指定了- module)或默认的模块集(如果指定了- main-jar)。该选项可以多次使用。 | | -模块路径或-p ... | 每个路径要么是模块的目录,要么是模块化 jar 的路径,并且是当前目录的绝对路径或相对路径。对于多个路径,在 Linux 和 macOS 上用冒号(:)或分号(;)或使用- module-path 选项的多个实例。该选项可以多次使用。 | | - jlink 选项 | 要传递给 jlink 的以空格分隔的选项列表。如果未指定,则默认为“—strip-native-commands–strip-debug–no-man-pages–no-header-files”。该选项可以多次使用。 | | -运行时图像 | 将复制到应用程序映像中的预定义运行时映像的路径(绝对路径或相对于当前目录的路径)。如果没有指定- runtime-image,jpackage 将使用选项- strip-debug、- no-header-files、- no-man-pages 和- strip-native-commands 运行 jlink 来创建运行时映像。 |表 10-1
jpackage–通用选项
| @ | 从文件中读取选项和/或模式。该选项可以多次使用。 | | -键入或-t | 要创建的包的类型。有效值为{"app-image "、" exe "、" msi "、" rpm "、" deb "、" pkg "、" dmg"}。如果未指定此选项,将创建依赖于平台的默认类型。 | | -应用程序版本 | 应用程序和/或包的版本。 | | -版权 | 应用程序的版权。 | | -描述 | 应用程序的描述。 | | -救命或者-h | 将带有当前平台的每个有效选项的列表和描述的用法文本打印到输出流并退出。 | | -名称或-n | 应用程序和/或包的名称。 | | - dest 或-d | 放置生成的输出文件的路径(绝对路径或相对于当前目录的路径)。默认为当前工作目录。 | | -温度 | 用于创建临时文件的新目录或空目录的路径(绝对路径或相对于当前目录的路径)。如果指定,临时目录将不会在任务完成时删除,必须手动删除。如果未指定,将在任务完成时创建并删除一个临时目录。 | | -供应商 | 应用程序的供应商。 | | -冗长 | 启用详细输出。 | | -版本 | 将产品版本打印到输出流并退出。 |要求
jpackage 创建的映像与开发人员为本地平台创建的其他应用程序没有什么不同。因此,jpackage 也使用用于为特定操作系统生成本机应用程序的相同工具。
对于 Windows,为了生成原生包,开发人员需要安装:
- WiX 工具集,一个生成 exe 和 msi 安装程序的免费第三方工具
WiX 设置
从 https://wixtoolset.org/releases/
下载 WiX 工具集。当前版本是 3.11.2。下载完成后,使用安装程序进行处理,完成后,从命令行将其添加到 path 中
setx /M PATH "%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin"
样品
我们现在将展示几个示例,向您展示如何使用 jpackage。这些示例本身是非常简单的 JavaFX 应用程序,我们不会在本章中讨论它们的功能。
让我们从没有构建工具或插件的终端窗口开始使用 jpackage。因为根据运行的平台不同,使用 jpackage 的方式也会略有不同,所以我们区分了在 Windows、macOS 和 Linux 上使用 jpackage 的方式。
非模块化应用:示例 1
作为第一个例子,我们解释如何打包一个本身不是模块的 Java 应用程序。应用程序仍然使用 JavaFX 模块,但是应用程序本身没有特定的 module-info.java。
我们描述了如何将这个应用程序打包到 Windows、Mac 和 Linux 的安装程序中。我们遵循的模式对于每个平台都是相似的:
-
定义一些环境变量。
-
将 JavaFX 应用程序编译成 Java 字节码。
-
运行并测试应用程序。
-
创建一个包含应用程序的 jar 文件。
-
使用 jpackage 创建一个安装程序。
Windows 说明
以下是为非模块化应用程序创建安装程序的必要步骤,如下所示:
https://github.com/modernclientjava/mcj-samples/tree/master/ch10-packaging/Sample1
克隆示例,并从终端将 cd 复制到应用程序的根目录中。
这前四个步骤与常规的 Java 编译和运行没有什么不同。
- 导出这些环境变量:
set JAVA_HOME="C:\Users\<user>\Downloads\jdk-17"
set PATH_TO_FX="C:\Users\<user>\Downloads\javafx-sdk-17\lib"
set PATH_TO_FX_MODS="C:\Users\<user>\Downloads\javafx-jmods-17"
请注意,如果您将不同的 JDK 添加到 PATH 环境变量中,这将具有优先权。
-
编译您的应用程序并将 fxml 和 css 资源文件复制到路径
out\org\modernclients
: -
运行和测试:
dir /s /b src\*.java > sources.txt & javac --module-path %PATH_TO_FX% --add-modules javafx.controls,javafx.fxml -d out @sources.txt & del sources.txt
copy src\org\modernclients\scene.fxml out\org\modernclients\ & copy src\org\modernclients\styles.css out\org\modernclients\
- 创建一个罐子:
java --module-path %PATH_TO_FX% --add-modules javafx.controls,javafx.fxml -cp out org.modernclients.Main
- 创建安装程序。
mkdir libs
jar --create --file=libs\sample1.jar --main-class=org.modernclients.Main -C out .
在这一步,我们使用 jpackage 创建一个安装程序。我们在表 10-1 中展示了可以提供给 jpackage 的不同选项。在以下命令中,我们指定了许多选项:
%JAVA_HOME%\bin\jpackage --type exe -d installer -i libs --main-jar sample1.jar -n Sample1 --module-path %PATH_TO_FX_MODS% --add-modules javafx.controls,javafx.fxml --main-class org.modernclients.Main
结果,jpackage 创建了可以分发的Sample1-1.0.exe
(26 MB),只需要双击就可以安装应用程序(图 10-6 )。
图 10-6
示例 1 Windows installer
使用--verbose
运行 jpackage 工具会显示以下输出,这有助于确定 jpackage 如何构建安装程序,它在哪里存储默认资源,以及如何定制这些设置:
Running candle.exe
Running light.exe
Detected [light.exe] version [3.11.2.4516].
Detected [candle.exe] version [3.11.2.4516].
WiX 3.11.2.4516 detected. Enabling advanced cleanup action.
Using default package resource java48.ico [icon] (add Sample1.ico to the resource-dir to customize).
Using default package resource WinLauncher.template [Template for creating executable properties file] (add Sample1.properties to the resource-dir to customize).
MSI ProductCode: 6ad6fbff-52ef-3f2f-959a-a12e4c9b1f68.
MSI UpgradeCode: 4e3a7148-be2c-3a36-bc72-feb6033ea68f.
Using default package resource main.wxs [Main WiX project file] (add main.wxs to the resource-dir to customize).
Using default package resource overrides.wxi [Overrides WiX project file] (add overrides.wxi to the resource-dir to customize).
Preparing MSI config: C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\images\win-exe.image\Sample1-1.0.msi.
Generating MSI: C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\images\win-exe.image\Sample1-1.0.msi.
Running candle.exe in C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\images\win-msi.image\Sample1
Command:
candle.exe -nologo C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\config\main.wxs -ext WixUtilExtension -arch x64 [...]
Output:
main.wxs
Returned: 0
Running candle.exe in C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\images\win-msi.image\Sample1
Command:
candle.exe -nologo C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\config\bundle.wxf -ext WixUtilExtension -arch x64 [...]
Output:
bundle.wxf
Returned: 0
Running light.exe in C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\images\win-msi.image\Sample1
Command:
light.exe -nologo -spdb -ext WixUtilExtension […]
Output:
C:\Users\<user>\AppData\Local\Temp\jdk.jpackage13545744068176887418\config\main.wxs(53) : warning LGHT1076 : ICE61: This product should remove only older versions of itself. No Maximum version was detected for the current product. (JP_DOWNGRADABLE_FOUND)
Returned: 0
Generating EXE for installer to: C:\Users\<user>\Downloads\mcj-samples\ch10-packaging\Sample1\installer.
Using default package resource WinInstaller.template [Template for creating executable properties file] (add WinInstaller.properties to the resource-dir to customize).
Installer (.exe) saved to: C:\Users\<user>\Downloads\mcj-samples\ch10-packaging\Sample1\installer
Succeeded in building EXE Installer Package package
修改安装程序
我们可以向 jpackage 命令添加更多选项。例如,我们可以将应用程序添加到系统菜单,创建桌面快捷方式,让用户选择安装目录,并使用基于来自
https://hg.openjdk.java.net/duke/duke/raw-file/e71b60779736/3D/Duke%20Waving/openduke.png
运行以下 jpackage 命令构建定制的安装程序,该安装程序将创建如图 10-7 所示的应用程序:
图 10-7
带有自定义图标的应用程序
%JAVA_HOME%\bin\jpackage --type exe -d installer -i libs --main-jar sample1.jar -n Sample1 --module-path %PATH_TO_FX_MODS% --add-modules javafx.controls,javafx.fxml --main-class org.modernclients.Main --win-menu --win-shortcut --win-dir-chooser --icon assets\win\openduke.ico
苹果
这些是为非模块化应用程序创建安装程序的必要步骤,类似于示例 1:
- 导出这些环境变量:
https://github.com/modernclientjava/mcj-samples/tree/master/ch10-packaging/Sample1
- 编译您的应用程序并将 fxml 和 css 资源文件复制到路径
out/org/modernclients
:
export JAVA_HOME=/Users/<user>/Downloads/jdk-17.jdk/Contents/Home/
export PATH_TO_FX=/Users/<user>/Downloads/javafx-sdk-17/lib/
export PATH_TO_FX_MODS=/Users/<user>/Downloads/javafx-jmods-17/
- 运行和测试:
javac --module-path $PATH_TO_FX --add-modules javafx.controls,javafx.fxml -d out $(find src -name "*.java")
cp src/org/modernclients/scene.fxml src/org/modernclients/styles.css out/org/modernclients/
- 创建一个罐子:
java --module-path $PATH_TO_FX --add-modules javafx.controls,javafx.fxml -cp out org.modernclients.Main
- 创建安装程序:
mkdir libs
jar --create --file=libs/sample1.jar --main-class=org.modernclients.Main -C out .
$JAVA_HOME/bin/jpackage --type dmg -d installer -i libs --main-jar sample1.jar -n Sample1 --module-path $PATH_TO_FX_MODS --add-modules javafx.controls,javafx.fxml --main-class org.modernclients.Main
图 10-8
Sample1 macOS 安装程序
这构建了Sample1-1.0.dmg
(83 MB),如图 10-8 所示,你可以分发它。它只需要双击安装应用程序。
用--verbose
运行 jpackage 工具展示了 jpackage 如何构建安装程序以及它在哪里存储默认资源。当您想要确定 jpackage 存储默认资源的位置并定制这些设置时,请使用--verbose
选项,如以下示例输出所示:
Building DMG package for Sample1
Building PKG package for Sample1
"Adding modules: [javafx.controls, javafx.fxml] to runtime image."
jlink arguments: [--output /var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/img/image-8684840536936452979/Sample1.app/Contents/runtime/Contents/Home --module-path /Users/<user>/Downloads/javafx-jmods-17-ea-13:/Users/<user>/Downloads/jdk-17.jdk/Contents/Home/jmods --add-modules javafx.controls,javafx.fxml --strip-native-commands --strip-debug --no-man-pages --no-header-files]
Using default package resource GenericApp.icns [icon] (add Sample1.icns to the resource-dir to customize)
Preparing Info.plist: /var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/img/image-8684840536936452979/Sample1.app/Contents/Info.plist
Using default package resource Info-lite.plist.template [Application Info.plist] (add Info.plist to the resource-dir to customize)
Using default package resource Runtime-Info.plist.template [Java Runtime Info.plist] (add Runtime-Info.plist to the resource-dir to customize)
Using default package resource background_pkg.png [pkg background image] (add Sample1-background.png to the resource-dir to customize)
Preparing distribution.dist: /var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/config/distribution.dist
no default package resource [script to run after application image is populated] (add Sample1-post-image.sh to the resource-dir to customize)
Running [pkgbuild, --root, /var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/img/image-9477246125921380963, --install-location, /Applications, --analyze, -/var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/config/cpl.plist]
pkgbuild: Inferring bundle components from contents of /var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/img/image-9477246125921380963
pkgbuild: Writing new component property list to /var/folders/90/fcwm6f8s0d39jnv8vc0_rww00000gn/T/jdk.jpackage3310158689456557035/config/cpl.plist
Preparing package scripts
Using default package resource preinstall.template [PKG preinstall script] (add preinstall to the resource-dir to customize)
Using default package resource postinstall.template [PKG postinstall script] (add postinstall to the resource-dir to customize)
...
修改安装程序
我们还可以根据 https://hg.openjdk.java.net/duke/duke/file/e71b60779736/3D/Duke%20Waving/openduke.png
中的公爵图像添加一个自定义图标:
$JAVA_HOME/bin/jpackage --type dmg -d installer -i libs --main-jar sample1.jar -n Sample1 --module-path $PATH_TO_FX_MODS --add-modules javafx.controls,javafx.fxml --main-class org.modernclients.Main --icon assets/mac/openduke.icns
我们得到的结果如图 10-9 所示。
图 10-9
定制安装程序
Linux 操作系统
以下是为非模块化应用程序(如 Sample1:
https://github.com/modernclientjava/mcj-samples/tree/master/ch10-packaging/Sample1
以下是为基于 Debian 的发行版创建安装程序的说明:
-
导出这些环境变量:
-
编译您的应用程序并将 fxml 和 css 资源文件复制到路径
out/org/modernclients
:
export JAVA_HOME=/home/<user>/Downloads/jdk-17/
export PATH_TO_FX=/home/<user>/Downloads/javafx-sdk-17/lib/
export PATH_TO_FX_MODS=/home/<user>/Downloads/javafx-jmods-17/
- 运行和测试:
javac --module-path $PATH_TO_FX --add-modules javafx.controls,javafx.fxml -d out $(find src -name "*.java")
cp src/org/modernclients/scene.fxml src/org/modernclients/styles.css out/org/modernclients/
- 创建一个罐子:
java --module-path $PATH_TO_FX --add-modules javafx.controls,javafx.fxml -cp out org.modernclients.Main
- 创建安装程序:
mkdir libs
jar --create --file=libs/sample1.jar --main-class=org.modernclients.Main -C out .
$JAVA_HOME/bin/jpackage --type deb -d installer -i libs --main-jar sample1.jar -n Sample1 --module-path $PATH_TO_FX_MODS --add-modules javafx.controls,javafx.fxml --main-class org.modernclients.Main
该命令的结果是在名为“installer”的目录中创建一个名为sample1-1.0.deb
的文件用文件浏览器定位这个文件,显示 sample1-1.0.deb 是一个 Debian 包(图 10-10 )。
图 10-10
示例 1 Linux 安装程序
模块化应用:示例 2
我们的第二个应用程序是模块化应用程序。源代码可以在 https://github.com/modernclientjava/mcj-samples/tree/master/ch10-packaging/Sample2
找到。
它包含一个 module-info.java 文件,jpackage 工具可以处理这个文件来处理模块依赖关系。module-info 文件非常简单:它声明了对 javafx.controls 和 javafx.fxml 模块的依赖关系,并导出了模块 org.modernclients。
module-info.java 文件如下所示:
module modernclients {
requires javafx.controls;
requires javafx.fxml;
opens org.modernclients to javafx.fxml;
exports org.modernclients;
}
Windows 操作系统
以下是在 Windows 上为模块化应用程序创建安装程序的必要步骤:
-
导出这些环境变量:
-
编译您的应用程序并将 fxml 和 css 资源文件复制到路径
out\org\modernclients
:
set JAVA_HOME="C:\Users\<user>\Downloads\jdk-17"
set PATH_TO_FX="C:\Users\<user>\Downloads\javafx-sdk-17\lib"
set PATH_TO_FX_MODS="C:\Users\<user>\Downloads\javafx-jmods-17"
- 运行和测试:
dir /s /b src\*.java > sources.txt & javac --module-path %PATH_TO_FX% --add-modules javafx.controls,javafx.fxml -d mods\modernclients @sources.txt & del sources.txt
copy src\org\modernclients\scene.fxml mods\modernclients\org\modernclients\ & copy src\org\modernclients\styles.css mods\modernclients\org\modernclients\
- 使用 jlink 创建自定义图像:
java --module-path %PATH_TO_FX%;mods -m modernclients/org.modernclients.Main
- 运行并测试映像:
%JAVA_HOME%\bin\jlink --module-path %PATH_TO_FX_MODS%;mods
--add-modules modernclients --output image
- 创建安装程序:
image\bin\java -m modernclients/org.modernclients.Main
%JAVA_HOME%\bin\jpackage --type exe -d installer -n Sample2 -m modernclients/org.modernclients.Main --runtime-image image
因此,您将获得可以分发的Sample2-1.0.exe
(32 MB),并且它只需要双击来安装应用程序。
苹果
以下是在 macOS 上为模块化应用程序创建安装程序的必要步骤:
-
导出这些环境变量:
-
编译您的应用程序并将 fxml 和 css 资源文件复制到路径
out/org/modernclients
:
export JAVA_HOME=/Users/<user>/Downloads/jdk-17.jdk/Contents/Home/
export PATH_TO_FX=/Users/<user>/Downloads/javafx-sdk-17/lib/
export PATH_TO_FX_MODS=/Users/<user>/Downloads/javafx-jmods-17/
- 运行和测试:
javac --module-path $PATH_TO_FX -d mods/modernclients $(find src -name "*.java")
cp src/org/modernclients/scene.fxml src/org/modernclients/styles.css mods/modernclients/org/modernclients/
- 使用 jlink 创建自定义图像:
java --module-path $PATH_TO_FX:mods -m modernclients/org.modernclients.Main
- 运行并测试映像:
$JAVA_HOME/bin/jlink --module-path $PATH_TO_FX_MODS:mods --add-modules modernclients --output image
- 创建安装程序:
image/bin/java -m modernclients/org.modernclients.Main
$JAVA_HOME/bin/jpackage --type dmg -d installer -n Sample2 -m modernclients/org.modernclients.Main --runtime-image image
因此,您将获得可以分发的Sample2-1.0.dmg
(38.3 MB),并且只需双击即可安装应用程序。
Linux 操作系统
以下是在 Linux 上为模块化应用程序创建安装程序的必要步骤:
-
导出这些环境变量:
-
编译您的应用程序并将 fxml 和 css 资源文件复制到路径
out/org/modernclients
:
export JAVA_HOME=/home/<user>/Downloads/jdk-17/
export PATH_TO_FX=/home/<user>/Downloads/javafx-sdk-17/lib/
export PATH_TO_FX_MODS=/home/<user>/Downloads/javafx-jmods-17/
- 运行和测试:
javac --module-path $PATH_TO_FX -d mods/modernclients $(find src -name "*.java")
cp src/org/modernclients/scene.fxml src/org/modernclients/styles.css mods/modernclients/org/modernclients/
- 使用 jlink 创建自定义图像:
java --module-path $PATH_TO_FX:mods -m modernclients/org.modernclients.Main
- 运行并测试映像:
$JAVA_HOME/bin/jlink --module-path $PATH_TO_FX_MODS:mods --add-modules modernclients --output image
- 创建安装程序:
image/bin/java -m modernclients/org.modernclients.Main
$JAVA_HOME/bin/jpackage --type deb -d installer -n Sample2 -m modernclients/org.modernclients.Main --runtime-image image
因此,您将获得可以分发或安装的Sample2-1.0.deb
。
Gradle 项目
前面的示例解释了如何使用命令行 jpackage 工具。与大多数命令一样,在现有的构建工具中使用它们通常是有意义的,例如 Maven 或 Gradle。
虽然你可以创建一个任务添加到你的 build.gradle 文件中,并提供运行 jpackage 工具所需的选项,但是有一个插件可以帮你做到这一点:塞尔班·约尔达切的 org.beryx.jlink 插件(见 https://badass-jlink-plugin.beryx.org/
)。
以下是使用 Gradle 为模块化应用程序创建安装程序的必要步骤,如下所示:
- 编辑 build.gradle 并检查所需的 JDK 路径。请注意,我们提供的插件版本是出版物发布时的最新版本;您应该检查这些插件版本号的更新。
https://github.com/modernclientjava/mcj-samples/tree/master/ch10-packaging/Sample3
- 运行和测试:
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'org.beryx.jlink' version '2.24.1'
}
repositories {
mavenCentral()
}
javafx {
version = 17.0.1
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
application {
mainModule = "modernclients"
mainClass = "org.modernclients.Main"
}
jlink {
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
launcher {
name = 'sample3'
}
jpackage {
if (javafx.getPlatform().name() == "OSX") {
installerType = "dmg"
jpackageHome =
"/Users/<user>/Downloads/jdk-17.jdk/Contents/Home"
} else if (javafx.getPlatform().name() == "WINDOWS") {
installerType = "exe"
jpackageHome = "C:\\Users\\<user>\\Downloads\\jdk-17"
installerOptions = ['--win-menu', '--win-shortcut', '--win-dir-chooser']
} else if (javafx.getPlatform().name() == "LINUX") {
installerType = "deb"
jpackageHome = "/home/<user>/Downloads/jdk-17"
}
}
}
- 创建自定义图像:
./gradlew run (Mac OSX or Linux)
gradlew run (Windows)
- 运行并测试映像:
./gradlew jlink (Mac OSX or Linux)
gradlew jlink (Windows)
- 创建安装程序:
build/image/bin/sample3 (Mac OSX or Linux)
build\image\bin\sample3 (Windows)
./gradlew jpackage (Mac OSX or Linux)
gradlew jpackage (Windows)
因此,您将在 macOS 上获得sample3-1.0.dmg
(35.8 MB),在 Windows 上获得 sample3-1.0.exe(34.5 MB),或者在 Linux 上获得可以分发的 sample3-1.0-1_amd64.deb (33.8 MB),它只需要双击即可安装应用程序。
使用 GraalVM 的本机映像
jpackage 工具允许您为特定的操作系统构建本机应用程序。Java 运行时与应用程序捆绑在一起,当执行原生应用程序时,它将在内部使用 Java 运行时来执行字节码。通常,Java 运行时包含一个将 Java 字节码编译成本机代码的实时(JIT)编译器。
构建本机应用程序的另一个选择是将编译步骤移到构建时间。有了 GraalVM 的原生映像,Java 代码就提前编译好了(AOT)。这意味着 Java 字节码在被执行之前和被捆绑到应用程序之前被编译成本机代码。
因此,生成的二进制文件不再包含 Java 运行时。图 10-11 显示了这种情况。
图 10-11
本机映像开发流程
尽管 GraalVM 项目(“在任何地方更快地运行程序”)已经活跃了很多年,但它直到最近才成为一个产品。GraalVM 仍在发展,它的一部分与 OpenJDK 项目集成在一起,反之亦然。我们建议您定期关注 https://graalvm.org
网站以及 GitHub 网站上 https://github.com/oracle/graal
的开源代码。
虽然 GraalVM 提供了 AOT 编译器,可以将 Java 字节码翻译成给定平台的本机代码,但是要将程序代码链接成可执行文件,还需要更多的操作。幸运的是,有开源工具可以帮助开发人员实现这一点。GluonFX 插件(来自 gluonhq.com)允许开发人员基于现有的 Java 代码为 Linux、macOS 和 Windows 创建本机映像。
这个插件也为移动应用程序生成本地图像,我们将在下一章讨论。
我们现在将向您展示如何使用 GluonFX 插件通过 HelloFX 示例应用程序构建一个本机可执行文件。
平台要求
为了构建本机映像,您可以使用 JDK 11 或 JDK 11。我们将简要描述 Maven 和 Gradle 项目在 macOS、Linux 和 Windows 上的需求。
您可以为每个目标系统下载 JDK 11 或 JDK 11。例如,您可以从以下 URL 下载 AdoptOpenJDK(为目标平台选择合适的版本):
https://adoptopenjdk.net/releases.html
您可以从这个 URL 下载 Gluon GraalVM 版本(为目标平台选择合适的版本):
https://github.com/gluonhq/graal/releases/
macOS 的要求
要使用该插件在 macOS 平台上开发和部署本机应用程序,您需要一台装有 macOS 10.13.2 或更高版本以及 Xcode 11 或更高版本的 Mac,可从 Mac App Store 获得。下载并安装 Xcode 后,打开它接受许可条款。
下载并安装后,将JAVA_HOME
设置为 JDK,例如:
export JAVA_HOME=/Users/<user>/Downloads/jdk-11.0.11+9/Contents/Home
对 Linux 的要求
下载适用于 Linux 的 JDK 后,导出适用于 Linux 平台 JDK 的 JAVA_HOME 环境变量,例如:
export JAVA_HOME=/home/<user>/Downloads/jdk-11.0.11+9
对 Windows 的要求
下载用于 Windows 的 JDK 后,为 Windows 平台 JDK 设置 JAVA_HOME 环境变量,例如:
set JAVA_HOME=C:\path\to\ jdk-11.0.11+9 2
将 JAVA_HOME 添加到环境变量列表中(高级系统设置)。
除了 Java JDK,还需要微软 Visual Studio 2019。社区版就足够了,可以从
https://visualstudio.microsoft.com/downloads/
在安装过程中,请确保至少选择以下单个组件:
-
选择英语语言包。
-
C++/CLI 支持 v142 构建工具(v 14.25 或更高版本)。
-
MSVC v 142–VS 2019 c++ x64/x86 构建工具(v 14.25 或更高版本)。
-
Windows 通用 CRT SDK。
-
Windows 10 SDK (10.0.19041.0 或更高版本)。
注意,所有构建命令都必须在名为 VS 2019 x64 原生工具命令提示符的 Visual Studio 2019 命令提示符中执行。
《守则》
这个例子的代码在 GitHub 上:
https://github.com/gluonhq/gluon-samples/tree/master/HelloFX/src/main/java/hellofx
清单 10-1 显示了 HelloFX 主类,清单 10-2 显示了 styles.css 文件。
.label {
-fx-text-fill: blue;
}
Listing 10-2File styles.css
package hellofx;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HelloFX extends Application {
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
Label label = new Label("Hello, JavaFX " + javafxVersion + ",
running on Java " + javaVersion + ".");
ImageView imageView = new ImageView(
new Image(HelloFX.class.getResourceAsStream("openduke.png")));
imageView.setFitHeight(200);
imageView.setPreserveRatio(true);
VBox root = new VBox(30, imageView, label);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 640, 480);
scene.getStylesheets().add(
HelloFX.class.getResource("styles.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 10-1HelloFX.java
Maven 项目
如果您有一个 Java 或 JavaFX 项目,并且您使用 Maven 作为构建工具,那么您可以包含这个插件来开始创建本地应用程序。
该插件可在以下位置找到:
https://github.com/gluonhq/gluonfx-maven-plugin
清单 10-3 显示了一个 Maven 项目的 pom 文件。
<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>hello</groupId>
<artifactId>hellofx</artifactId>
<version> 1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hellofx</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<javafx.version>17.0.1</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version> 0.0.8</version>
<configuration>
<mainClass>hellofx.HelloFX</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId> gluonfx-maven-plugin</artifactId>
<version>1.0.7</version>
<configuration>
<mainClass>hellofx.HelloFX</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<pluginRepositories>
<pluginRepository>
<id>gluon-releases</id>
<url>
http://nexus.gluonhq.com/nexus/content/repositories/releases
</url>
</pluginRepository>
</pluginRepositories>
</project>
Listing 10-3pom.xml file
Gradle 项目
如果您有一个 Java 或 JavaFX 项目,并且您使用 Gradle 作为构建工具,那么您可以包含该插件来开始创建本地应用程序。
该插件可在以下位置找到:
https://github.com/gluonhq/gluonfx-gradle-plugin.
清单 10-4 显示了 build.gradle 文件,清单 10-5 显示了 gradle 项目的 settings.gradle 文件。
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'HelloFX'
Listing 10-5File settings.gradle
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.3'
}
repositories {
mavenCentral()
}
gluonfx {
}
javafx {
modules = [ "javafx.controls" ]
}
mainClassName = 'hellofx.HelloFX'
Listing 10-4File build.gradle
构建项目
第一步是将项目作为一个常规的 Java 项目来构建和运行(在一个用于本地开发的常规 JVM 上,例如 HotSpot)。
与格雷尔:
./gradlew clean build run (Mac OSX or Linux)
gradlew clean build run (Windows)
使用 Maven:
mvn clean javafx:run
结果如图 10-12 所示。
图 10-12
在 OpenJDK 16 上运行 HelloFX
我们现在将编译、打包和运行本机桌面应用程序。
编制
与 Gradle 一起运行:
./gradlew nativeCompile (Mac OS or Linux)
gradlew nativeCompile (Windows)
或者用 Maven:
mvn gluonfx:compile
您需要等到任务成功完成(根据您的计算机,可能需要 5 分钟或更长时间)。您将看到在这个过程中提供的反馈,比如清单 10-6 。
...
[INFO] ==================== COMPILE TASK ====================
[SUB] [hellofx.hellofx:13197] classlist: 1,810.51 ms, 0.96 GB
[SUB] [hellofx.hellofx:13197] (cap): 2,187.64 ms, 0.96 GB
[SUB] [hellofx.hellofx:13197] setup: 4,359.53 ms, 0.96 GB
[SUB] [hellofx.hellofx:13197] (clinit): 812.63 ms, 4.65 GB
[SUB] [hellofx.hellofx:13197] (typeflow): 19,802.32 ms, 4.65 GB
[SUB] [hellofx.hellofx:13197] (objects): 29,770.98 ms, 4.65 GB
[SUB] [hellofx.hellofx:13197] (features): 2,568.14 ms, 4.65 GB
[SUB] [hellofx.hellofx:13197] analysis: 54,581.85 ms, 4.65 GB
[SUB] [hellofx.hellofx:13197] universe: 1,677.48 ms, 4.65 GB
[SUB] [hellofx.hellofx:13197] (parse): 10,890.07 ms, 5.43 GB
[SUB] [hellofx.hellofx:13197] (inline): 10,567.77 ms, 6.10 GB
[SUB] [hellofx.hellofx:13197] (compile): 35,567.94 ms, 6.16 GB
[SUB] [hellofx.hellofx:13197] compile: 60,462.59 ms, 6.16 GB
[SUB] [hellofx.hellofx:13197] image: 7,202.38 ms, 6.16 GB
[SUB] [hellofx.hellofx:13197] write: 1,006.42 ms, 6.16 GB
[SUB] # Printing build artifacts to: hellofx.hellofx.build_artifacts.txt
[SUB] [hellofx.hellofx:13197] [total]: 131,599.90 ms, 6.16 GB
[INFO] BUILD SUCCESS
[INFO] Total time: 02:22 min
Listing 10-6Output during the native compilation phase
结果你会在target/gluonfx/{target-architecture}/gvm/tmp/SVM-
*** /
下看到 hellofx.hellofx.o (65.0 MB)或者 hellofx.hellofx.obj。
如果不是这样,请在target/gluonfx/{target-architecture}/gvm/log
下的日志文件中检查任何可能的故障。
环
既然应用程序的 Java 代码已经编译成本机代码,我们可以使用nativeLink
任务将生成的代码与所需的库和资源打包在一起。
与 Gradle 一起运行:
./gradlew nativeLink (Mac OSX or Linux)
gradlew nativeLink (Windows)
或者用 Maven:
mvn gluonfx:link
链接步骤在目标子目录target/gluonfx/{target-architecture}/HelloFX
(65.4 MB)或 windows 的 target \ Glu onfx \ x86 _ 64-Windows \ hello FX . exe 中生成可执行文件。图 10-13 显示了 macOS 文件系统中的可执行文件。
图 10-13
HelloFX 可执行文件
奔跑
最后,您可以使用 Gradle 运行它:
./gradlew nativeRun (Mac OS or Linux)
gradlew nativeRun (Windows)
或者用 Maven:
mvn gluonfx:run
您应该得到如图 10-14 所示的输出。
图 10-14
运行 HelloFX 的输出
请注意,您可以将这个本机应用程序分发到任何具有匹配架构(macOS、Linux 或 Windows)的机器上,并像任何其他常规应用程序一样直接运行它。
结论
将应用程序代码与所有必需的依赖项、Java 运行时和资源打包在一起,在桌面、移动和嵌入式设备上越来越流行。
历史上的缺点,包括较大的尺寸和较长的下载时间,由于带宽和存储的改善而变得不那么重要。
将一个应用程序打包到一个自包含的包中的优势意味着减少了最终用户的麻烦,他们可以使用与其他应用程序相似的安装方法。
JavaFX 应用程序是常规的 Java 应用程序。因此,对于 JavaFX 应用程序,您也可以使用现有的打包工具,比如 jpackage、jlink 和 Graal Native Image。
由于这些工具发展迅速,我们建议您密切关注 GitHub 资源库 https://github.com/gluonhq/gluon-samples
中的示例,因为它们将会更新到最新版本。
十一、iOS 和 Android 的原生移动应用
由何塞·佩雷达和约翰·沃斯撰写
Java 最初是作为嵌入式设备的编程语言出现的。20 世纪 90 年代初,Sun Microsystems 内部的一个团队为一组下一代硬件产品开发了一个软件堆栈。
这些硬件产品的原型被称为 Star7,看起来像是介于手机和平板电脑之间的东西。这些设备的软件最初代号为 Oak,也就是我们现在所知的 Java。
硬件和软件的发展走上了截然不同的道路。Java 软件因使用小程序制作网页动画而大受欢迎,后来成为开发企业和云应用程序的首选语言。
手机和设备制造商、电信运营商和内容提供商之间的复杂互动决定了硬件的发展。商业模式非常分散,在很长一段时间里,开发者一般都不容易接触到移动设备。
随着应用商店的日益流行,开发人员为移动平台编写应用变得更加容易。此外,现在大多数手机要么基于 Android,要么基于 iOS。这减少了两个主要平台的碎片。
由于 Java 最初是为移动和嵌入式系统开发的,所以创建 Java 应用程序并在移动设备上运行它们是非常有意义的,特别是因为 Java 领域最近的发展使得向移动设备交付高性能应用程序变得很容易。
为什么在移动设备上使用 JavaFX
在当今的数字基础设施中,网页通常用于以简单的方式呈现来自后端的信息。
台式机和笔记本电脑系统广泛用于处理需要用户交互、数据同步、高性能渲染和云集成的应用程序。因此,它们补充了 web 应用程序。
IT 领域的发展导致移动设备的重要性日益增加,例如手机和平板电脑。因此,IT 后端现在必须服务于三个不同的渠道:基于 web 的前端、桌面应用程序和移动应用程序。如图 11-1 所示。
图 11-1
服务于不同渠道的业务后端
JavaFX 是 Java,因此支持一次编写,随处运行的范例,非常适合创建在台式机和笔记本电脑上运行的应用程序,但也可以在手机和平板电脑上运行。
在前一章中,我们展示了如何将 JavaFX 应用程序转换成客户机本地的应用程序。当我们谈论“本地”时,我们指的是两个事物的组合:
-
代码在设备的本地方法中执行。交付给手机的应用程序直接执行机器代码,而不是在设备上解释或翻译。这允许快速执行。
-
没有中间呈现引擎(例如,浏览器),JavaFX 控件直接使用客户端上可用的硬件加速图形引擎来呈现。
在本章中,我们将解释如何在移动设备上部署 JavaFX 应用程序,从而利用在这些设备上使用的硬件加速的本机渲染。
移动应用的不同方法
为移动设备创建应用程序有多种方法,它们可以分为多种类别。任何分类都是人为的,所以我们在这里使用的只是一种可能性。
我们考虑三种不同的方法,如图 11-2 所示:
图 11-2
移动应用的三种可能方式
-
基于网络的移动应用
-
使用硬件加速本地渲染的应用
-
使用特定于操作系统的本机控件的应用程序
这些选项各有优势,三个选项都有有效的用例。
特定于操作系统的本机控件
使用特定于操作系统的本机控件,如方法 3 中的情况,可以实现与本机操作系统的真正平滑集成。在这种情况下,本地控件(例如,按钮、标签、列表等。)用于呈现用户界面。对于最终用户来说,这是很方便的,因为他们认识到他们在其他应用程序中也使用的典型 UI 组件。
这种方法需要与目标操作系统相关的非常特殊的技能。由于大多数移动应用程序同时面向 Android 和 iOS,并且这些平台都有自己的原生用户界面方法,因此遵循这种方法的应用程序通常由不同的团队创建:一个用于 Android,一个用于 iOS。
此外,特定于操作系统的本机控件会发生快速变化。虽然许多最终用户喜欢这些环境中的快速创新,但对于软件开发人员来说,这往往是一个问题,因为他们必须经常升级他们的原生应用程序,否则他们可能会过时。
移动网站
最简单的方法通常是简单地创建一个移动友好的网站,并在移动设备上可用的移动浏览器中呈现——可选地与应用程序图标集成,以便用户可以更容易地启动应用程序。
理论上,用于在桌面浏览器中呈现的同一网站可以在移动设备上呈现。然而,这些网站通常是为大屏幕创建的,并且使用鼠标控制来操作。使用鼠标点击按钮比在触摸设备上触摸相同的按钮更容易。一般来说,网络体验与移动体验有很大不同。习惯在移动设备上使用原生应用的用户经常对网站感到失望,这损害了品牌。
设备本机呈现
JavaFX 方法就属于这一类。JavaFX 有自己的一套控件,开发者可以轻松创建自己的控件。JavaFX 的渲染是在目标平台的硬件加速驱动程序之上完成的。目前,用于 iOS 的 JavaFX 和用于 Android 的 JavaFX 的渲染管道都使用 OpenGL,使用与 macOS 和 Linux 上的渲染相同的代码。OpenGL 是一个非常成熟和稳定的协议,iOS 或 Android 的原生 UI 控件的变化并不影响 OpenGL 的开发。事实上,许多游戏开发者也在使用原生 OpenGL,他们希望在移动设备上实现最大的性能和灵活性。
与使用 web 浏览器进行渲染相比,JavaFX 渲染更加“本机”,因为它不需要中间 web 浏览器,而是直接针对用于渲染本机 iOS 或本机 Android 控件的相同本机驱动程序。
就其核心而言,iOS 和大多数 Android 设备都是采用 AArch64 处理器的系统,运行某种 Linux。这些系统,以及 OpenGL,在业界被广泛使用,而不是由一个移动设备制造商控制。因此,它们提供了一个稳定的基础,避免了供应商锁定。
特定于应用的 JavaFX 堆栈如图 11-3 所示。
图 11-3
JavaFX 堆栈
你好,iOS 和 Android 上的 JavaFX
虽然 Android 和 iOS 是非常不同的系统,但 Java 开发人员的体验将非常相似。因此,虽然我们在这里只讨论 iOS,但是同样的原理和工具也适用于 Android。
GluonFX 插件降低复杂性
将应用程序部署到移动设备比在本地开发机器(笔记本电脑或台式机)上部署应用程序稍微复杂一些。这主要有两个原因:
-
当编译一个必须在与我们用来编译的系统相同的系统上执行的应用程序时,我们可以利用开发系统的现有工具链,包括编译器和链接器。然而,在为另一个系统编译应用程序时,我们需要考虑操作系统和该系统的体系结构。这通常被称为交叉编译,被认为更复杂。
-
移动应用商店的运营商 Google 和 Apple 对于创建移动应用都有自己的附加要求,例如,与签名和最终可执行包的结构相关的要求。
Java 普遍成功的原因之一是庞大的生态系统。不同的公司和个人提供了一套工具和库,使得开发人员更容易从事他们的项目。
对于移动设备上的 JavaFX,Gluon 创建了许多插件,使得 Java 开发人员可以更容易地在移动设备上部署 Java 应用程序。这些插件处理交叉编译的复杂性和来自各自应用商店的特定需求。
这些插件目前可用于 Maven 和 Gradle 项目。由于流行的 Java IDEs 对 Maven 和 Gradle 有一流的支持,所以很容易使用现有的 ide 来创建和维护移动 JavaFX 应用程序。
在我们的项目中使用 Maven 或 Gradle 构建工具,我们可以使用 GluonFX 插件在我们的本地(开发)系统上运行项目(依赖于 JavaFX 插件),还可以创建一个可以部署到目标平台的本地映像。
开发流程
虽然至少在理论上,可以创建一个移动应用程序,并且只在移动设备上测试/运行它,但是强烈建议首先在桌面上工作。
典型的部署周期包含许多步骤:
-
写点代码。
-
编译代码。
-
运行代码。
-
测试输出和行为是否符合预期。
这些步骤经常需要重复,导致给定项目的许多部署周期。
应该清楚的是,移动设备上的部署周期比桌面设备上的部署周期花费更多的时间。尤其是对移动设备来说,编译代码需要更多的时间。因此,最好使用您的台式机或笔记本电脑开发系统进行部署。我们将在下面描述的工具使您能够在移动和桌面设备上使用完全相同的代码,并且行为也是相似的。
当然,移动体验仍然是不同的,这只能在移动设备上进行真正的测试。例如,旋转、收缩和缩放等手势必须在特定的移动设备上进行微调,以便尽可能直观。
典型的移动应用开发流程如图 11-4 所示。
图 11-4
移动应用的开发流程
在参考图中,大部分开发是在桌面上完成的。应用程序得到了改进,包括业务逻辑部分和 UI 部分。在这个阶段,像NullPointerException
s、错误的值或不正确的 UI 元素这样的问题被修复。
在给定的时刻,应用程序在桌面上按预期工作。业务逻辑符合需求,UI 遵循设计。此时,应用程序被部署在移动设备上并经过测试。作为这些测试的结果,在桌面或移动设备上执行新的周期。例如,如果在移动设备上的测试导致一个隐藏的问题浮出水面,建议返回到桌面周期并添加一个失败的测试。这个问题可以在桌面上解决。
另一方面,如果检测到特定于移动设备的问题(例如,缩放太快),则该问题可以在移动设备上的开发周期中直接修复。
《守则》
我们将向您展示一个非常简单的 HelloFX 应用程序,它可以在桌面和 iPhone 或 Android 设备上运行。这个例子可以在这里找到(Gradle 和 Maven):
https://github.com/modernclientjava/mcj-samples/tree/master/ch11-Mobile/Gradle/HelloFX
https://github.com/modernclientjava/mcj-samples/tree/master/ch11-Mobile/Maven/HelloFX
清单 11-1 显示了 HelloFX 主类,清单 11-2 显示了 styles.css 文件。
.label {
-fx-text-fill: blue;
}
Listing 11-2File styles.css
package hellofx;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HelloFX extends Application {
public void start(Stage stage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
Label label = new Label("Hello, JavaFX " + javafxVersion +
", running on Java " + javaVersion + ".");
ImageView imageView = new ImageView(
new Image(HelloFX.class.getResourceAsStream("openduke.png")));
imageView.setFitHeight(200);
imageView.setPreserveRatio(true);
VBox root = new VBox(30, imageView, label);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 640, 480);
scene.getStylesheets().add(
HelloFX.class.getResource("styles.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Listing 11-1HelloFX.java file
清单 11-3 显示了 build.gradle 文件,清单 11-4 显示了 gradle 项目的 settings.gradle 文件。
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'HelloFX'
Listing 11-4File settings.gradle
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.3'
}
repositories {
mavenCentral()
}
dependencies {
}
gluonfx {
target = 'host'
if (project.hasProperty('target')) {
target = project.getProperty('target')
}
}
javafx {
version = "17.0.1"
modules = [ "javafx.controls" ]
}
mainClassName = 'hellofx.HelloFX'
Listing 11-3File build.gradle
build.gradle
文件显示,除了常规的应用程序插件,我们还使用了两个特殊的插件:
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.3'
javafxplugin
是开发 JavaFX 应用程序和处理 JavaFX 模块和依赖关系的通用插件。这是您通常用于开发 JavaFX 应用程序的插件。
gluonfx-gradle-plugin
是 Gluon 的插件,能够为 iOS 和 Android 设备交叉编译代码。
为了使用这些插件,我们必须告诉 Gradle 在哪里搜索插件,这些插件位于通用的 Gradle 插件门户中。这解释了清单 11-4 中显示的settings.gradle
文件。
与 Gradle 的构建文件类似,您可以使用 pom 文件来声明如何使用 Maven 构建应用程序。
清单 11-5 显示了一个 Maven 项目的等价 pom 文件。
<?xml version="1.0" encoding="UTF-8"?>
<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>hello</groupId>
<artifactId>hellofx</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hellofx</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>11</maven.compiler.release>
<javafx.version>17.0.1</javafx.version>
<mainClassName>hellofx.HelloFX</mainClassName>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>${mainClassName}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>1.0.7</version>
<configuration>
<target>${gluonfx.target}</target>
<mainClass>${mainClassName}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>ios</id>
<properties>
<gluonfx.target>ios</gluonfx.target>
</properties>
</profile>
<profile>
<id>android</id>
<properties>
<gluonfx.target>android</gluonfx.target>
</properties>
</profile>
</profiles>
</project>
Listing 11-5File pom.xml
ios
要求
要使用该插件在 iOS 平台上开发和部署本机应用程序,您需要一台装有 macOS 10.15.6 或更高版本以及 Xcode 11 或更高版本的 Mac,可从 Mac App Store 获得。下载并安装 Xcode 后,打开它接受许可条款。
或者,在没有 Mac 的情况下,GitHub Actions 可以用于远程构建和部署(参见 https://docs.gluonhq.com/#platforms_ios_github_actions
)。
JavaFX 应用程序可以在 JVM 上运行,不需要任何额外的要求,因此任何 JDK 11+都应该足够了。然而,要创建本地映像并部署到移动设备,您将需要 GraalVM。胶子建造版本的最新版本可以在 https://github.com/gluonhq/graal/releases/latest
找到。选择 macOS 的 Darwin 版本。
一旦下载并安装,你需要设置GRAALVM_HOME
指向它,就像
export GRAALVM_HOME=
/path/to/graalvm-svm-darwin-gluon-21.2.0-dev/Contents/Home
export JAVA_HOME=$GRAALVM_HOME
最后,如果你想通过 App Store 测试和发布你的应用,你需要加入苹果开发者计划。但是,如果你只想在你自己的 iOS 设备上测试,你可以使用免费供应。
按照此链接中的说明获取有效的预置描述文件和有效的签名身份: https://docs.gluonhq.com/client/#_ios_deployment
。
构建项目
第一步是将项目作为一个常规的 Java 项目来构建和运行(在一个用于本地开发的常规 JVM 上)。
与格雷尔:
./gradlew clean run
使用 Maven:
mvn clean gluonfx:run
结果如图 11-5 所示。
图 11-5
在 OpenJDK 11.0.2 上运行 HelloFX
这些指令将编译应用程序及其依赖项,并使用系统的默认 Java 虚拟机运行生成的类。这与任何其他常规 Java 开发非常相似。这里不涉及交叉编译。
正如我们之前所说的,使用常规虚拟机验证应用程序在您的开发系统上正常工作是非常重要的。部署到移动设备时将执行的 AOT 编译需要很长时间,因此只有当项目准备好进入这个阶段时才应该调用它。
编制
典型的 Gradle run
任务将检查应用程序是否已编译;否则,它将重新编译所需的类。
对于在移动设备上运行的应用程序,编译任务必须手动调用,因为这可能需要很长时间。这是为了允许开发者修改与 Java 文件无关的东西(但是,例如,与应用程序图标无关),而不必每次都经历漫长的编译阶段。
Gradle 和 Maven 都可以进行编译。
与 Gradle 一起运行:
./gradlew -Ptarget=ios nativeCompile
与 Maven 一起运行:
mvn -Pios gluonfx:compile
然后等待一段时间(取决于您的机器,可能需要 5 分钟或更长时间),直到任务成功完成。如果您检查终端,您可以看到过程中提供的反馈,如清单 11-6 所示。
==================== COMPILE TASK ====================
We will now compile your code for arm64-apple-ios. This may take some time.
[SUB] [hellofx.hellofx:19668] classlist: 2,087.16 ms, 0.96 GB
[SUB] [hellofx.hellofx:19668] (cap): 206.66 ms, 0.96 GB
[SUB] [hellofx.hellofx:19668] setup: 2,126.59 ms, 0.96 GB
[SUB] [hellofx.hellofx:19668] (clinit): 850.75 ms, 5.56 GB
[SUB] [hellofx.hellofx:19668] (typeflow): 36,924.18 ms, 5.56 GB
[SUB] [hellofx.hellofx:19668] (objects): 29,823.15 ms, 5.56 GB
[SUB] [hellofx.hellofx:19668] (features): 2,873.60 ms, 5.56 GB
[SUB] [hellofx.hellofx:19668] analysis: 72,256.12 ms, 5.56 GB
[SUB] [hellofx.hellofx:19668] universe: 2,483.79 ms, 5.66 GB
[SUB] [hellofx.hellofx:19668] (parse): 3,013.34 ms, 5.66 GB
[SUB] [hellofx.hellofx:19668] (inline): 12,337.56 ms, 7.09 GB
[SUB] [hellofx.hellofx:19668] (compile): 41,955.76 ms, 6.97 GB
[SUB] [hellofx.hellofx:19668] (bitcode): 4,023.85 ms, 6.97 GB
[SUB] [hellofx.hellofx:19668] (prelink): 8,245.57 ms, 6.97 GB
[SUB] [hellofx.hellofx:19668] (llvm): 118,298.09 ms, 6.97 GB
[SUB] [hellofx.hellofx:19668] (postlink): 12,859.86 ms, 6.97 GB
[SUB] [hellofx.hellofx:19668] compile: 201,311.01 ms, 6.97 GB
[SUB] [hellofx.hellofx:19668] image: 11,973.28 ms, 7.05 GB
[SUB] [hellofx.hellofx:19668] write: 2,908.47 ms, 7.05 GB
[SUB] [hellofx.hellofx:19668] [total]: 295,742.00 ms, 7.05 GB
Listing 11-6Output during the native compilation phase for iOS
结果在target/gluonfx/arm64-ios/gvm/tmp/SVM-16
*** /hellofx.hellofx.o
下可以找到 54.6 MB 的 hellofx.hellofx.o。
如果不是这样,请在target/gluonfx/arm64-ios/gvm/log
下的日志文件中检查任何可能的故障。
接下来的步骤会更快,但它们需要有效的签名身份和有效的预置描述文件,以便您在应用程序部署到设备之前对其进行签名。
链接和包
既然应用程序的 Java 代码已经编译成本机代码,我们就可以将生成的代码与所需的库和资源打包在一起,对应用程序进行签名,并执行更多特定于 iOS 的任务。
插件在nativeLink
任务中结合了这种打包。
与 Gradle 一起运行:
./gradlew -Ptarget=ios nativeLink
与 Maven 一起运行:
mvn -Pios gluonfx:link
它产生 target/Glu onfx/arm 64-IOs/hello FX . app(136.3 MB)。
现在,如果你想构建一个可以提交到 App Store 的 IPA 文件(见 https://docs.gluonhq.com/#platforms_ios_distribution
,可以运行
./gradlew -Ptarget=ios nativePackage
或者与 Maven 一起运行:
mvn -Pios gluonfx:package
奔跑
恭喜你!您的手机应用程序现已准备就绪!您可以将此应用程序部署到您的手机上,如下所述。
插入你的 iOS 设备,运行 Gradle:
./gradlew -Ptarget=ios nativeRun
或者与 Maven 一起运行:
mvn -Pios gluonfx:nativerun
请注意,您需要解锁您的设备。一旦安装,它将启动(图 11-6 )。
图 11-6
iOS 上的 HelloFX 应用
机器人
要求
要使用该插件在 Android 上开发和部署本地应用程序,您需要一台 Linux 机器。或者,你可以从 Windows PC 上使用 WSL2 ( https://docs.microsoft.com/en-us/windows/wsl/install-win10
),也可以使用 GitHub 动作远程完成(参见 https://docs.gluonhq.com/#platforms_android_github_actions
)。
JavaFX 应用程序可以在 JVM 上运行,不需要任何额外的要求,因此任何 JDK 11+都应该足够了。然而,要创建本地映像并部署到移动设备,您将需要 GraalVM。胶子建造版本的最新版本可以在 https://github.com/gluonhq/graal/releases/latest
找到。选择 Linux 版本。
一旦下载并安装,你需要设置GRAALVM_HOME
指向它,就像
export GRAALVM_HOME=
/path/to/graalvm-svm-linux-gluon-21.2.0-dev/
export JAVA_HOME=$GRAALVM_HOME
Android SDK 和 NDK 需要为 Android 平台构建应用程序。两者都将被 GluonFX 插件自动下载并配置所需的包。
编制
Gradle 和 Maven 都可以进行编译。
与 Gradle 一起运行:
./gradlew -Ptarget=android nativeCompile
与 Maven 一起运行:
mvn -Pandroid gluonfx:compile
然后等待一段时间(取决于您的机器,可能需要 3 分钟或更长时间),直到任务成功完成。如果您检查终端,您可以看到过程中提供的反馈,如清单 11-7 所示。
==================== COMPILE TASK ====================
We will now compile your code for aarch64-linux-android. This may take some time.
[SUB] Warning: Ignoring server-mode native-image argument --no-server.
[SUB] [hellofx.hellofx:4176] classlist: 1,692.66 ms, 0.96 GB
[SUB] [hellofx.hellofx:4176] (cap): 222.33 ms, 0.96 GB
[SUB] [hellofx.hellofx:4176] setup: 2,350.95 ms, 0.96 GB
[SUB] [hellofx.hellofx:4176] (clinit): 1,055.23 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] (typeflow): 26,771.58 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] (objects): 30,115.69 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] (features): 2,812.99 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] analysis: 62,782.08 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] universe: 2,307.81 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] (parse): 2,497.79 ms, 5.45 GB
[SUB] [hellofx.hellofx:4176] (inline): 3,905.41 ms, 5.66 GB
[SUB] [hellofx.hellofx:4176] (compile): 46,861.15 ms, 6.27 GB
[SUB] [hellofx.hellofx:4176] compile: 56,083.83 ms, 6.27 GB
[SUB] [hellofx.hellofx:4176] image: 9,097.73 ms, 5.37 GB
[SUB] [hellofx.hellofx:4176] write: 558.92 ms, 5.37 GB
[SUB] [hellofx.hellofx:4176] [total]: 135,607.09 ms, 5.37 GB
Listing 11-7Output during the native compilation phase for Android
结果在target/gluonfx/aarch64-android/gvm/tmp/SVM-16
*** /hellofx.hellofx.o
下可以找到 84.7 MB 的 hellofx.hellofx.o。
如果不是这样,请在target/gluonfx/aarch64-android/gvm/log
下的日志文件中检查任何可能的故障。
链接和包
既然应用程序的 Java 代码已经编译成本机代码,我们就可以将生成的代码与所需的库和资源链接并打包。
插件在任务nativeLink
和nativePackage
中结合了这种打包。
与 Gradle 一起运行:
./gradlew -Ptarget=android nativeLink nativePackage
与 Maven 一起运行:
mvn -Pandroid gluonfx:link gluonfx:package
它产生target/gluonfx/aarch64-android/gvm/hellofx.apk
(28.1 MB)。这个文件可以提交到 Google Play,前提是它已经被签署发布(见 https://docs.gluonhq.com/#platforms_android_distribution
)。
奔跑
您现在可以将这个 apk 部署到您的 Android 手机上,如下所述。
插入您的 Android 设备,并运行 Gradle:
./gradlew -Ptarget=android nativeInstall nativeRun
或者与 Maven 一起运行:
mvn -Pandroid gluonfx:install gluonfx:nativerun
一旦安装,它将启动(图 11-7 )。
图 11-7
Android 上的 HelloFX 应用程序
它是如何工作的?
作为一名普通的 Java 开发人员,尤其是 JavaFX 开发人员,您可以专注于 Java APIs,并实现使您的应用程序工作所需的代码。Java 平台本身将确保您的 Java 应用程序被翻译成 Java 字节码,该字节码不依赖于操作系统(例如,Windows、macOS、Linux 变体、iOS)或处理器(例如,ARM 64、Intel x86-64、ARMv6hf)。
典型的 Java 方法是,下一步,在本地系统上执行 Java 字节码,是使用在目标系统上运行的 Java 虚拟机(JVM)来实现的。在这种情况下,JVM 使用特定于目标的本机指令解释字节码并执行它。大多数 JVM 还包含一个实时(JIT)编译器,可以将常用的 Java 方法动态转换为本机代码。一般来说,本机代码比解释代码运行得快得多;因此,Java 应用程序的性能通常会在运行一段时间后得到提高。将 Java 字节码编译成本机代码需要一些时间,而且在此期间应用程序正在运行,因此不会立即达到最佳性能。
在前一章中,我们介绍了用于创建基于 JavaFX 应用程序的本机包的 Graal 本机映像工具。前一章中讨论的优点同样适用于移动设备,而且在移动设备上使用这种方法还有另外一个原因。苹果不允许在 iOS 设备上运行时生成动态代码。因此,典型的 Java 方法,即首先解释代码,然后(在运行时)优化代码,在 iOS 设备上是不允许的。
仅在运行时使用解释器运行移动 Java 应用程序是可能的,但是解释器比执行本地代码慢得多。
最重要的是,一个 JVM 安装可以处理大量应用程序的(旧的)服务器端方法在移动设备上并不常见。移动应用程序是自包含的应用程序,捆绑了它们所有的依赖关系——除了本机操作系统提供的一小组 API。在运行时下载额外的库或组件来满足特定应用程序所需的依赖是绝对不行的。
上一节演示的 Gluon 客户端插件包含将 Java 应用程序转换成字节码所需的工具;调用 Graal 本机映像工具将字节码转换为本机代码,包括所需的 VM 功能;并将结果链接到可部署到移动设备的可执行文件中。
这示意性地显示在图 11-8 中。
图 11-8
本机应用程序工作流
由 Gluon 客户端工具获得的结果原生映像在概念上与使用特定于 OS 的工具(例如,用于创建 iOS 应用的 Xcode 和用于创建 Android 应用的 Android Studio)创建的原生映像没有区别。开发人员仍然需要将这个图像上传到应用程序商店,从而记录应用程序、提供屏幕截图等等。
使用插件选项
默认情况下,插件将使用最佳配置将您的应用程序部署到移动设备上。插件和 GraalVM 中的不同组件分析您的代码及其依赖关系,以决定是否包含原生库、使用反射和 JNI 等等。预计这些分析工具将随着时间的推移而改进。
因为目前不可能用分析工具覆盖所有的边缘情况,所以插件允许开发者设置配置特定的设置(例如,用于反射的附加类,包括本地符号等)。)
联邦主义者
将添加到已包含的默认捆绑包列表中的附加完全限定捆绑包资源列表
com/sun/javafx/scene/control/skin/resources/controls
com.sun.javafx.tk.quantum.QuantumMessagesBundle
例如,如果您使用一个用于内部化目的的资源包,比如src/resources/hellofx/hello.properties
(和hello_EN.properties
等等),您将需要使用 Gradle,
bundlesList = ["hellofx.hello"]
或者使用 Maven
<bundlesList>
<list>hellofx.hello</list>
</bundlesList>
资源列表
将添加到默认资源列表中的附加资源模式或扩展的列表,默认资源列表已经包括
png, jpg, jpeg, gif, bmp, ttf, raw
xml, fxml, css, gls, json, dat,
license, frag, vert, obj
例如,如果你正在使用一个属性文件(没有包含在资源包中),比如src/resources/hellofx/logging.properties
,你将需要使用 Gradle,
resourcesList = ["properties"]
或者使用 Maven
<resourcesList>
<list>properties</list>
</resourcesList>
反射列表
将添加到默认反射列表中的附加完全限定类的列表,默认反射列表已经包含了大多数 JavaFX 类。
当前列表被添加到下的文件中
{build/target}/gluonfx/gvm/reflectionconfig-$target.json
金利斯特
将添加到默认 JNI 列表中的附加完全限定类的列表,该列表已经包含了大多数 JavaFX 类。
当前列表被添加到下的文件中
{build/target}/gluonfx/gvm/jniconfig-$target.json
运行代理任务/目标
作为反射和 JNI 列表的替代,runAgent 任务在桌面上运行项目,与javafx-maven-plugin
、GraalVM 的 JVM (HotSpot)和native-image-agent
相结合来记录 Java 应用程序的行为。它为反射、JNI、资源、代理和序列化生成配置文件,这些文件将由本机映像生成使用。
如果需要,这个目标应该在其他目标之前执行,并且需要用户干预来发现所有可到达的类,通过运行 Gradle:
./gradlew runAgent
或者与 Maven 一起运行:
mvn gluonfx:runagent
创建真正的移动应用
虽然为桌面创建的 JavaFX 应用程序可以在移动设备上运行,但在许多情况下,您可以通过调整应用程序以适应移动应用来改善用户体验。这样做,你最终得到的是特定于移动设备的代码,而不是特定于桌面设备的代码,你可能会问,与用本地语言进行移动开发相比,这有什么优势。不过,这有一些很好的理由:
-
您的大部分应用程序代码仍然可以共享。所有的业务逻辑,还有很大一部分 UI 代码,都可以在移动端和桌面端共享。
-
有了合适的框架(例如,Glisten),许多现有的 JavaFX 控件都可以进行移动应用。在这种情况下,使用完全相同的 JavaFX 代码。
-
对于移动和桌面之间完全不同的 UI 组件,至少代码都是用 Java 编写的,可以由相同的开发人员编写,由相同的工具编译,并集成到相同的 CI 基础架构中。
一个好的客户端应用程序架构可以清晰地将业务逻辑和 UI 组件分开。在 JavaFX 中,应用程序的 UI 部分可以被进一步分离。数据通常保存在ObservableObject
或ObservableList
的实例中,它们与 UI 组件的渲染细节没有直接关系——因此,它们可以被认为是通用组件的一部分。
有许多方法可以使 JavaFX 应用程序的 UI 组件更加移动化,我们将简要讨论以下内容:
-
对移动和桌面使用不同的样式表。
-
对手机使用特定的控制。
Gluon 的 glorin 框架[参见 https://docs.gluonhq.com/#_glisten_apis
]是 Gluon Mobile 的一部分,结合了这两种方法。
不同的样式表
JavaFX 用户界面灵活且易于更改的原因之一是因为对 CSS 的支持,这将在第五章“掌握可视化和 CSS 设计”中讨论样式表使用 CSS 文件以声明的方式定义了用户界面不同组件的外观。它们与实现逻辑是分离的,这允许许多组合。
看看我们在本章前面讨论的 HelloFX 应用程序的代码,在 JavaFX 应用程序中添加样式表很容易,例如,在应用程序的start
方法中:
Scene scene = new Scene(root, 640, 480);
scene.getStylesheets().add(
HelloFX.class.getResource("styles.css").toExternalForm());
在这段代码中,我们将名为styles.css
的样式表添加到场景图中。我们在这里使用的样式表非常简单,它描述了一个Label
控件中文本的样式:
.label {
-fx-text-fill: blue;
}
我们可以创建第二个样式表,将-fx-text-fill
属性设置为不同的颜色,例如红色。
我们可以将styles.css
文件复制到styles2.css
并编辑如下:
.label {
-fx-text-fill: red;
}
我们现在修改应用程序,以便加载这个样式表,而不是原来的样式表:
Scene scene = new Scene(root, 640, 480);
scene.getStylesheets().add(
HelloFX.class.getResource("styles2.css").toExternalForm());
如果我们现在运行这个应用程序,我们会看到文本颜色确实发生了变化,如图 11-9 所示。
这显示了通过提供不同的样式表来重新配置应用程序的用户界面是多么容易,但是我们现在为所有部署更改了样式表。我们真正想要的是基于目标平台加载不同的样式表。
一个简单的解决方案是检查系统属性" os.name
",并基于此加载不同的样式表,如下面的代码片段所示:
Scene scene = new Scene(root, 640, 480);
If (System.getProperty("os.name").equals("ios")) {
scene.getStylesheets().add(
HelloFX.class.getResource("styles.css").toExternalForm());
} else {
scene.getStylesheets().add(
HelloFX.class.getResource("styles2.css").toExternalForm());
}
这个代码片段将导致在 iOS 系统(设备或模拟器)上运行的情况下应用"styles.css"
样式表,在所有其他情况下将使用"styles2.css"
样式表。
图 11-9
带有 CSS 的 HelloFX 应用程序
有了 Gluon Mobile,你可以走得更远。与 Gluon Mobile 捆绑的组件之一是开源框架 Gluon Attach,它是在 https://github.com/gluonhq/attach
开发的。
Gluon Attach 包含许多服务,这些服务公开了一个 Java API,并使用特定的 API 在不同的平台上实现。由 Gluon Attach 实现的示例服务有位置、存储、应用内计费、图片、蓝牙低能耗等等。
使用 Attach,可以检测平台(例如 iOS 或 Android)和尺寸(例如手机或平板电脑)。这允许为 iPad 系统加载一个特定的样式表,为 Android 手机加载另一个特定的样式表。
下面的代码片段展示了如何检测到这一点,它将在每种情况下使用不同的样式表:
Scene scene = new Scene(root, 640, 480);
if (Platform.isIOS()) {
scene.getStylesheets().add(
HelloFX.class.getResource("styles.css").toExternalForm());
} else if (Platform.isAndroid()) {
scene.getStylesheets().add(
HelloFX.class.getResource("styles2.css").toExternalForm());
}
在这种情况下,我们利用com.gluonhq.attach.util.Platform
来获取应用程序运行的当前平台。
为了能够导入这个类,我们需要 build.gradle 文件中的依赖项,如清单 11-8 所示。
repositories {
mavenCentral()
maven {
url 'https://nexus.gluonhq.com/nexus/content/repositories/releases'
}
}
dependencies {
implementation "com.gluonhq:charm-glisten:6.0.6"
implementation "com.gluonhq.attach:util:4.0.11"
}
Listing 11-8Adding Attach to a Gradle project
我们在 pom 文件中需要类似的依赖,如清单 11-9 所示。
<dependencies>
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>charm-glisten</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>util</artifactId>
<version>4.0.11</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>Gluon</id>
<url>
https://nexus.gluonhq.com/nexus/content/repositories/releases
</url>
</repository>
</repositories>
Listing 11-9Adding Attach to a Maven project
特定于手机的控件
虽然我们可以用样式表做很多事情,但是有些控件确实只与移动设备相关。
为移动设备创建一个控件与为桌面应用程序创建一个控件没有什么不同,这已经在第七章“桥接 Swing 和 JavaFX”中讨论过了
Gluon Mobile 包含了许多特定于移动设备的控件,这些控件在典型的移动应用程序中经常遇到。这些控件的列表位于
https://docs.gluonhq.com/charm/javadoc/6.0.6/com.gluonhq.charm.glisten/com/gluonhq/charm/glisten/control/package-summary.html
作为一个例子,我们将展示一个使用FloatingActionButton
控件的应用程序。这个项目可以在这里找到(Gradle 和 Maven):
https://github.com/modernclientjava/mcj-samples/tree/master/ch11-Mobile/Gradle/HelloGluon
https://github.com/modernclientjava/mcj-samples/tree/master/ch11-Mobile/Maven/HelloGluon
《守则》
清单 11-10 显示了build.gradle
文件,清单 11-11 显示了 Gradle 项目的settings.gradle
文件。
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'HelloGluon'
Listing 11-11File settings.gradle
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'com.gluonhq.gluonfx-gradle-plugin' version '1.0.3'
}
repositories {
mavenCentral()
maven {
url 'https://nexus.gluonhq.com/nexus/content/repositories/releases/'
}
}
dependencies {
implementation "com.gluonhq:charm-glisten:6.0.6"
}
gluonfx {
target = 'host'
if (project.hasProperty('target')) {
target = project.getProperty('target')
}
attachConfig {
version = "4.0.11"
services 'display', 'lifecycle', 'statusbar', 'storage'
}
}
javafx {
version = "17-ea+16"
modules = [ "javafx.controls" ]
}
mainClassName = "$moduleName/com.gluonhq.hello.HelloGluon"
Listing 11-10File build.gradle
如果您有一个 Maven 项目,清单 11-12 显示了等价的 pom 文件。
<?xml version="1.0" encoding="UTF-8"?>
<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>com.gluonhq.hello</groupId>
<artifactId>hellogluon</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hellogluon</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>11</maven.compiler.release>
<javafx.version>17.0.1</javafx.version>
<attach.version>4.0.11</attach.version>
<mainClassName>com.gluonhq.hello.HelloGluon</mainClassName>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq</groupId>
<artifactId>charm-glisten</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>display</artifactId>
<version>${attach.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>lifecycle</artifactId>
<version>${attach.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>statusbar</artifactId>
<version>${attach.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>storage</artifactId>
<version>${attach.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>util</artifactId>
<version>${attach.version}</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>Gluon</id>
<url>https://nexus.gluonhq.com/nexus/content/repositories/releases</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.6</version>
<configuration>
<mainClass>${mainClassName}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>1.0.7</version>
<configuration>
<target>${gluonfx.target}</target>
<attachList>
<list>display</list>
<list>lifecycle</list>
<list>statusbar</list>
<list>storage</list>
</attachList>
<mainClass>${mainClassName}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>ios</id>
<properties>
<gluonfx.target>ios</gluonfx.target>
</properties>
</profile>
<profile>
<id>android</id>
<properties>
<gluonfx.target>android</gluonfx.target>
</properties>
</profile>
</profiles>
</project>
Listing 11-12pom.xml file
清单 11-13 显示了HelloGluon
主类,清单 11-14 显示了 styles.css 文件。
.label {
-fx-font-size: 2em;
-fx-text-fill: -primary-swatch-700;
}
Listing 11-14File styles.css
package hellofx;
import com.gluonhq.attach.display.DisplayService;
import com.gluonhq.attach.util.Platform;
import com.gluonhq.charm.glisten.application.MobileApplication;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.control.FloatingActionButton;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import com.gluonhq.charm.glisten.visual.Swatch;
import javafx.geometry.Dimension2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
public class HelloGluon extends MobileApplication {
@Override
public void init() {
addViewFactory(HOME_VIEW, () -> {
FloatingActionButton fab =
new FloatingActionButton(MaterialDesignIcon.SEARCH.text,
e -> System.out.println("Search"));
ImageView imageView = new ImageView(new Image(
HelloGluon.class.getResourceAsStream("openduke.png")));
imageView.setFitHeight(200);
imageView.setPreserveRatio(true);
Label label = new Label("Hello, Gluon Mobile!");
VBox root = new VBox(20, imageView, label);
root.setAlignment(Pos.CENTER);
View view = new View(root) {
@Override
protected void updateAppBar(AppBar appBar) {
appBar.setTitleText("Gluon Mobile");
}
};
fab.showOn(view);
return view;
});
}
@Override
public void postInit(Scene scene) {
Swatch.LIGHT_GREEN.assignTo(scene);
scene.getStylesheets().add(
HelloGluon.class.getResource("styles.css").toExternalForm());
if (Platform.isDesktop()) {
Dimension2D dimension2D = DisplayService.create()
.map(display -> display.getDefaultDimensions())
.orElse(new Dimension2D(640, 480));
scene.getWindow().setWidth(dimension2D.getWidth());
scene.getWindow().setHeight(dimension2D.getHeight());
}
}
public static void main(String[] args) {
launch();
}
}
Listing 11-13HelloFX.java file
构建项目
第一步是将项目作为一个常规的 Java 项目来构建和运行(在一个用于本地开发的常规 JVM 上,例如 HotSpot)。
与格雷尔:
./gradlew clean build run
使用 Maven:
mvn clean gluonfx:run
结果如图 11-10 所示。
图 11-10
在桌面上运行 HelloGluon
一旦项目准备就绪,我们现在将在 iOS 上编译、打包和运行应用程序(这同样适用于 Android)。
编译链接
与 Gradle 一起运行:
./gradlew -Ptarget=ios nativeBuild
或者与 Maven 一起运行:
mvn -Pios gluonfx:build
运行
与 Gradle 一起运行:
./gradlew -Ptarget=ios nativeRun
或者与 Maven 一起运行:
mvn -Pios gluonfx:nativerun
结果如图 11-11 所示。
图 11-11
iOS 上的 HelloGluon 应用程序
摘要
JavaFX 应用程序非常适合部署在移动设备上。JavaFX 平台和 Graal 原生图像组件的结合,集成在 Gluon 移动客户端包中,使所有 Java 开发人员能够使用他们的 Java 技能,创建可以上传到流行的移动应用商店的应用。
有许多工具可以帮助开发人员以非常熟悉的方式将 Java 和 JavaFX 应用程序部署到移动设备上。
为了使应用程序真正对移动设备友好并适应移动设备环境,可以使用许多框架(例如,Gluon Attach 和 Glisten)。