Developpez.com - Rubrique Java

Le Club des Développeurs et IT Pro

Bonnes pratiques pour exécuter une tâche de fond en JavaFX,

Un tutoriel de Fabrice Bouyé

Le 2015-02-20 01:44:54, par bouye, Rédacteur/Modérateur
Bonjour,
je vous propose un nouvel article concernant JavaFX, cette fois-ci sur les bonnes pratiques pour exécuter une tâche de fond, de longue durée ou récurrente sans pour autant bloquer votre interface graphique :

http://fabrice-bouye.developpez.com/...thread-javafx/

Vous pouvez profiter de ce message pour partager vos commentaires. Cet article a été commencé sous JavaFX 2.2 il y a plus d'un an et j'ai ensuite mis à jour certaines parties pour JavaFX 8 qui a entre autres rajouté une classe permettant d’effectuer des tâches récurrentes, donc, surtout, n’hésitez pas à m'indiquer toutes erreurs, omissions ou encore des éventuels anomalies.
  Discussion forum
17 commentaires
  • Phoste
    Nouveau membre du Club
    Bonjour, j'ai lu ton tutoriel sur les Service et les Task mais j'ai tout de même un problème avec l'un des mes programmes ...
    J'obtiens le message d'erreur suivant : Exception in thread "MQTT Call: ID1" java.lang.IllegalStateException: Not on FX application thread; currentThread = MQTT Call: ID1
    Je créé pourtant un Service qui établi la connection avec la base de donnée MQTT et qui souscrit à une valeur de la base de donnée mqtt. Voici la partie de code qui pose problème :

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    import javafx.concurrent.Service;
    import javafx.concurrent.Task;
    import javafx.scene.image.ImageView;
    import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
    import org.eclipse.paho.client.mqttv3.MqttCallback;
    import org.eclipse.paho.client.mqttv3.MqttClient;
    import org.eclipse.paho.client.mqttv3.MqttException;
    import org.eclipse.paho.client.mqttv3.MqttMessage;
    
    public class Led extends ImageView implements MqttCallback {
        
        private static final String BROKER = "tcp://255.255.255.255:1883";  // adresse ip du serveur mqtt
        
        private final String name;
        private final String id;
        private static int idCount = 1;
        
        public Led(String name) {
            this.name = name.toUpperCase();
            this.id = "IDLED"+idCount++;
            this.getStyleClass().add("led_on");
            Service<Void> connectionMqtt = new Service<Void>() {
                
                @Override
                protected Task<Void> createTask() {
                    return new Task<Void>() {
    
                        @Override
                        protected Void call() throws Exception {
                            try {
                                MqttClient client = new MqttClient(BROKER, Led.this.id);
                                client.setCallback(Led.this);
                                client.connect();
                                client.subscribe("A000009/"+Led.this.name);
                            } catch(MqttException e) {
                                e.printStackTrace();
                            }
                            return null;
                        }                    
                    };
                }            
            };
            connectionMqtt.start();
        }
        
        @Override
        public void connectionLost(Throwable thrwbl) {
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }
    
        @Override
        public void messageArrived(String string, MqttMessage mm) throws Exception {
            if (mm.toString().equals("1"))
                this.getStyleClass().setAll("led_on");
            else
                this.getStyleClass().setAll("led_off");
        }
    
        @Override
        public void deliveryComplete(IMqttDeliveryToken imdt) {
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
        }
        
    }
    Alors que la partie de code qui permet de publier un message à la base de donnée mqtt ne pose aucun soucis et ne renvoie par d'exception :

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    import javafx.scene.control.Button;
    import org.eclipse.paho.client.mqttv3.MqttClient;
    import org.eclipse.paho.client.mqttv3.MqttException;
    import org.eclipse.paho.client.mqttv3.MqttMessage;
    
    public class Switch extends Button {
        
        private static final String BROKER = "tcp://255.255.255.255:1883";
        private static final int QOS = 2;
        
        private String name;
        private String status = "ON";
        private final String id;
        private static int idCount = 1;
        private MqttClient client;
        
        public Switch(String name) {
            this.name = name.toUpperCase();
            this.id = "IDSWI"+idCount++;
            try {
                client = new MqttClient(BROKER, this.id);
            } catch(MqttException e) {
                e.printStackTrace();
            }
        }
        
        public void publish() {
            try {
                client.connect();
                MqttMessage message = new MqttMessage(status.getBytes());
                status = (status.equals("ON")) ? "OFF" : "ON";
                message.setQos(QOS);
                client.publish("A000009/"+this.name, message);
                client.disconnect();
            } catch (MqttException e) {
                e.printStackTrace();
            }
        }
        
    }
    L'idée est que le LED réagisse au changement de valeur de la base de donnée mqtt

    Merci d'avance
  • bouye
    Rédacteur/Modérateur
    Pour rappel, le corps de la Task s’exécute dans un autre thread.

    D’après ton erreur, ici tu appelles, depuis un autre thread, quelques chose qui devrait en fait s'effectuer dans le JavaFX Application Thread.

    Donc je suppose, a priori, qu'il s'agit de quelque chose que tu appelles dans ta tache (puisque tu n'as pas donne de trace ou de numéro de ligne pour cette erreur) :

    Code :
    1
    2
    3
    4
    MqttClient client = new MqttClient(BROKER, Led.this.id);
    client.setCallback(Led.this);
    client.connect();
    client.subscribe("A000009/"+Led.this.name);
    Il faudrait donc trouver ce dont il s'agit.
    Ça serait plus facile si je savais quelle ligne provoque l'erreur
  • Phoste
    Nouveau membre du Club
    Alors, niveau des erreurs j'obtiens ceci :

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    Exception in thread "MQTT Call: IDLED1" java.lang.IllegalStateException: Not on FX application thread; currentThread = MQTT Call: IDLED1
    	at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:204)
    	at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:364)
    	at javafx.scene.Scene.addToDirtyList(Scene.java:485)
    	at javafx.scene.Node.addToSceneDirtyList(Node.java:424)
    	at javafx.scene.Node.impl_markDirty(Node.java:415)
    	at javafx.scene.Node.impl_geomChanged(Node.java:3784)
    	at javafx.scene.image.ImageView.access$300(ImageView.java:143)
    	at javafx.scene.image.ImageView$1.invalidated(ImageView.java:223)
    	at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:111)
    	at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:145)
    	at javafx.scene.image.ImageView.setImage(ImageView.java:189)
    	at javafx.scene.image.ImageView$2.invalidated(ImageView.java:259)
    	at javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:109)
    	at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:143)
    	at javafx.css.StyleableStringProperty.set(StyleableStringProperty.java:83)
    	at javafx.css.StyleableStringProperty.applyStyle(StyleableStringProperty.java:69)
    	at javafx.css.StyleableStringProperty.applyStyle(StyleableStringProperty.java:45)
    	at javafx.scene.CssStyleHelper.resetToInitialValues(CssStyleHelper.java:452)
    	at javafx.scene.CssStyleHelper.createStyleHelper(CssStyleHelper.java:180)
    	at javafx.scene.Node.reapplyCss(Node.java:8785)
    	at javafx.scene.Node.impl_reapplyCSS(Node.java:8748)
    	at javafx.scene.Node$3.onChanged(Node.java:998)
    	at com.sun.javafx.collections.TrackableObservableList.lambda$new$20(TrackableObservableList.java:45)
    	at com.sun.javafx.collections.TrackableObservableList$$Lambda$54/602720129.onChanged(Unknown Source)
    	at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
    	at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
    	at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
    	at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
    	at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
    	at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
    	at javafx.collections.ModifiableObservableListBase.setAll(ModifiableObservableListBase.java:90)
    	at javafx.collections.ObservableListBase.setAll(ObservableListBase.java:250)
    	at simulateur_v2.Led.messageArrived(Led.java:60)
    	at org.eclipse.paho.client.mqttv3.internal.CommsCallback.handleMessage(CommsCallback.java:354)
    	at org.eclipse.paho.client.mqttv3.internal.CommsCallback.run(CommsCallback.java:162)
    	at java.lang.Thread.run(Thread.java:745)
    J'espère que cela pourra t'aider car moi je bloque ...
    Autre chose... J'ai exécuté le même code sur un PC à mon école et je n'ai eu aucune exception. Est-il possible que cela change d'un environnement à un autre ? (mon netbeans est à la v8.0.1 et celui de l'école à la v8.0)
  • bouye
    Rédacteur/Modérateur
    Le problème semble avoir donc une cause assez simple : tu as passé ta LED comme callback or cette dernière a reçu un message et a tenté de le traiter dans un thread hors du JavaFX Application Thread. Comme c'est du multithread, l'ordre de séquences d'instructions peut varier suivant plein de facteurs (rapidité/lenteur d’exécution, etc...) donc ça marche d’un coté (école) et pas de l'autre (maison) ; mais le problème persiste : tu as tenté une modification de la scène depuis un thread autre que le JavaFX Application Thread, donc *pouf* ça plante.

    Code :
    at simulateur_v2.Led.messageArrived(Led.java:60)
    Il faut donc s'assurer que le message arrive et est traité dans le JavaFX Application Thread puisque ce dernier manipule la scène (via la modif de style).

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override
        public void messageArrived(String string, MqttMessage mm) throws Exception {
            if (!Platform.isFxApplicationThread()) {
                Platform.runLater(() -> messageArrived(mm));
            } else {
                String ledStyle = (mm.toString().equals("1")) ? "led_on" : "led_off";
                this.getStyleClass().setAll(ledStyle);
            }
        }
  • Phoste
    Nouveau membre du Club
    En fait ça ne fonctionnait pas mieux à mon école, je n'avais juste pas d'exception qui avait été levée donc aucun message ...
    Ça marche avec ta solution merci !
    Je n'ai même pas besoin de me connecter à la db sur un autre thread (il le fait peut-être seul ?)
  • bouye
    Rédacteur/Modérateur
    Faut voir avec ta méthode de connexion sur la DB. Sur une DB distante avec une connexion réseau lente, un serveur surchargé ou avec une authentification lente, ça pourrait éventuellement prendre un temps non négligeable et donc ça bloquerait ton UI si tu ne passait pas par un service. Ici, on prend tout le temps qu'il faut dans la tache et seul le changement d'état de la LED a lieu dans le JavaFX Application Thread, donc c'est pile poil.
  • foxfx
    Nouveau Candidat au Club
    Envoyé par bouye
    Bonjour,
    je vous propose un nouvel article concernant JavaFX, cette fois-ci sur les bonnes pratiques pour exécuter une tâche de fond, de longue durée ou récurrente sans pour autant bloquer votre interface graphique :

    http://fabrice-bouye.developpez.com/...thread-javafx/

    Vous pouvez profiter de ce message pour partager vos commentaires. Cet article a été commencé sous JavaFX 2.2 il y a plus d'un an et j'ai ensuite mis à jour certaines parties pour JavaFX 8 qui a entre autres rajouté une classe permettant d’effectuer des tâches récurrentes, donc, surtout, n’hésitez pas à m'indiquer toutes erreurs, omissions ou encore des éventuels anomalies.
    J'ai tout l'article, il est clair et les exemples sont aident à la compréhension des API de concurrence de JavaFX merci pour cet article !
    J'ai juste une remarque sur la récupération de la raison de l'erreur, je pense qu'il s'agit de :
    Pour récupérer la raison de l'échec d'un service, il suffit d'appeler la méthode getException() du service dans le listener ou le callback approprié.
    à la place de
    Pour récupérer la raison de l'échec d'un service, il suffit d'appeler la méthode getError() du service dans le listener ou le callback approprié.
  • bouye
    Rédacteur/Modérateur
    Merci, ça aura échappé aux multiples relectures (/doh). Je corrige de suite.
  • Gouyon
    Membre expérimenté
    Très bon tutoriel

    Par contre j'ai un cas qui fonctionne très mal.
    Le code ci dessous charge un scénario pour un jeu et prépare l'affichage. Le chargement dure environ une quinzaine de seconde ce qui est long et peut faire penser que le programme est planté.

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public void chargerJeu(String Nom) {
    		try {
    			FileInputStream fSvg = new FileInputStream(Nom);
    			ObjectInputStream svgBat = new ObjectInputStream(fSvg);
    			scenario = (Jeu) svgBat.readObject();
    			prepareScenario();
    
    			fondDeCarte = new Visu(scenario.getLeTerrain(), scenario.getFeux());
    			vueCarte.setImage(fondDeCarte.getImage());
    			int fig = 0;
    			Pion unite = scenario.getPion(fig++);
    			while (unite != null) {
    				if (!unite.estAlaBase())
    					affiche(unite);
    				if (unite instanceof Vehicule) {
    					((Vehicule) unite).majCarc(paramJeu);
    					if (!unite.estAlaBase())
    						doCalcNouvGraphePour((Vehicule) unite, unite.getX(), unite.getY());
    				}
    				unite = scenario.getPion(fig++);
    			}
    			centreEnXY(0, 0);
    			changeZoom(1);
    			corrigeLocation(0, 0);
    			jgeCtrl.MAJ();
    			jgeCtrl.razMessage();
    			jgeCtrl.setUniteEnMain("", "", null);
    			cmdCtrl.setMiniCarte(fondDeCarte.getImage());
    			cmdCtrl.enableAll(true);
    			cmdCtrl.MAJCmd();
    		} catch (IOException | ClassNotFoundException e1) {
    
    			e1.printStackTrace();
    		}
    		
    	}
    Du coups j'ai voulu mettre une boite de dialogue qui montre la progression du chargement. J'ai donc écrit le code ci dessous

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    	private void doLoadGame(String Nom) {
    		final Cursor oldCursor = primaryScene.getCursor();
    		primaryScene.setCursor(Cursor.WAIT);
    		final Stage dialStage = new Stage();
    		
    
    		final Service<Void> loadGameService = new Service<Void>() {
    
    			@Override
    			protected Task<Void> createTask() {
    				return new Task<Void>() {
    
    					@Override
    					protected Void call() throws Exception {
    						updateMessage("Lecture");
    						FileInputStream fSvg = new FileInputStream(Nom);
    						ObjectInputStream svgBat = new ObjectInputStream(fSvg);
    						scenario = (Jeu) svgBat.readObject();
    						final int nb = scenario.getNbVehicule() + 2;
    						updateMessage("Graphismes");
    						updateProgress(1, nb);
    						prepareScenario();
    						updateMessage("Carte");
    						fondDeCarte = new Visu(scenario.getLeTerrain(), scenario.getFeux());
    						vueCarte.setImage(fondDeCarte.getImage());
    						updateMessage("Calcul des mouvements");
    						int fig = 0;
    						Pion unite = scenario.getPion(fig++);
    						while (unite != null) {
    							updateProgress(fig + 2, nb);
    							if (!unite.estAlaBase())
    								affiche(unite);
    							if (unite instanceof Vehicule) {
    								((Vehicule) unite).majCarc(paramJeu);
    								if (!unite.estAlaBase())
    									doCalcNouvGraphePour((Vehicule) unite, unite.getX(), unite.getY());
    							}
    							unite = scenario.getPion(fig++);
    						}
    						return null;
    					}
    				};
    			}
    		};
    
    		loadGameService.setOnSucceeded((WorkerStateEvent event) -> {
    			primaryScene.setCursor(oldCursor);
    			centreEnXY(0, 0);
    			changeZoom(1);
    			corrigeLocation(0, 0);
    			jgeCtrl.MAJ();
    			jgeCtrl.razMessage();
    			jgeCtrl.setUniteEnMain("", "", null);
    			cmdCtrl.setMiniCarte(fondDeCarte.getImage());
    			cmdCtrl.enableAll(true);
    			cmdCtrl.MAJCmd();
    			dialStage.close();
    		});
    
    		FXMLLoader loader = new FXMLLoader();
    		URL url = MainFF.class.getResource("view/DialAttente.fxml");
    		loader.setLocation(url);
    
    		try {
    			AnchorPane dial = (AnchorPane) loader.load();
    			dialStage.initModality(Modality.NONE);
    			dialStage.initOwner(primaryStage);
    			Scene scene = new Scene(dial);
    			dialStage.setScene(scene);
    			DialAttenteController dialCtrl = loader.getController();
    			dialCtrl.setDialStage(dialStage, "Charger");
    			dialCtrl.getProg().progressProperty().bind(loadGameService.progressProperty());
    			dialCtrl.getTache().textProperty().bind(loadGameService.messageProperty());
    			dialStage.show();
    			loadGameService.start();
    
    		} catch (IOException e) {
    
    			e.printStackTrace();
    		}
    
    	}
    Mais j'ai un allongement extraordinaire du temps d'exécution supérieur à la minute avec au final des exceptions de pointeur null car il semble terminer la tache avant la fin.
    Je ne comprend pas bien ce qui se passe.
  • bouye
    Rédacteur/Modérateur
    Et, euh, ça plante où, comment, avec quoi comme message ?
    Tu as essayer de tester avec une simple ProgressBar ou un ProgressMonitor dans un Stage au lieu de ton dialogue maison FXML ?