如何在预加载器中处理 java web start (jnlp) 下载进度?
How to handle java web start (jnlp) downloading progress in a preloader?
问题
我的应用程序有一个预加载器,用于处理特定于应用程序的初始化。现在我正在尝试扩展它,以便预加载器也显示下载的应用程序 JAR 的进度。
TL;DR
为什么预加载器在 阶段 2 期间没有加载,因为这应该处理 PreloaderFx::handleProgressNotification();
以跟踪 JAR 的下载我想?
2016 年 3 月 14 日更新:使用 DownloadServiceListener 是解决此问题的方法吗?如何将其连接到 JavaFX 阶段?
文档
According to Oracle,应用程序启动有 4 个阶段:
- 阶段 1:初始化:Java 运行时的初始化和初始检查确定在启动应用程序之前必须加载和执行的组件。
在此阶段,会显示启动画面。默认是这样的:
第 2 阶段:加载和准备:从网络或磁盘缓存加载所需的资源,并进行验证过程。所有执行模式都看到默认或自定义预加载器。在此阶段,应该显示我的自定义预加载器。
阶段 3:特定于应用程序的初始化:应用程序已启动,但可能需要加载额外的资源或执行其他冗长的准备工作才能完全完成功能性的。目前,我的自定义预加载器显示为:
- 阶段 4:应用程序执行:显示应用程序并可以使用。在我的例子中,显示登录 window,用户可以继续。
我的案例
我注意到的第一件事是,在第 2 阶段,处理应用程序 JAR 下载的默认 JavaFX 预加载器没有显示。因此,用户感觉程序没有启动或过早终止,使他们多次打开 JNLP 文件。下载 JAR 后,我们进入 阶段 3 并显示预加载器。
但是,我希望我的自定义预加载器也能处理 ProgressBar 中的下载进度(第 2 阶段)。我使一切都尽可能简单,以跟踪在我的应用程序启动期间发生的事件。这是基于 example of Jewelsea and on Oracle examples:
预加载器:
public class PreloaderFX extends Preloader {
Stage stage;
//boolean noLoadingProgress = true;
public static final String APPLICATION_ICON
= "http://cdn1.iconfinder.com/data/icons/Copenhagen/PNG/32/people.png";
public static final String SPLASH_IMAGE
= "http://fxexperience.com/wp-content/uploads/2010/06/logo.png";
private Pane splashLayout;
private ProgressBar loadProgress;
private Label progressText;
private static final int SPLASH_WIDTH = 676;
private static final int SPLASH_HEIGHT = 227;
@Override
public void init() {
ImageView splash = new ImageView(new Image(
SPLASH_IMAGE
));
loadProgress = new ProgressBar();
loadProgress.setPrefWidth(SPLASH_WIDTH - 20);
progressText = new Label("Loading . . .");
splashLayout = new VBox();
splashLayout.getChildren().addAll(splash, loadProgress, progressText);
progressText.setAlignment(Pos.CENTER);
splashLayout.setStyle(
"-fx-padding: 5; "
+ "-fx-background-color: white; "
+ "-fx-border-width:5; "
);
splashLayout.setEffect(new DropShadow());
}
@Override
public void start(Stage stage) throws Exception {
System.out.println("PreloaderFx::start();");
//this.stage = new Stage(StageStyle.DECORATED);
stage.setTitle("Title");
stage.getIcons().add(new Image(APPLICATION_ICON));
stage.initStyle(StageStyle.UNDECORATED);
final Rectangle2D bounds = Screen.getPrimary().getBounds();
stage.setScene(new Scene(splashLayout));
stage.setX(bounds.getMinX() + bounds.getWidth() / 2 - SPLASH_WIDTH / 2);
stage.setY(bounds.getMinY() + bounds.getHeight() / 2 - SPLASH_HEIGHT / 2);
stage.show();
this.stage = stage;
}
@Override
public void handleProgressNotification(ProgressNotification pn) {
System.out.println("PreloaderFx::handleProgressNotification(); progress = " + pn.getProgress());
//application loading progress is rescaled to be first 50%
//Even if there is nothing to load 0% and 100% events can be
// delivered
if (pn.getProgress() != 1.0 /*|| !noLoadingProgress*/) {
loadProgress.setProgress(pn.getProgress() / 2);
/*if (pn.getProgress() > 0) {
noLoadingProgress = false;
}*/
}
}
@Override
public void handleStateChangeNotification(StateChangeNotification evt) {
//ignore, hide after application signals it is ready
System.out.println("PreloaderFx::handleStateChangeNotification(); state = " + evt.getType());
}
@Override
public void handleApplicationNotification(PreloaderNotification pn) {
if (pn instanceof ProgressNotification) {
//expect application to send us progress notifications
//with progress ranging from 0 to 1.0
double v = ((ProgressNotification) pn).getProgress();
System.out.println("PreloaderFx::handleApplicationNotification(); progress = " + v);
//if (!noLoadingProgress) {
//if we were receiving loading progress notifications
//then progress is already at 50%.
//Rescale application progress to start from 50%
v = 0.5 + v / 2;
//}
loadProgress.setProgress(v);
} else if (pn instanceof StateChangeNotification) {
System.out.println("PreloaderFx::handleApplicationNotification(); state = " + ((StateChangeNotification) pn).getType());
//hide after get any state update from application
stage.hide();
}
}
}
在阶段 3 中处理的代码来自与预加载器交互的主应用程序,这是在进度条中看到的内容:
public class MainApp extends Application {
BooleanProperty ready = new SimpleBooleanProperty(false);
public static void main(String[] args) throws Exception {
launch(args);
}
@Override
public void start(final Stage initStage) throws Exception {
System.out.println("MainApp::start();");
this.mainStage = initStage;
longStart();
ready.addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
if (Boolean.TRUE.equals(t1)) {
Platform.runLater(() -> {
System.out.println("MainApp::showMainStage();");
showMainStage();
});
}
});
}
private void longStart() {
//simulate long init in background
Task task = new Task<Void>() {
@Override
protected Void call() throws Exception {
int max = 10;
for (int i = 1; i <= max; i++) {
Thread.sleep(500);
System.out.println("longStart " + i);
// Send progress to preloader
notifyPreloader(new ProgressNotification(((double) i)/max)); //this moves the progress bar of the preloader
}
// After init is ready, the app is ready to be shown
// Do this before hiding the preloader stage to prevent the
// app from exiting prematurely
ready.setValue(Boolean.TRUE);
notifyPreloader(new StateChangeNotification(
StateChangeNotification.Type.BEFORE_START));
return null;
}
};
new Thread(task).start();
}
private void showMainStage() {
//showing the login window
}
}
JNLP
<jnlp spec="1.0+" xmlns:jfx="http://javafx.com" codebase="<***>/preloadertest/jnlp" href="launch.jnlp">
<information>
...
</information>
<resources>
<j2se version="1.6+" href="http://java.sun.com/products/autodl/j2se" />
... //whole bunch of JARS
<jar href="lib/preloader-1.1.1.jar" download="progress" />
</resources>
<security>
<all-permissions/>
</security>
<applet-desc width="1024" height="768" main-class="com.javafx.main.NoJavaFXFallback" name="JavaFX Client">
<param name="requiredFXVersion" value="8.0+"/>
</applet-desc>
<jfx:javafx-desc width="1024" height="768" main-class="GUI.MainApp" name="JavaFX Client" preloader-class="GUI.PreloaderFX" />
<update check="background"/>
</jnlp>
调试
我在启动文件时仔细观察了 Java 控制台(启用显示日志记录,禁用显示跟踪)并注意到以下内容:
在 阶段 2 期间,Java 控制台中没有任何显示(此阶段后控制台关闭)
在 阶段 3 期间,生成以下输出(在新控制台 window 中):
PreloaderFx::start();
PreloaderFx::handleProgressNotification(); progress = 1.0
PreloaderFx::handleStateChangeNotification(); state = BEFORE_LOAD
PreloaderFx::handleStateChangeNotification(); state = BEFORE_INIT
PreloaderFx::handleStateChangeNotification(); state = BEFORE_START
MainApp::start();
MainApp::longstart();
longStart 1
PreloaderFx::handleApplicationNotification(); progress = 0.1
longStart 2
PreloaderFx::handleApplicationNotification(); progress = 0.2
longStart 3
PreloaderFx::handleApplicationNotification(); progress = 0.3
longStart 4
PreloaderFx::handleApplicationNotification(); progress = 0.4
longStart 5
PreloaderFx::handleApplicationNotification(); progress = 0.5
longStart 6
PreloaderFx::handleApplicationNotification(); progress = 0.6
longStart 7
PreloaderFx::handleApplicationNotification(); progress = 0.7
longStart 8
PreloaderFx::handleApplicationNotification(); progress = 0.8
longStart 9
PreloaderFx::handleApplicationNotification(); progress = 0.9
longStart 10
PreloaderFx::handleApplicationNotification(); progress = 1.0
MainApp::showMainStage();
PreloaderFx::handleApplicationNotification(); state = BEFORE_START
2016 年 3 月 13 日更新:
- 调整了代码,以便使用方法中传递的阶段而不是创建一个新阶段,并注释掉与
noLoadingProgress
布尔值相关的所有内容(由 nhylated 建议)
- 在 MainApp
中添加了一些额外的 System.out.println()
解决方案
只需将 <jfx:javafx-runtime version="8.0+"/>
添加到 JNLP 文件即可修复它。添加该行后,预加载器显示在第 2 阶段。我还冒昧地将 j2se version="1.6+"
更改为 j2se version="1.8+"
结果:
前 50% 是 JAR 下载的处理。这是通过 handleProgressNotification()
方法完成的。第二个 50% 是 MainApp 的实际初始化(longstart()
通知预加载器),由 handleApplicationNotification()
.
完成
注意:我还没有测试或执行代码;我的回答主要基于查看代码。我没有使用过 JavaFX,但我可以掌握代码,因为我之前使用过 Swing 和 JNLP。
The first thing I notice, is that in Phase 2, the default JavaFX
preloader handling the downloading of the application JARs is not
showing.
这似乎是因为 PreloaderFX.start(stage)
方法。作为方法参数传递的 stage
保持为空,因为该方法构造了一个 new Stage()
并向其添加了子项。如果您将子元素添加到方法的参数中,它应该在第 2 阶段显示您的自定义 preloader/progressbar。
如果此后代码仍无法按预期工作,我将尝试调试与 noLoadingProgress
标志关联的所有 logic/code。尝试将所有内容注释掉(基本上将 noLoadingProgress
从图片中删除)并查看它是否解决了问题。
此外,即使您在代码中正确地做到了这一点,请参阅 this 答案 - 根据该答案,所有 ProgressNotification
都由 handleApplicationNotification
方法处理。
希望对您有所帮助!
最近我也一直在和这个作斗争。我切换回(丑陋的)默认预加载器(因为它显示得很好),直到我找到更多时间来调查这个。
如果启用Java Webstart 完整跟踪
"<JAVA_HOME>\bin\javaws.exe" -userConfig deployment.trace true
"<JAVA_HOME>\bin\javaws.exe" -userConfig deployment.trace.level all
您应该会看到预加载器消息,这些消息应该为您提供有关正在发生的事情的一些信息。就我而言,我可以看到很多这样的消息
preloader: Added pending event 2: DownloadEvent[type=load,loaded=0, total=62791, percent=0]
表示自定义预加载器尚未 verified/started 但下载事件已经进入。
如果将 <update check="background"/>
切换为 <update check="always"/>
会怎样?
编辑
这是我的测试JNLP。似乎您没有指定 JavaFX 运行时资源?
<?xml version="1.0" encoding="utf-8"?>
<jnlp spec="1.0+" xmlns:jfx="http://javafx.com" codebase="http://localhost:8080/HelloWorldFX" href="HelloWorldFX.jnlp">
<information>
<title>HelloWorldFX</title>
<vendor>Unknown</vendor>
<description>HelloWorldFX</description>
<offline-allowed/>
</information>
<resources os="Windows">
<jfx:javafx-runtime version="8.0+"/>
</resources>
<resources>
<j2se version="1.8+" href="http://java.sun.com/products/autodl/j2se"/>
<jar href="HelloWorldPreloader.jar" size="10774" download="progress" />
<jar href="HelloWorldFX.jar" size="248884114" download="eager" main="true" />
</resources>
<jfx:javafx-desc width="600" height="400" main-class="sample.Main" name="HelloWorldFX" preloader-class="HelloWorldPreloader"/>
<update check="always"/>
</jnlp>
问题
我的应用程序有一个预加载器,用于处理特定于应用程序的初始化。现在我正在尝试扩展它,以便预加载器也显示下载的应用程序 JAR 的进度。
TL;DR
为什么预加载器在 阶段 2 期间没有加载,因为这应该处理
PreloaderFx::handleProgressNotification();
以跟踪 JAR 的下载我想?2016 年 3 月 14 日更新:使用 DownloadServiceListener 是解决此问题的方法吗?如何将其连接到 JavaFX 阶段?
文档
According to Oracle,应用程序启动有 4 个阶段:
- 阶段 1:初始化:Java 运行时的初始化和初始检查确定在启动应用程序之前必须加载和执行的组件。 在此阶段,会显示启动画面。默认是这样的:
第 2 阶段:加载和准备:从网络或磁盘缓存加载所需的资源,并进行验证过程。所有执行模式都看到默认或自定义预加载器。在此阶段,应该显示我的自定义预加载器。
阶段 3:特定于应用程序的初始化:应用程序已启动,但可能需要加载额外的资源或执行其他冗长的准备工作才能完全完成功能性的。目前,我的自定义预加载器显示为:
- 阶段 4:应用程序执行:显示应用程序并可以使用。在我的例子中,显示登录 window,用户可以继续。
我的案例
我注意到的第一件事是,在第 2 阶段,处理应用程序 JAR 下载的默认 JavaFX 预加载器没有显示。因此,用户感觉程序没有启动或过早终止,使他们多次打开 JNLP 文件。下载 JAR 后,我们进入 阶段 3 并显示预加载器。
但是,我希望我的自定义预加载器也能处理 ProgressBar 中的下载进度(第 2 阶段)。我使一切都尽可能简单,以跟踪在我的应用程序启动期间发生的事件。这是基于 example of Jewelsea and on Oracle examples:
预加载器:
public class PreloaderFX extends Preloader {
Stage stage;
//boolean noLoadingProgress = true;
public static final String APPLICATION_ICON
= "http://cdn1.iconfinder.com/data/icons/Copenhagen/PNG/32/people.png";
public static final String SPLASH_IMAGE
= "http://fxexperience.com/wp-content/uploads/2010/06/logo.png";
private Pane splashLayout;
private ProgressBar loadProgress;
private Label progressText;
private static final int SPLASH_WIDTH = 676;
private static final int SPLASH_HEIGHT = 227;
@Override
public void init() {
ImageView splash = new ImageView(new Image(
SPLASH_IMAGE
));
loadProgress = new ProgressBar();
loadProgress.setPrefWidth(SPLASH_WIDTH - 20);
progressText = new Label("Loading . . .");
splashLayout = new VBox();
splashLayout.getChildren().addAll(splash, loadProgress, progressText);
progressText.setAlignment(Pos.CENTER);
splashLayout.setStyle(
"-fx-padding: 5; "
+ "-fx-background-color: white; "
+ "-fx-border-width:5; "
);
splashLayout.setEffect(new DropShadow());
}
@Override
public void start(Stage stage) throws Exception {
System.out.println("PreloaderFx::start();");
//this.stage = new Stage(StageStyle.DECORATED);
stage.setTitle("Title");
stage.getIcons().add(new Image(APPLICATION_ICON));
stage.initStyle(StageStyle.UNDECORATED);
final Rectangle2D bounds = Screen.getPrimary().getBounds();
stage.setScene(new Scene(splashLayout));
stage.setX(bounds.getMinX() + bounds.getWidth() / 2 - SPLASH_WIDTH / 2);
stage.setY(bounds.getMinY() + bounds.getHeight() / 2 - SPLASH_HEIGHT / 2);
stage.show();
this.stage = stage;
}
@Override
public void handleProgressNotification(ProgressNotification pn) {
System.out.println("PreloaderFx::handleProgressNotification(); progress = " + pn.getProgress());
//application loading progress is rescaled to be first 50%
//Even if there is nothing to load 0% and 100% events can be
// delivered
if (pn.getProgress() != 1.0 /*|| !noLoadingProgress*/) {
loadProgress.setProgress(pn.getProgress() / 2);
/*if (pn.getProgress() > 0) {
noLoadingProgress = false;
}*/
}
}
@Override
public void handleStateChangeNotification(StateChangeNotification evt) {
//ignore, hide after application signals it is ready
System.out.println("PreloaderFx::handleStateChangeNotification(); state = " + evt.getType());
}
@Override
public void handleApplicationNotification(PreloaderNotification pn) {
if (pn instanceof ProgressNotification) {
//expect application to send us progress notifications
//with progress ranging from 0 to 1.0
double v = ((ProgressNotification) pn).getProgress();
System.out.println("PreloaderFx::handleApplicationNotification(); progress = " + v);
//if (!noLoadingProgress) {
//if we were receiving loading progress notifications
//then progress is already at 50%.
//Rescale application progress to start from 50%
v = 0.5 + v / 2;
//}
loadProgress.setProgress(v);
} else if (pn instanceof StateChangeNotification) {
System.out.println("PreloaderFx::handleApplicationNotification(); state = " + ((StateChangeNotification) pn).getType());
//hide after get any state update from application
stage.hide();
}
}
}
在阶段 3 中处理的代码来自与预加载器交互的主应用程序,这是在进度条中看到的内容:
public class MainApp extends Application {
BooleanProperty ready = new SimpleBooleanProperty(false);
public static void main(String[] args) throws Exception {
launch(args);
}
@Override
public void start(final Stage initStage) throws Exception {
System.out.println("MainApp::start();");
this.mainStage = initStage;
longStart();
ready.addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
if (Boolean.TRUE.equals(t1)) {
Platform.runLater(() -> {
System.out.println("MainApp::showMainStage();");
showMainStage();
});
}
});
}
private void longStart() {
//simulate long init in background
Task task = new Task<Void>() {
@Override
protected Void call() throws Exception {
int max = 10;
for (int i = 1; i <= max; i++) {
Thread.sleep(500);
System.out.println("longStart " + i);
// Send progress to preloader
notifyPreloader(new ProgressNotification(((double) i)/max)); //this moves the progress bar of the preloader
}
// After init is ready, the app is ready to be shown
// Do this before hiding the preloader stage to prevent the
// app from exiting prematurely
ready.setValue(Boolean.TRUE);
notifyPreloader(new StateChangeNotification(
StateChangeNotification.Type.BEFORE_START));
return null;
}
};
new Thread(task).start();
}
private void showMainStage() {
//showing the login window
}
}
JNLP
<jnlp spec="1.0+" xmlns:jfx="http://javafx.com" codebase="<***>/preloadertest/jnlp" href="launch.jnlp">
<information>
...
</information>
<resources>
<j2se version="1.6+" href="http://java.sun.com/products/autodl/j2se" />
... //whole bunch of JARS
<jar href="lib/preloader-1.1.1.jar" download="progress" />
</resources>
<security>
<all-permissions/>
</security>
<applet-desc width="1024" height="768" main-class="com.javafx.main.NoJavaFXFallback" name="JavaFX Client">
<param name="requiredFXVersion" value="8.0+"/>
</applet-desc>
<jfx:javafx-desc width="1024" height="768" main-class="GUI.MainApp" name="JavaFX Client" preloader-class="GUI.PreloaderFX" />
<update check="background"/>
</jnlp>
调试
我在启动文件时仔细观察了 Java 控制台(启用显示日志记录,禁用显示跟踪)并注意到以下内容:
在 阶段 2 期间,Java 控制台中没有任何显示(此阶段后控制台关闭)
在 阶段 3 期间,生成以下输出(在新控制台 window 中):
PreloaderFx::start();
PreloaderFx::handleProgressNotification(); progress = 1.0
PreloaderFx::handleStateChangeNotification(); state = BEFORE_LOAD
PreloaderFx::handleStateChangeNotification(); state = BEFORE_INIT
PreloaderFx::handleStateChangeNotification(); state = BEFORE_START
MainApp::start();
MainApp::longstart();
longStart 1
PreloaderFx::handleApplicationNotification(); progress = 0.1
longStart 2
PreloaderFx::handleApplicationNotification(); progress = 0.2
longStart 3
PreloaderFx::handleApplicationNotification(); progress = 0.3
longStart 4
PreloaderFx::handleApplicationNotification(); progress = 0.4
longStart 5
PreloaderFx::handleApplicationNotification(); progress = 0.5
longStart 6
PreloaderFx::handleApplicationNotification(); progress = 0.6
longStart 7
PreloaderFx::handleApplicationNotification(); progress = 0.7
longStart 8
PreloaderFx::handleApplicationNotification(); progress = 0.8
longStart 9
PreloaderFx::handleApplicationNotification(); progress = 0.9
longStart 10
PreloaderFx::handleApplicationNotification(); progress = 1.0
MainApp::showMainStage();
PreloaderFx::handleApplicationNotification(); state = BEFORE_START
2016 年 3 月 13 日更新:
- 调整了代码,以便使用方法中传递的阶段而不是创建一个新阶段,并注释掉与
noLoadingProgress
布尔值相关的所有内容(由 nhylated 建议) - 在 MainApp 中添加了一些额外的
System.out.println()
解决方案
只需将 <jfx:javafx-runtime version="8.0+"/>
添加到 JNLP 文件即可修复它。添加该行后,预加载器显示在第 2 阶段。我还冒昧地将 j2se version="1.6+"
更改为 j2se version="1.8+"
结果:
前 50% 是 JAR 下载的处理。这是通过 handleProgressNotification()
方法完成的。第二个 50% 是 MainApp 的实际初始化(longstart()
通知预加载器),由 handleApplicationNotification()
.
注意:我还没有测试或执行代码;我的回答主要基于查看代码。我没有使用过 JavaFX,但我可以掌握代码,因为我之前使用过 Swing 和 JNLP。
The first thing I notice, is that in Phase 2, the default JavaFX preloader handling the downloading of the application JARs is not showing.
这似乎是因为 PreloaderFX.start(stage)
方法。作为方法参数传递的 stage
保持为空,因为该方法构造了一个 new Stage()
并向其添加了子项。如果您将子元素添加到方法的参数中,它应该在第 2 阶段显示您的自定义 preloader/progressbar。
如果此后代码仍无法按预期工作,我将尝试调试与 noLoadingProgress
标志关联的所有 logic/code。尝试将所有内容注释掉(基本上将 noLoadingProgress
从图片中删除)并查看它是否解决了问题。
此外,即使您在代码中正确地做到了这一点,请参阅 this 答案 - 根据该答案,所有 ProgressNotification
都由 handleApplicationNotification
方法处理。
希望对您有所帮助!
最近我也一直在和这个作斗争。我切换回(丑陋的)默认预加载器(因为它显示得很好),直到我找到更多时间来调查这个。
如果启用Java Webstart 完整跟踪
"<JAVA_HOME>\bin\javaws.exe" -userConfig deployment.trace true
"<JAVA_HOME>\bin\javaws.exe" -userConfig deployment.trace.level all
您应该会看到预加载器消息,这些消息应该为您提供有关正在发生的事情的一些信息。就我而言,我可以看到很多这样的消息
preloader: Added pending event 2: DownloadEvent[type=load,loaded=0, total=62791, percent=0]
表示自定义预加载器尚未 verified/started 但下载事件已经进入。
如果将 <update check="background"/>
切换为 <update check="always"/>
会怎样?
编辑
这是我的测试JNLP。似乎您没有指定 JavaFX 运行时资源?
<?xml version="1.0" encoding="utf-8"?>
<jnlp spec="1.0+" xmlns:jfx="http://javafx.com" codebase="http://localhost:8080/HelloWorldFX" href="HelloWorldFX.jnlp">
<information>
<title>HelloWorldFX</title>
<vendor>Unknown</vendor>
<description>HelloWorldFX</description>
<offline-allowed/>
</information>
<resources os="Windows">
<jfx:javafx-runtime version="8.0+"/>
</resources>
<resources>
<j2se version="1.8+" href="http://java.sun.com/products/autodl/j2se"/>
<jar href="HelloWorldPreloader.jar" size="10774" download="progress" />
<jar href="HelloWorldFX.jar" size="248884114" download="eager" main="true" />
</resources>
<jfx:javafx-desc width="600" height="400" main-class="sample.Main" name="HelloWorldFX" preloader-class="HelloWorldPreloader"/>
<update check="always"/>
</jnlp>