JavaFX 8 中的一般异常处理

Posted

技术标签:

【中文标题】JavaFX 8 中的一般异常处理【英文标题】:General Exception handling in JavaFX 8 【发布时间】:2014-12-09 06:59:19 【问题描述】:

给定场景的控制器调用引发异常的业务代码。如何以一般方式处理这些异常?

我尝试了 Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler) 方法,但它没有被调用,所以我相信异常是在 JavaFX 框架内的某个地方捕获的。

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

【问题讨论】:

【参考方案1】:

从 JavaFX 8 开始,Thread.setDefaultUncaughtExceptionHandler(...) 应该可以工作:请参阅 RT-15332。

如果在执行start(...) 方法期间发生未捕获的异常,情况会有些复杂。根据应用程序的启动方式,调用start()的代码(例如Application.launch(...)的实现)可能会捕获异常并处理它,这显然会阻止调用默认的异常处理程序。

特别是,在我的系统(Mac OS X 10.9.5 上的 JDK 1.8.0_20)上,如果我的应用程序通过调用 Application.launch(...)main(...) 方法启动,@987654328 中会引发任何异常@ 方法被捕获(而不是重新抛出)。

但是,如果我删除 main(...) 方法(请参阅下面的注释)并直接启动应用程序,则 start() 方法中抛出的任何异常都会重新抛出,从而允许调用默认异常处理程序。请注意,它不仅仅是向上传播。在 FX 应用程序线程上调用 start(),并从主线程重新抛出异常。实际上,当这种情况发生时,假定 FX 应用程序线程正在运行的默认处理程序中的代码无法运行:所以我的猜测是这种情况下的启动代码在 start() 方法和 catch 块中捕获异常,关闭FX Application Thread,然后从调用线程重新抛出异常。

所有这一切的结果是它很重要 - 如果您希望您的默认处理程序处理 start() 方法中的异常,如果没有在 FX 应用程序线程上引发异常,则不应调用任何 UI 代码(即使通过Platform.runLater(...))。

注意:(对于那些可能不知道这一点的人)。从 Java 8 开始,您可以直接启动 Application 子类,即使它没有 main(...) 方法,也可以通过将类名作为参数以常规方式(即 java MyApp)传递给 JVM 可执行文件。这符合您的预期:启动 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 上交流之外别无他法 ;-) 没有fxml没有更简单的解决方案吗? @Hexworks 不知道你在这里问什么。 showErrorDialog() 方法可以做任何你想做的事情——当然它不是强制使用 FXML。【参考方案2】:

这实际上有点棘手,我之前遇到过同样的问题,但我想不出任何优雅的解决方案。显然,处理这个问题的一种非常严厉的方法(老实说,可能是完全错误的方法)是在每个控制器类方法(以@FXML 开头的方法)中,将方法的整个主体包装在@987654321 @block,然后在你的 throwable catch 内部,对异常的结果做一些分析,尝试确定在发生灾难时向用户显示哪些有用的信息。

同样值得注意的是,至少在 Javafx 8 中(我还没有尝试过 2.0-2.2)如果您尝试包装加载 FXML 的位置(例如在您的应用程序的主“开始”方法中) ,在同一种throwable 块中,它没有从Controller 类中捕获异常,这似乎意味着该线程与FXML Controller 类中使用的线程之间存在某种分离。但是,它肯定在同一个应用程序线程上,因为如果您在调用类中保留对 Thread.currentThread(); 对象的引用,然后在控制器中执行相同操作,则两者上的 .equals 将变为 true。因此,在工作表下,Javafx 正在做一些魔术来从这些类中分离未经检查的异常。

我没有比这更进一步。

说实话,我什至讨厌把这个答案放在这里,因为我担心有人会在没有正确理解这是多么不正确的情况下使用它。因此,如果有人提出更好的答案,我将立即删除它。

祝你好运!

【讨论】:

非常感谢!但是甲骨文的人必须考虑过这个问题,并且必须有一个聪明的解决方案,你不觉得吗?也许他们中的任何一个都在阅读这个可以给出答案...... 这就是我所希望的,我也非常希望有一个解决方案。也许我可以从 Ensemble 中拆开代码示例,看看他们是如何处理的(如果他们处理的话) Jamnes_D 提出的解决方案是否适用于您的环境?

以上是关于JavaFX 8 中的一般异常处理的主要内容,如果未能解决你的问题,请参考以下文章

java中的异常处理---捕获异常或者抛出异常

Java 8 lambda表达式中的异常处理

Java 8 lambda表达式中的异常处理

java 8 lambda表达式中的异常处理

Python 异常处理总结

Python学习异常处理