JavaFX 8中的通用异常处理

19

如果一个场景的控制器调用了引发异常的业务代码,我该如何以一般的方式处理这些异常?

我尝试使用 Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler) 方法,但它没有被调用,所以我认为这些异常在JavaFX框架内被捕获了。

我能做些什么来处理这些异常或至少向用户显示一些有用的信息?

2个回答

41
截至JavaFX 8,Thread.setDefaultUncaughtExceptionHandler(...)应该有效:请参见RT-15332
如果在执行start(...)方法期间发生未捕获的异常,则情况会有些复杂。根据应用程序的启动方式,调用start()的代码(例如Application.launch(...)的实现)可能会捕获并处理异常,这显然会防止调用默认的异常处理程序。
特别是在我的系统上(JDK 1.8.0_20,Mac OS X 10.9.5),如果我的应用程序通过调用Application.launch(...)main(...)方法启动,那么在start()方法中抛出的任何异常都会被捕获(而不是重新抛出)。
然而,如果我删除main(...)方法(见下面的注释)并直接启动应用程序,则在start()方法中抛出的任何异常都会被重新抛出,从而允许调用默认的异常处理程序。请注意,它不仅仅是向上传播。start()在FX应用程序线程上被调用,并且异常是从主线程重新抛出的。实际上,当发生这种情况时,默认处理程序中的代码假定FX应用程序线程正在运行,但无法运行:因此我的猜测是,在这种情况下,启动代码捕获start()方法中的异常,并在catch块中关闭FX Application Thread,然后从调用线程重新抛出异常。
总之,重要的是 - 如果您希望默认处理程序处理start()方法中的异常,则如果异常没有在FX应用程序线程上抛出(即使通过Platform.runLater(...)),则不应调用任何UI代码。
注意:(对于那些可能不知道的人)从Java 8开始,即使没有main(...)方法,您也可以直接启动Application子类,方法是将类名作为参数传递给JVM可执行文件,以通常的方式执行(即java MyApp)。这样做的效果是您所期望的:启动FX工具包,启动FX应用程序线程,实例化Application子类并调用init(),然后在FX应用程序线程上调用start()。有趣的是(也许是错误的),调用Application.launch()的main(...)方法与start(...)方法中未捕获的异常相关的行为略有不同。
以下是一个基本示例。取消注释Controller.initialize()中的代码以查看在start()方法中抛出异常的情况。
package application;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {

        Thread.setDefaultUncaughtExceptionHandler(Main::showError);

        Parent root = FXMLLoader.load(getClass().getResource("Main.fxml"));
        Scene scene = new Scene(root,400,400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static void showError(Thread t, Throwable e) {
        System.err.println("***Default exception handler***");
        if (Platform.isFxApplicationThread()) {
            showErrorDialog(e);
        } else {
            System.err.println("An unexpected error occurred in "+t);

        }
    }

    private static void showErrorDialog(Throwable e) {
        StringWriter errorMsg = new StringWriter();
        e.printStackTrace(new PrintWriter(errorMsg));
        Stage dialog = new Stage();
        dialog.initModality(Modality.APPLICATION_MODAL);
        FXMLLoader loader = new FXMLLoader(Main.class.getResource("Error.fxml"));
        try {
            Parent root = loader.load();
            ((ErrorController)loader.getController()).setErrorText(errorMsg.toString());
            dialog.setScene(new Scene(root, 250, 400));
            dialog.show();
        } catch (IOException exc) {
            exc.printStackTrace();
        }
    }

//  public static void main(String[] args) {
//      launch(args);
//  }
}

使用 Main.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.geometry.Insets?>

<HBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller"
    alignment="center" spacing="5">
    <children>
        <Button text="Do something safe" onAction="#safeHandler" />
        <Button text="Do something risky" onAction="#riskyHandler" />
        <Label fx:id="label" />
    </children>
    <padding>
        <Insets top="10" left="10" right="10" bottom="10" />
    </padding>
</HBox>

Controller.java:

package application;

import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    private final IntegerProperty counter = new SimpleIntegerProperty(1);

    @FXML
    private Label label ;

    public void initialize() throws Exception {
        label.textProperty().bind(Bindings.format("Count: %s", counter));

        // uncomment the next line to demo exceptions in the start() method:
        // throw new Exception("Initializer exception");
    }

    @FXML
    private void safeHandler() {
        counter.set(counter.get()+1);
    }

    @FXML
    private void riskyHandler() throws Exception {
        if (Math.random() < 0.5) {
            throw new RuntimeException("An unknown error occurred");
        }
        safeHandler();
    }
}

Error.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.ErrorController">
    <center>
        <ScrollPane>
            <content>
                <Label fx:id="errorMessage" wrapText="true" />
            </content>
        </ScrollPane>
    </center>
    <bottom>
        <HBox alignment="CENTER">
            <Button text="OK" onAction="#close"/>
        </HBox>
    </bottom>
</BorderPane>

ErrorController.java:

package application;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class ErrorController {
    @FXML
    private Label errorMessage ;

    public void setErrorText(String text) {
        errorMessage.setText(text);
    }

    @FXML
    private void close() {
        errorMessage.getScene().getWindow().hide();
    }
}

我现在知道是什么导致了我的UncaughtExceptionHandler中的异常:我尝试通过JavaFX显示对话框,显然舞台没有被初始化,所以我得到了一个异常。很抱歉将此添加为评论,但除了在SO上进行沟通外,没有其他方法;-) - Hannes
有没有不需要使用FXML的更简单的解决方案? - Hexworks
@Hexworks 不确定你在这里问什么。showErrorDialog()方法可以做任何你想要的事情 - 当然它不强制使用FXML。 - James_D

1

这实际上有点棘手,我以前遇到过同样的问题,但我想不出任何优雅的解决方案。显然,一种非常笨重的处理方式(而且说实话,可能完全错误的方式)是在每个控制器类方法(以 @FXML 开头的方法)中,将整个方法体包裹在 try{} catch(Throwable t){} 块中,然后在可抛出的 catch 中,对异常结果进行分析,以尝试确定在灾难事件发生时向用户显示什么有用信息。

值得注意的是,在Javafx 8中(我没有尝试过2.0-2.2),如果您尝试在加载FXML的地方包装它(例如在应用程序的主“Start”方法中),使用相同类型的throwable块,它将无法捕获来自控制器类的异常,这似乎意味着该线程与FXML控制器类中使用的线程存在某种分离。然而,它绝对处于同一应用程序线程中,因为如果在调用类中保留对Thread.currentThread();对象的引用,然后在控制器中执行相同操作,则两者的.equals将返回true。因此,在底层,Javafx正在执行某些操作,以将未经检查的异常与这些类分离。 我没深入研究过这个问题。老实说,我甚至不愿意提供这个答案,因为我担心有人会在没有正确理解的情况下使用它。所以,如果有人提供更好的答案,我会立即删除这个答案。祝好运!

无论如何还是非常感谢!但是Oracle的人一定已经考虑过这个问题,肯定有一个聪明的解决方案,你不觉得吗?也许他们中的任何一个正在阅读这篇文章并能给出答案... - Hannes
这正是我所期望的,我也非常希望能够解决这个问题。也许我可以拆开Ensemble的代码示例,看看它们是如何处理它的(如果它们处理它的话)。 - WillBD
Jamnes_D提出的解决方案在你的环境中是否有效? - Hannes

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