如何在预加载器中处理Java Web Start (JNLP)下载进度?

28

问题

我有一个应用程序的预加载器,处理特定于应用程序的初始化。现在我正在尝试扩展它,以便预加载器还显示已下载的应用程序JAR文件的进度。


简述

  • 为什么在第二阶段期间,预加载程序未被加载,因为这应该处理PreloaderFx :: handleProgressNotification();来跟踪JAR的下载,我想知道是什么原因?

  • 2016年3月14日更新:使用DownloadServiceListener解决这个问题的方式是什么?如何将其连接到JavaFX舞台?


文档

根据Oracle的说法, 应用程序启动时有4个阶段:

  • 第一阶段: 初始化: Java Runtime的初始化和初始检查确定必须在启动应用程序之前加载和执行的组件。 在此阶段,会显示一个启动画面。默认情况下是这个:

Java starts

  • 第二阶段:加载和准备:必要的资源从网络或磁盘缓存中加载,并进行验证程序。所有执行模式都可以看到默认的或自定义的预加载器。在此阶段,应该显示我的自定义预加载器。

  • 第三阶段:应用程序特定初始化:应用程序已启动,但可能需要加载其他资源或执行其他漫长的准备工作,才能完全发挥功能。目前,正在显示我的自定义预加载器:

preloader

  • 第四阶段:应用程序执行:应用程序被显示并准备好使用。在我的情况下,会显示一个登录窗口,用户可以继续操作。

login


我的案例

我注意到的第一件事是,在第二阶段中,处理应用程序JAR文件下载的默认JavaFX预加载程序未显示。由于这个原因,用户会感觉程序没有启动或提前终止,导致他们多次打开JNLP文件。一旦JAR文件被下载,我们进入第三阶段并显示预加载程序。

然而,我希望我的自定义预加载程序也能在ProgressBar中处理下载进度(第二阶段)。我尽可能简单地跟踪了应用程序启动期间发生的事件。这是基于 Jewelsea的示例Oracle的示例

预加载程序:

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();
            }
        }
    }

第三阶段处理的代码来自与预加载程序交互的主应用程序,这是在进度条中看到的内容:

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阶段期间,生成以下输出(在新的控制台窗口中):

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()

解决方案

只需在JNLP文件中添加<jfx:javafx-runtime version="8.0+"/>即可解决问题。添加此行后,预加载程序将在第二阶段显示。我还修改了j2se version="1.6+"j2se version="1.8+"。结果如下:

Preloader result

前50%是处理JAR下载,由handleProgressNotification()方法完成。后50%是实际初始化MainApp (longstart()通知预加载程序),由handleApplicationNotification()完成。


它在我的JRE8u144上无法工作。你使用的是哪个版本? - Eng.Fouad
你遇到了什么问题?目前,预加载器似乎只在下载完成后才显示。所以基本上在上面的示例中初始化50%阶段。 - Perneel
没错。预加载器只会在下载完成后显示。 - Eng.Fouad
如果预加载器仅在下载完成后显示,则表示JRE中存在错误。是否有任何错误报告?如果没有,请提出一个并让制造商参与其中。 - Andrew Thompson
2个回答

9

最近我也在与这个问题奋斗。我已经切换回(丑陋的)默认预加载程序(因为它能正常显示),直到我有更多时间来调查这个问题。

如果您启用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]

这表示自定义预加载程序尚未经过验证/启动,但下载事件已经在进行中。

如果您将<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>

<update check="background"/> 更改为 <update check="always"/> 不幸地并没有改变任何东西。你看到默认的 JavaFX 预加载器了吗?因为我不知道为什么没有看到... - Perneel
是的,我有。但只有在您没有指定自定义预加载程序类的情况下才能看到它。如果您甚至无法看到默认的预加载程序,那么很可能是您的JNLP出了问题。提供完整的跟踪信息将有助于解决问题... - jansohn
1
我认为我们正在取得进展,将<jfx:javafx-runtime version="8.0+"/>添加到资源中现在可以在第二阶段中显示我的自定义预加载程序,并且PreloaderFx :: handleProgressNotification();正在被调用。然而,根据此链接,<jfx:javafx-runtime version="8.0+"/>应该已经过时了,因为Java 8:https://bugs.openjdk.java.net/browse/JDK-8096671。今天下午我会更深入地研究它! :) - Perneel
1
通过添加JavaFX运行时版本,它可以完美地工作!非常感谢! - Perneel
在最新版本的Java 8中,使用<jfx:javafx-runtime version="8.0+"/>似乎无法正常工作 :( - Eng.Fouad

5
注意:我没有测试或执行这段代码;我的答案主要基于查看代码。虽然我没有使用过JavaFX,但由于之前使用了Swing和JNLP,所以我可以理解这段代码。
我注意到的第一件事是,在第二阶段中,处理应用程序JAR下载的默认JavaFX预加载程序未显示。这似乎是由于PreloaderFX.start(stage)方法造成的。该方法的stage参数保持为空,因为该方法构造了一个new Stage()并向其中添加子元素。如果您将子元素添加到该方法的参数中,则在第二阶段应该会显示您的自定义预加载程序/进度条。
如果在此之后代码仍然不能按预期工作,我建议尝试调试与noLoadingProgress标志相关的所有逻辑/代码。尝试注释掉所有这些内容(基本上将noLoadingProgress从中排除),看看是否可以解决问题。
此外,即使您的代码已经正确,也请参考this的答案-根据该答案,所有的ProgressNotification都由handleApplicationNotification方法处理。
希望这可以帮助到您!

首先,感谢您抽出时间查看这个问题。我根据您的建议和我的发现编辑了问题。在使用方法传递的阶段并注释掉“noLoadingProgress”布尔值后,结果仍然相同。我在MainApp中添加了一些额外的System.out.println()行,以检查第3阶段何时开始。我仍然坚信handleApplicationNotification应该只用于处理来自MainApp的notifyPreloader()操作。搜索继续 :) - Perneel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接