2014-10-14 9 views
9

Biorąc pod uwagę kontroler Sceny wywołuje kod biznesowy wywołujący wyjątek. Jak poradzić sobie z takimi wyjątkami w sposób ogólny?Ogólne obsługa wyjątków w JavaFX 8

Próbowałem metody Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler), ale nie jest wywoływany, więc uważam, że wyjątki są gdzieś wewnątrz struktury JavaFX.

Co mogę zrobić, aby obsłużyć Wyjątki lub przynajmniej pokazać użytkownikowi przydatne informacje?

Odpowiedz

20

Począwszy od oprogramowania JavaFX 8, Thread.setDefaultUncaughtExceptionHandler(...) powinno działać: patrz: RT-15332.

Rzeczy są nieco skomplikowane, jeśli wystąpi nieprzechwycony wyjątek podczas wykonywania metody start(...). W zależności od sposobu uruchomienia aplikacji kod wywołujący start() (na przykład implementacja Application.launch(...)) może wychwycić wyjątek i obsłużyć go, co oczywiście uniemożliwiłoby wywołanie domyślnej procedury obsługi wyjątku.

W szczególności, w moim systemie (JDK 1.8.0_20 na Mac OS X 10.9.5), wydaje się, że jeśli moja aplikacja uruchamia się za pomocą metody, która wywołuje main(...)Application.launch(...), każdy wyjątek rzucony w metodzie start() zostanie złapany (i nie powtórnie rzucony).

Jeśli jednak usuniemy metodę main(...) (patrz uwaga poniżej) i uruchomię aplikację bezpośrednio, każdy wyjątek zgłoszony w metodzie start() zostanie ponownie zgłoszony, umożliwiając wywołanie domyślnej procedury obsługi wyjątku. Zauważ, że nie tylko się rozprzestrzenia. start() jest wywoływany w wątku aplikacji FX, a wyjątek jest ponownie wysyłany z wątku głównego.W rzeczywistości, gdy to nastąpi, kod w domyślnej procedurze obsługi, która zakłada, że ​​wątek aplikacji FX działa, nie działa: więc domyślam się, że kod uruchamiania w tym przypadku przechwytuje wyjątki w metodzie start(), aw bloku catch zamyka FX Application Thread, a następnie ponownie wyrzuca wyjątek z wątku wywołującego.

Wszystko to jest ważne - jeśli chcesz, aby Twój domyślny program obsługi obsługiwał wyjątki w metodzie start(), nie powinieneś wywoływać żadnego kodu interfejsu użytkownika, jeśli wyjątek nie zostanie zgłoszony w wątku aplikacji FX (nawet przez a Platform.runLater(...)).

Uwaga: (dla tych, którzy mogą nie być tego świadomi). Od wersji Java 8 można bezpośrednio uruchomić podklasę Application, nawet jeśli nie ma ona metody main(...), przekazując nazwę klasy jako argument do pliku wykonywalnego JVM w zwykły sposób (tj. java MyApp). Robi to, czego oczekujesz: uruchamia zestaw narzędzi FX, uruchamia wątek aplikacji FX, tworzy instancję Application i wywołuje init(), a następnie wywołuje wątek aplikacji FX start(). Interesująco (i być może niepoprawnie), metoda , która wywołuje Application.launch(), zachowuje się nieco inaczej w odniesieniu do niezatłoczonych wyjątków w metodzie start(...).

Oto podstawowy przykład. Odkomentuj kod w Controller.initialize(), aby wyświetlić wyjątek zgłoszony w metodzie 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); 
// } 
} 

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

Teraz wiem, co spowodowało wyjątek w moim UncaughtExceptionHandler: Próbowałem wyświetlić okno dialogowe za pośrednictwem JavaFX i najwyraźniej etap nie został zainicjowany, a więc otrzymałem wyjątek. Przepraszam, że dodałem to jako komentarz, ale nie ma innego sposobu niż komunikacja na SO ;-) – Hannes

0

To jest naprawdę trudne, wcześniej spotkałem się z tym samym problemem i nie mogłem wymyślić żadnych eleganckich rozwiązań. Oczywiście, jeden naprawdę ciężki sposób (i szczerze, prawdopodobnie całkowicie niewłaściwy sposób) poradzenia sobie z tym, jest w każdej z metod klasy kontrolera (tych, które zaczynają się od @FXML), owinąć cały korpus metody w bloku try{} catch(Throwable t){} , a następnie wewnątrz twojego throwable catch, wykonaj analizę wyniku wyjątku, aby spróbować określić, jakie użyteczne informacje pokażą użytkownikowi w razie katastrofy.

Warto również zauważyć, że przynajmniej w Javafx 8 (nie próbowałem z wersją 2.0-2.2), jeśli próbujesz owijać miejsce, w którym ładujesz FXML (jak w głównej aplikacji "Metoda", na przykład), w tym samym rodzaju bloku , to Nie przechwytuje wyjątku z klasy kontrolera, co wydaje się implikować pewien rodzaj separacji między tym wątkiem a tym, który jest używany w klasie kontrolera FXML. Jednak jest to zdecydowanie w tym samym wątku aplikacji, ponieważ jeśli zachowujesz odniesienie do obiektu Thread.currentThread(); w klasie wywołującej, a następnie wykonaj to samo w kontrolerze, wartości .equals na tych dwóch będą prawdziwe. Tak więc pod prześcieradłami Javafx robi trochę magii, aby oddzielić nie sprawdzone wyjątki od tych klas.

Nie szukałem więcej w tym niż to.

Prawdę mówiąc, nienawidzę nawet posiadania tej odpowiedzi tutaj, ponieważ obawiam się, że ktoś użyje jej bez właściwego zrozumienia, jak niepoprawne jest to. W związku z tym, jeśli ktoś wpisze lepszą odpowiedź, usuniemy to od razu.

Powodzenia!

+0

Wielkie dzięki! Ale chłopaki z Oracle MUSZĄ myśleć o tym problemie i musi być sprytne rozwiązanie, nie sądzisz? Może któreś z nich czyta to, może dać odpowiedź ... – Hannes

+0

To jest coś, na co mam nadzieję, ja również bardzo bym to rozwiązał. Być może uda mi się rozebrać próbki kodu z Ensemble i zobaczyć, jak sobie z nimi poradzą (jeśli sobie z tym poradzą). – WillBD

+0

Czy rozwiązanie zaproponowane przez Jamnesa_D działa w twoim środowisku? – Hannes