Tutoriel sur l'exécution d'une tâche de fond en JavaFX

Comment lancer un traitement lourd en arrière-plan pour ne pas bloquer son UI JavaFX.

Fabrice Bouyé

Cet article a pour but de vous expliquer comment exécuter une tâche de fond en JavaFX sans avoir à recourir à un Thread lancée manuellement et sans bloquer l'exécution du thread principal de gestion des événements.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez

- Titre : Tutoriel sur l'exécution d'une tâche de fond en JavaFX

- Auteur : Fabrice Bouyé

- Parution : 18 mars 2015

- Mise à jour : 24 mars 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Fabrice Bouyé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Problématique

- II-A. Exemple

- II-B. Raisonnement

- II-C. Solution

- III. Interfaces et classes importantes

- III-A. javafx.concurrent.Worker<V>

- III-B. javafx.concurrent.Service<V>

- III-C. javafx.concurrent.Task<V>

- III-D. Javafx.concurrent.ScheduledService<V>

- IV. Utiliser un service

- IV-A. Création et lancement

- IV-B. Annulation

- IV-C. Relancer un service

- IV-D. Avancement du service

- IV-E. État du service

- IV-E-1. ChangeListener sur l'état du service

- IV-E-2. Callback sur le service

- IV-F. Récupérer le résultat

- IV-G. Récupérer la raison de l'échec

- V. Utiliser une tâche

- V-A. Notification de l'avancement

- V-B. Annulation

- V-B-1. Lors d'un appel bloquant

- V-C. État d'une tâche

- V-D. Produire des résultats partiels

- V-D-1. JDK 7

- V-D-2. JDK 8

- V-E. Utiliser une tâche dans un contexte non JavaFX

- VI. Utiliser un service répétable

- VI-A. Délai avant l'exécution

- VI-B. Période d'exécution

- VI-C. Gestion des échecs

- VI-D. Derniers bons résultats

- VII. Conclusion

- VIII. Remerciements

- IX. Liens

I - Introduction

Comme dans n'importe quel logiciel, il peut arriver que l'utilisateur soit amené à exécuter une tâche de longue durée pour par exemple un calcul, charger ou sauvegarder un fichier, effectuer une requête web ou sur une base de données ou encore faire une pause pendant une durée déterminée ou effectuer une boucle infinie pour surveiller l'état d'un port, d'un périphérique ou d'un fichier.

Or si on ne prend pas attention à la manière dont ces tâches longues sont traitées, il est très facile de bloquer le fonctionnement de l'UI, ce qui laissera à penser à l'utilisateur que le programme a planté puisque plus rien ne répond à ses saisies. Fort heureusement, l'API JavaFX fournit via son API de concurrence une interface et une paire de classes tout spécialement dédiées à la gestion des tâches de fond, ce qui facilite grandement la gestion de ce genre d'opérations.

II - Problématique

II-A - Exemple

Prenons un problème simple : nous disposons d'une UI contenant un simple bouton et nous allons lancer le calcul de 1 000 000 d'entiers lorsque nous cliquons sur ce bouton. Nous allons également changer le curseur de l'UI et le remplacer par un curseur d'attente et nous désactiverons les boutons permettant de lancer le calcul pendant que ce dernier tourne.

Code JDK7

1. package test;

3. import javafx.application.Application;

4. import javafx.application.Platform;

5. import javafx.event.ActionEvent;

6. import javafx.event.EventHandler;

7. import javafx.scene.Cursor;

8. import javafx.scene.Scene;

9. import javafx.scene.control.Button;

10. import javafx.scene.control.Menu;

11. import javafx.scene.control.MenuBar;

12. import javafx.scene.control.MenuItem;

13. import javafx.scene.layout.BorderPane;

14. import javafx.scene.layout.StackPane;

15. import javafx.stage.Stage;

17. public class Main extends Application {

19. private MenuItem calculateItem;

20. private Button calculateButton;

21. private Scene scene;

23. @Override

24. public void start(Stage primaryStage) {

25. final MenuItem exitItem = new MenuItem("Quitter");

26. exitItem.setOnAction(new EventHandler<ActionEvent>() {

28. @Override

29. public void handle(ActionEvent t) {

30. Platform.exit();

31. }

32. });        final Menu fileMenu = new Menu("Fichier");

33. fileMenu.getItems().add(exitItem);

34. calculateItem = new MenuItem("Calculer");

35. calculateItem.setOnAction(new EventHandler<ActionEvent>() {

37. @Override

38. public void handle(ActionEvent t) {

39. doCalculate();

40. }

41. });

42. final Menu actionMenu = new Menu("Action");

43. actionMenu.getItems().add(calculateItem);

44. final MenuBar menuBar = new MenuBar();

45. menuBar.getMenus().setAll(fileMenu, actionMenu);

46. calculateButton = new Button();

47. calculateButton.setText("Lancer le calcul !");

48. calculateButton.setOnAction(new EventHandler<ActionEvent>() {

50. @Override

51. public void handle(ActionEvent event) {

52. doCalculate();

53. }

54. });

55. StackPane center = new StackPane();

56. center.getChildren().add(calculateButton);

57. final BorderPane root = new BorderPane();

58. root.setTop(menuBar);

59. root.setCenter(center);

60. scene = new Scene(root, 300, 250);

61. primaryStage.setTitle("Test");

62. primaryStage.setScene(scene);

63. primaryStage.show();

64. }

66. private void doCalculate() {

67. final Cursor oldCursor = scene.getCursor();

68. scene.setCursor(Cursor.WAIT);

69. calculateItem.setDisable(true);

70. calculateButton.setDisable(true);

71. final int maxIterations = 1000000;

72. for (int iterations = 0; iterations < maxIterations; iterations ++) {

73. System.out.println(iterations);

74. }

75. scene.setCursor(oldCursor);

76. calculateItem.setDisable(false);

77. calculateButton.setDisable(false);

78. }

80. public static void main(String[] args) {

81. launch(args);

82. }

83. }

Code JDK8

1. package test;

3. import javafx.application.Application;

4. import javafx.application.Platform;

5. import javafx.event.ActionEvent;

6. import javafx.scene.Cursor;

7. import javafx.scene.Scene;

8. import javafx.scene.control.Button;

9. import javafx.scene.control.Menu;

10. import javafx.scene.control.MenuBar;

11. import javafx.scene.control.MenuItem;

12. import javafx.scene.layout.BorderPane;

13. import javafx.scene.layout.StackPane;

14. import javafx.stage.Stage;

16. public class Main extends Application {

18. private MenuItem calculateItem;

19. private Button calculateButton;

20. private Scene scene;

22. @Override

23. public void start(Stage primaryStage) {

24. final MenuItem exitItem = new MenuItem("Quitter");

25. exitItem.setOnAction((ActionEvent t) -> Platform.exit());

26. final Menu fileMenu = new Menu("Fichier");

27. fileMenu.getItems().add(exitItem);

28. calculateItem = new MenuItem("Calculer");

29. calculateItem.setOnAction((ActionEvent t) -> doCalculate());

30. final Menu actionMenu = new Menu("Action");

31. actionMenu.getItems().add(calculateItem);

32. final MenuBar menuBar = new MenuBar();

33. menuBar.getMenus().setAll(fileMenu, actionMenu);

34. calculateButton = new Button();

35. calculateButton.setText("Lancer le calcul !");

36. calculateButton.setOnAction((ActionEvent event) -> doCalculate());

37. StackPane center = new StackPane();

38. center.getChildren().add(calculateButton);

39. final BorderPane root = new BorderPane();

40. root.setTop(menuBar);

41. root.setCenter(center);

42. scene = new Scene(root, 300, 250);

43. primaryStage.setTitle("Test");

44. primaryStage.setScene(scene);

45. primaryStage.show();

46. }

48. private void doCalculate() {

49. final Cursor oldCursor = scene.getCursor();

50. scene.setCursor(Cursor.WAIT);

51. calculateItem.setDisable(true);

52. calculateButton.setDisable(true);

53. final int maxIterations = 1000000;

54. for (int iterations = 0; iterations < maxIterations; iterations ++) {

55. System.out.println(iterations);

56. }

57. scene.setCursor(oldCursor);

58. calculateItem.setDisable(false);

59. calculateButton.setDisable(false);

60. }

62. public static void main(String[] args) {

63. launch(args);

64. }

65. }

Ce qui nous donne une interface similaire à celle-ci :

Figure 1 - L'interface qui va lancer le calcul.

Nous pouvons désormais lancer notre calcul soit en cliquant directement sur le bouton Lancer le calcul !, soit en allant dans le menu Action -> Calculer.

Lorsque nous démarrons le calcul, nous pouvons nous rendre compte de plusieurs choses :

- le curseur ne change jamais d'apparence ;

- le bouton n'est jamais désactivé ;

- l'interface est complètement bloquée : il n'est plus possible d'aller dans les menus par exemple ou même de fermer la fenêtre normalement.

C'est bien normal : nous réagissons immédiatement à l'action sur le bouton ou le menu et nous exécutons notre long calcul dans le même thread que celui qui gère l'affichage et les événements dans l'UI. Cette dernière ne peut donc plus rafraichir son affichage ni même réagir à nos saisies.

Et souvenez-vous qu'il s'agit ici d'un simple calcul numérique, le problème se pose également lors de la lecture ou l'écriture de fichiers, d'un accès à une base de données, sur le réseau ou sur Internet…

II-B - Raisonnement

Tout comme Swing et AWT s'exécutent dans l'EDT (Event Dispatch Thread), JavaFX utilise des threads qui lui sont propres tels que le JavaFX Application Thread, le thread de rendu Prism ou encore le thread media. Les événements reçus via les listeners ou le binding ou encore les appels aux callbacks sont tous exécutés dans le JavaFX Application Thread.

Tout comme dans Swing et AWT, lancer un traitement long dans le thread de gestion des événements de JavaFX peut mener à un blocage (freeze) de l'UI de l'application qui ne répond alors plus aux entrées de l'utilisateur. Ce dernier peut alors être amené à penser que l'application est boguée ou plantée. Il faut donc exécuter ce genre de traitement en tâche de fond (background task) sans bloquer les threads de l'UI.

Bien qu'il soit possible de lancer manuellement un ou plusieurs Thread pour effectuer le traitement, la gestion de la remontée d'informations vers l'UI (progression, message, remontée des erreurs) peut s'avérer assez compliquée à programmer. C'est ici qu'intervient l'API de concurrence de JavaFX !

II-C - Solution

Reprenons notre code et utilisons l'API de concurrence de JavaFX. Pour cela, nous allons juste modifier le code de la méthode doCalculate() ainsi que les imports pour lister les nouvelles classes nécessaires. Nous reviendrons ultérieurement sur les diverses classes et mécanismes introduits dans ce code :

Code JDK7

1. package test;

3. import javafx.application.Application;

4. import javafx.application.Platform;

5. import javafx.beans.value.ChangeListener;

6. import javafx.beans.value.ObservableValue;

7. import javafx.concurrent.Service;

8. import javafx.concurrent.Task;

9. import javafx.concurrent.Worker;

10. import javafx.event.ActionEvent;

11. import javafx.event.EventHandler;

12. import javafx.scene.Cursor;

13. import javafx.scene.Scene;

14. import javafx.scene.control.Button;

15. import javafx.scene.control.Menu;

16. import javafx.scene.control.MenuBar;

17. import javafx.scene.control.MenuItem;

18. import javafx.scene.layout.BorderPane;

19. import javafx.scene.layout.StackPane;

20. import javafx.stage.Stage;

22. public class Main extends Application {

24. private MenuItem calculateItem;

25. private Button calculateButton;

26. private Scene scene;

28. @Override

29. public void start(Stage primaryStage) {

30. final MenuItem exitItem = new MenuItem("Quitter");

31. exitItem.setOnAction(new EventHandler<ActionEvent>() {

33. @Override

34. public void handle(ActionEvent t) {

35. Platform.exit();

36. }

37. });        final Menu fileMenu = new Menu("Fichier");

38. fileMenu.getItems().add(exitItem);

39. calculateItem = new MenuItem("Calculer");

40. calculateItem.setOnAction(new EventHandler<ActionEvent>() {

42. @Override

43. public void handle(ActionEvent t) {

44. doCalculate();

45. }

46. });

47. final Menu actionMenu = new Menu("Action");

48. actionMenu.getItems().add(calculateItem);

49. final MenuBar menuBar = new MenuBar();

50. menuBar.getMenus().setAll(fileMenu, actionMenu);

51. calculateButton = new Button();

52. calculateButton.setText("Lancer le calcul !");

53. calculateButton.setOnAction(new EventHandler<ActionEvent>() {

55. @Override

56. public void handle(ActionEvent event) {

57. doCalculate();

58. }

59. });

60. StackPane center = new StackPane();

61. center.getChildren().add(calculateButton);

62. final BorderPane root = new BorderPane();

63. root.setTop(menuBar);

64. root.setCenter(center);

65. scene = new Scene(root, 300, 250);

66. primaryStage.setTitle("Test");

67. primaryStage.setScene(scene);

68. primaryStage.show();

69. }

71. private void doCalculate() {

72. final Cursor oldCursor = scene.getCursor();

73. scene.setCursor(Cursor.WAIT);

74. calculateItem.setDisable(true);

75. calculateButton.setDisable(true);

76. final Service<Void> calculateService = new Service<Void>() {

78. @Override

79. protected Task<Void> createTask() {

80. return new Task<Void>() {

82. @Override

83. protected Void call() throws Exception {

84. final int maxIterations = 1000000;

85. for (int iterations = 0; iterations < maxIterations; iterations ++) {

86. System.out.println(iterations);

87. }

88. return null;

89. }

90. };

91. }

92. };

93. calculateService.stateProperty().addListener(new ChangeListener<Worker.State>() {

95. @Override

96. public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldValue, Worker.State newValue) {

97. switch (newValue) {

98. case FAILED:

99. case CANCELLED:

100. case SUCCEEDED:

101. scene.setCursor(oldCursor);

102. calculateItem.setDisable(false);

103. calculateButton.setDisable(false);

104. break;

105. }

106. }

107. });

108. calculateService.start();

109. }

111. public static void main(String[] args) {

112. launch(args);

113. }

114. }

Code JDK

1. package test;

3. import javafx.application.Application;

4. import javafx.application.Platform;

5. import javafx.beans.value.ObservableValue;

6. import javafx.concurrent.Service;

7. import javafx.concurrent.Task;

8. import javafx.concurrent.Worker;

9. import javafx.event.ActionEvent;

10. import javafx.scene.Cursor;

11. import javafx.scene.Scene;

12. import javafx.scene.control.Button;

13. import javafx.scene.control.Menu;

14. import javafx.scene.control.MenuBar;

15. import javafx.scene.control.MenuItem;

16. import javafx.scene.layout.BorderPane;

17. import javafx.scene.layout.StackPane;

18. import javafx.stage.Stage;

20. public class Main extends Application {

22. private MenuItem calculateItem;

23. private Button calculateButton;

24. private Scene scene;

26. @Override

27. public void start(Stage primaryStage) {

28. final MenuItem exitItem = new MenuItem("Quitter");

29. exitItem.setOnAction((ActionEvent t) -> Platform.exit());

30. final Menu fileMenu = new Menu("Fichier");

31. fileMenu.getItems().add(exitItem);

32. calculateItem = new MenuItem("Calculer");

33. calculateItem.setOnAction((ActionEvent t) -> doCalculate());

34. final Menu actionMenu = new Menu("Action");

35. actionMenu.getItems().add(calculateItem);

36. final MenuBar menuBar = new MenuBar();

37. menuBar.getMenus().setAll(fileMenu, actionMenu);

38. calculateButton = new Button();

39. calculateButton.setText("Lancer le calcul !");

40. calculateButton.setOnAction((ActionEvent event) -> doCalculate());

41. StackPane center = new StackPane();

42. center.getChildren().add(calculateButton);

43. final BorderPane root = new BorderPane();

44. root.setTop(menuBar);

45. root.setCenter(center);

46. scene = new Scene(root, 300, 250);

47. primaryStage.setTitle("Test");

48. primaryStage.setScene(scene);

49. primaryStage.show();

50. }

52. private void doCalculate() {

53. final Cursor oldCursor = scene.getCursor();

54. scene.setCursor(Cursor.WAIT);

55. calculateItem.setDisable(true);

56. calculateButton.setDisable(true);

57. final Service<Void> calculateService = new Service<Void>() {

59. @Override

60. protected Task<Void> createTask() {

61. return new Task<Void>() {

63. @Override

64. protected Void call() throws Exception {

65. final int maxIterations = 1000000;

66. for (int iterations = 0; iterations < maxIterations; iterations ++) {

67. System.out.println(iterations);

68. }

69. return null;

70. }

71. };

72. }

73. };

74. calculateService.stateProperty().addListener((ObservableValue<? extends Worker.State> observableValue, Worker.State oldValue, Worker.State newValue) -> {

75. switch (newValue) {

76. case FAILED:

77. case CANCELLED:

78. case SUCCEEDED:

79. scene.setCursor(oldCursor);

80. calculateItem.setDisable(false);

81. calculateButton.setDisable(false);

82. break;

83. }

84. });

85. calculateService.start();

86. }

88. public static void main(String[] args) {

89. launch(args);

90. }

91. }

Désormais, notre programme se comporte comme prévu :

- le curseur change d'apparence au début et à la fin du calcul ;

- le bouton, ainsi que l'entrée dans le menu, est désactivé au début du calcul et réactivé lorsque ce dernier se termine ;

- l'interface n'est plus bloquée : nous pouvons naviguer dans les menus et nous pouvons fermer la fenêtre normalement.

Nous pouvons également voir que nous n'avons pas eu à créer un nouveau thread manuellement ni d'avoir à nous synchroniser dessus. Cependant le calcul s'est bien déroulé dans un autre fil d'exécution différent du JavaFX Application Thread.

III - Interfaces et classes importantes

III-A - javafx.concurrent.Worker<V>

Cette interface est l'entité parente des classes concrètes qui permettent de lancer une tâche de longue durée en fond de l‘UI. Vous ne serez pas amené à la manipuler directement, cependant vous pouvez remarquer la présence de plusieurs méthodes retournant des propriétés telles que progress, message, title, etc. qui permettent de suivre de la tâche depuis l'UI.

Le type V passé en paramètre de Worker est tout simplement le type de la valeur qui sera retournée par la méthode call(). C'est-à-dire le type du résultat de la tâche, car cette méthode est celle qui sera appelée pour effectuer la tâche de longue durée. Généralement, pour des tâches ne retournant pas de valeur, V sera soit non déclaré (et donc implicitement de type Object) ou de type Void avec une valeur de retour à null dans les deux cas.

Note : java.lang.Void est un type spécial de la JVM dont la seule valeur possible est null.

Un Worker dispose également d'une propriété état, state, qui changera de valeur au cours de la vie de la tâche. Les valeurs possibles de cet état sont définies dans l'enum Worker.State :

- READY - la tâche est prête à être exécutée ; c'est l'état initial ;

- SCHEDULED - la tâche est prévue pour être exécutée ; il s'agit d'un état intermédiaire entre READY et RUNNING ;

- RUNNING - la tâche démarre son exécution ;

- FAILED - la tâche a échoué, généralement à cause d'une exception qui a été levée au cours du traitement ;

- CANCELLED - la tâche a été annulée, généralement par l'utilisateur depuis l'UI ;

- SUCCEEDED - la tâche s'est correctement exécutée et a retourné son résultat.

Cette interface est implémentée par trois classes abstraites :

- Service<V> : une classe qui est instanciée et manipulée par l'UI pour créer, gérer et superviser la tâche de longue durée ;

- ScheduledService<V> : une classe qui est instanciée et manipulée par l'UI pour créer, gérer et superviser la tâche de longue durée qui peut se répéter à intervalles réguliers. JDK8 ou ultérieur ;

- Task<V> : une classe qui est instanciée et manipulée par le service ou le service répétable pour exécuter la tâche de longue durée dans un thread séparé.

Figure 2 - Hiérarchie des classes.

III-B - javafx.concurrent.Service<V>

La classe Service est destinée à être utilisée et manipulée dans l'UI. Son seul vrai but est de créer une tâche (Task) qui sera exécutée en arrière-plan dans un autre thread. Une instance de Service est destinée à être réutilisable, vous pouvez donc conserver une référence sur une instance de Service et l'exécuter plusieurs fois d'affilée.

Pour instancier un service, il suffit de créer une nouvelle classe anonyme qui étend Service<V> et de surcharger la méthode abstraite createTask() pour qu'elle crée une Task<V> du même type que le service :

1. Service<Image> imageLoadingService = new Service<Image>(){

3. @Override

4. protected Task<Image> createTask() {

5. // Instancier et retourner une Task<Image> ici.

6. }

7. };

On peut aussi étendre Service<V> dans une classe à part :

1. public class ImageLoadingService extends Service<Image> {

3. @Override

4. protected Task<Image> createTask() {

5. // Instancier et retourner une Task<Image> ici.

6. }

7. };

III-C - javafx.concurrent.Task<V>

C'est cette classe qui sera amenée à faire le calcul, la requête ou le traitement de longue durée et c'est dans la méthode call() de cette classe que la tâche est effectuée ; cette méthode sera appelée dans un thread séparé, différent du JavaFX Application Thread.

Contrairement à Service, une Task est destinée à n'être exécutée qu'une seule et unique fois. Une tâche n'est pas réutilisable !

Pour créer une tâche, il suffit de créer une nouvelle classe anonyme qui étend Task<V> et de surcharger la méthode call() pour qu'elle renvoie une instance du type approprié :

1. Task<Image> imageLoadingTask = new Task<Image>(){

3. @Override

4. protected Image call() throws Exception {

5. // Charger l'image ici.

6. Image image = ...

7. return image;

8. }

9. };

On peut aussi étendre Task<V> dans une classe à part :

1. public class ImageLoadingTask extends Task<Image> {

3. @Override

4. protected Image call() throws Exception {

5. // Charger l'image ici.

6. Image image = ...

7. return image;

8. }

9. }

En plus des calculs, accès IO, requête web ou BD ou autre, vous pouvez faire tout ce que vous faites habituellement dans un thread au cours de ce traitement : boucle infinie, appel à Thread.sleep(), etc. sans pour autant que cela ne bloque l'UI.

Attention cependant, toute exception qui n'est pas catchée dans ce traitement remontera au niveau supérieur et passera automatiquement la tâche en l'état FAILED en plus d'arrêter le calcul ou le traitement en cours.

III-D - Javafx.concurrent.ScheduledService<V>

La classe ScheduledService, introduite dans le JDK8 permet de créer des services qui se répètent à intervalles réguliers. Elle fonctionne comme la class Service mais dispose de propriétés supplémentaires pour gérer la répétition ainsi que les conditions de gestion des échecs.

Un ScheduledService, se relance automatiquement lorsque son exécution précédente s'est terminée correctement une fois le temps d'attente spécifié par le programmeur écoulé. Il peut également se relancer en cas d'erreur d'exécution suivant les conditions de gestion des échecs qui ont été spécifiées.

IV - Utiliser un service

Vous serez donc amené à utiliser la classe Service depuis le code de votre UI, en réaction à un clic sur un bouton, dans un menu, une sélection dans une table, une liste ou un menu déroulant, etc. La majorité de ces actions s'exécutera donc dans le JavaFX Application Thread. Voyons donc maintenant comment utiliser un service.

IV-A - Création et lancement

Reprenons notre code qui permet d'initialiser notre service et complétons-le avec le code qui initialise la tâche :

1. Service<Image> imageLoadingService = new Service<Image>(){

3. @Override

4. protected Task<Image> createTask() {

5. return new Task<Image>(){

7. @Override

8. protected Image call() throws Exception {

9. // Charger l'image ici.

10. Image image = ...

11. return image;

12. }

13. };

14. }

15. };

16. imageLoadingService.start();

Ou, pour une tâche qui ne retourne rien :

1. Service<Void> fileSaveService = new Service<Void>(){

3. @Override

4. protected Task<Void> createTask() {

5. return new Task<Void>(){

7. @Override

8. protected Void call() throws Exception {

9. // Sauvegarder le fichier ici.

10. [...]

11. return null;

12. }

13. };

14. }

15. };

16. fileSaveService.start();

L'appel à la méthode start() du service, provoquera le démarrage de la tâche de fond dans un nouveau thread.

IV-B - Annulation

Pour annuler un service en cours d'exécution, il suffit d'appeler la méthode cancel() du service depuis l'UI (par exemple, un bouton). Lorsque la méthode cancel() est appelée, l'état du service et de la tâche passe à CANCELLED.

Attention, cela n'aura pas pour effet d'arrêter la tâche immédiatement ! Comme nous le verrons ultérieurement, vous devrez prendre des précautions supplémentaires dans la tâche pour vous assurer qu'elle termine bien son exécution.

1. imageLoadingService.cancel();

IV-C - Relancer un service

Pour qu'un service puisse être relancé, il doit être dans l'état READY, faute de quoi tout appel à la méthode start() se soldera par la levée d'une exception de type IllegalStateException. Il convient donc d'appeler sa méthode reset() avant d'appeler la méthode start(). À l'exécution, une nouvelle instance de Task sera alors créée et exécutée par le service.

1. imageLoadingService.reset();

2. imageLoadingService.start();

Il est également possible d'appeler la méthode restart() qui fera exactement la même chose que le couple reset() + start() :

1. imageLoadingService.restart();

IV-D - Avancement du service

Il est possible de binder les propriétés retournées par progressProperty(), messageProperty(), titleProperty(), etc. d'un service vers des éléments de l'UI tels que des instances de Label, ProgressBar ou ProgressIndicator de manière à suivre l'avancement de l'exécution et les changements d'état de la tâche de fond.

1. Stage dialog = ...

2. ProgressIndicator progressIndicator = ...

3. Label informationLabel = ...

4. [...]

5. Service<Image> imageLoadingService = new Service<Image>() {

6. [...]

7. };

8. dialog.titleProperty().bind(imageLoadingTask.titleProperty());

9. progressIndicator.progressProperty.bind(imageLoadingTask.progressProperty()) ;

10. informationLabel.textProperty().bind(imageLoadingTask.messageProperty());

11. imageLoadingService.start();

Il est aussi possible de mettre des ChangeListener sur chacune de ces propriétés pour transmettre leurs changements de valeur sur d'autres éléments de l'UI.

IV-E - État du service

En général vous aurez envie de superviser les changements d'état d'un service de manière à afficher des messages d'erreur ou de succès d'opération dans votre UI.

Figure 3 - Les divers états d'un service.

Pour superviser les changements d'état d'une tâche de fond, vous avez deux possibilités.

IV-E-1 - ChangeListener sur l'état du service

Vous pouvez mettre un écouteur de type ChangeListener sur la propriété state du service et superviser les changements de valeur de cette propriété :

Code JDK7

1. Service<Image> imageLoadingService = new Service<Image>() {

2. [...]

3. };

4. imageLoadingService.stateProperty().addListener(new ChangeListener<Worker.State>() {

5. @Override

6. public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {

7. switch (newValue) {

8. case READY:

9. // La tâche est prête.

10. [...]

11. break;

12. case SCHEDULED:

13. // La tâche va être exécutée.

14. [...]

15. break;

16. case RUNNING:

17. // La tâche est en cours d'exécution.

18. [...]

19. break;

20. case FAILED:

21. // La tâche a échoué.

22. [...]

23. break;

24. case CANCELLED:

25. // La tâche a été annulée.

26. [...]

27. break;

28. case SUCCEEDED:

29. // La tâche s'est correctement terminée.

30. [...]

31. break;

32. }

33. }

34. });

35. imageLoadingService.start();

Code JDK8

1. Service<Image> imageLoadingService = new Service<Image>() {

2. [...]

3. };

4. imageLoadingService.stateProperty().addListener((ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) -> {

5. switch (newValue) {

6. case READY:

7. // La tâche est prête.

8. [...]

9. break;

10. case SCHEDULED:

11. // La tâche va être exécutée.

12. [...]

13. break;

14. case RUNNING:

15. // La tâche est en cours d'exécution.

16. [...]

17. break;

18. case FAILED:

19. // La tâche a échoué.

20. [...]

21. break;

22. case CANCELLED:

23. // La tâche a été annulée.

24. [...]

25. break;

26. case SUCCEEDED:

27. // La tâche s'est correctement terminée.

28. [...]

29. break;

30. }

31. });

32. imageLoadingService.start();

IV-E-2 - Callback sur le service

Mais l'API vous permet également de mettre des callbacks en place, par exemple :

Code JDK7

1. Service<Image> imageLoadingService = new Service<Image>() {

2. [...]

3. };

4. imageLoadingService.setOnSucceeded(new EventHandler<WorkerStateEvent>(){

6. @Override

7. public void handle(WorkerStateEvent event) {

8. // La tâche s'est correctement terminée.

9. [...]

10. }

11. });

12. imageLoadingService.setOnFailed(new EventHandler<WorkerStateEvent>(){

14. @Override

15. public void handle(WorkerStateEvent event) {

16. // La tâche a échoué.

17. [...]

18. }

19. });

20. imageLoadingService.start();

Code JDK8

1. Service<Image> imageLoadingService = new Service<Image>() {

2. [...]

3. };

4. imageLoadingService.setOnSucceeded((WorkerStateEvent event) -> {

5. // La tâche s'est correctement terminée.

6. [...]

7. });

8. imageLoadingService.setOnFailed((WorkerStateEvent event) -> {

9. // La tâche a échoué.

10. [...]

11. });

12. imageLoadingService.start();

Il existe des callbacks pour tous les états possibles :

- setOnReady() - la tâche est prête ;

- setOnScheduled() - la tâche va être exécutée ;

- setOnRunning() - la tâche démarre son exécution ;

- setOnFailed() - la tâche a échoué ;

- setOnCancelled() - la tâche a été annulée ;

- setOnSucceded() - la tâche s'est correctement terminée.

Toutes ces méthodes sont appelées depuis le JavaFX Application Thread.

IV-F - Récupérer le résultat

Pour récupérer le résultat d'un service, il suffit d'appeler la méthode getValue() du service dans le listener ou le callback approprié. Ainsi le résultat de la tâche pourra être transmis à l'UI ou à un autre traitement.

Code JDK7

1. // Le service est final, car il doit être appelé depuis le callback.

2. final Service<Image> imageLoadingService = new Service<Image>() {

3. [...]

4. };

5. imageLoadingService.setOnSucceeded(new EventHandler<WorkerStateEvent>(){

7. @Override

8. public void handle(WorkerStateEvent event) {

9. // La tâche s'est correctement terminée.

10. final Image image = imageLoadingService.getValue();

11. [...]

12. }

13. });

14. imageLoadingService.start();

Code JDK8

1. // Le service est final, car il doit être appelé depuis le callback.

2. final Service<Image> imageLoadingService = new Service<Image>() {

3. [...]

4. };

5. imageLoadingService.setOnSucceeded((WorkerStateEvent event) -> {

6. // La tâche s'est correctement terminée.

7. final Image image = imageLoadingService.getValue();

8. [...]

9. });

10. imageLoadingService.start();

IV-G - Récupérer la raison de l'échec

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é. Cela permettra d'afficher l'erreur à l'utilisateur ou de la stocker dans un journal.

Code JDK7

1. // Le service est final, car il doit être appelé depuis le callback.

2. final Service<Image> imageLoadingService = new Service<Image>() {

3. [...]

4. };

5. imageLoadingService.setOnFailed(new EventHandler<WorkerStateEvent>(){

7. @Override

8. public void handle(WorkerStateEvent event) {

9. // La tâche a échoué.

10. final Throwable error = imageLoadingService.getException();

11. [...]

12. }

13. });

14. imageLoadingService.start();

Code JDK8

1. // Le service est final, car il doit être appelé depuis le callback.

2. final Service<Image> imageLoadingService = new Service<Image>() {

3. [...]

4. };

5. imageLoadingService.setOnFailed((WorkerStateEvent event) -> {

6. // La tâche a échoué.

7. final Throwable error = imageLoadingService.getException();

8. [...]

9. });

10. imageLoadingService.start();

V - Utiliser une tâche

De manière générale, la classe Task sera uniquement manipulée par votre service et la majorité de son code s'exécutera dans son propre Thread, indépendant du JavaFX Application Thread. Voyons donc maintenant comment utiliser une tâche.

V-A - Notification de l'avancement

Depuis la méthode call() de la tâche, il est possible d'envoyer des informations vers l'UI via les méthodes updateProgress(), updateMessage(), etc. Les valeurs passées à ces méthodes seront transmises vers le JavaFX Application Thread.

1. return new Task<Integer>() {

2. @Override

3. protected Integer call() throws Exception {

4. final int maxIterations = 1000000;

5. int iterations = 0;

6. for (iterations = 0; iterations < maxIterations; iterations++) {

7. // On envoie un nouveau message.

8. updateMessage("iteration "+ (iterations+1));

9. // On change l'avancement de la tâche.

10. updateProgress(iterations, maxIterations);

11. [...]

12. }

13. return iterations;

14. }

15. updateMessage("Done");

16. updateProgress(iterations, maxIterations);

17. };

Si trop d'appels à ces méthodes ont lieu dans un court laps de temps, il y aura agrégation des différents appels et seules les dernières valeurs seront transmises à l'UI. Ainsi certains messages peuvent être perdus et la progression peut sauter des valeurs si elle est trop rapide.

Dans le cas de boucles ou de progressions obtenues par calcul, il faut également faire attention au fait que updateProgress() générera une exception de type IllegalArgumentException si la valeur d'avancement devient strictement supérieure à la valeur maximum attendue.

V-B - Annulation

Comme nous l'avons vu précédemment, appeler la méthode cancel() du service met la tâche dans l'état CANCELLED, mais cela ne suffit pas en soi pour arrêter l'exécution de la tâche. Dans votre traitement, vous devez vérifier à intervalles réguliers la valeur retournée par la méthode isCancelled() de la tâche et interrompre le calcul en cours le cas échéant . Par exemple, la tâche suivante effectue 1 000 000 itérations et vérifie si la tâche a été annulée à chaque itération, interrompant la boucle si c'est le cas.

1. return new Task<Integer>() {

2. @Override

3. protected Integer call() throws Exception {

4. final int maxIterations = 1000000;

5. int iterations = 0;

6. for (iterations = 0; iterations < maxIterations; iterations++) {

7. // Arrêt de la boucle si la tâche est annulée.

8. if (isCancelled()) {

9. break;

10. }

11. // On envoie un nouveau message.

12. updateMessage("iteration "+ (iterations+1));

13. // On change l'avancement de la tâche.

14. updateProgress(iterations, maxIterations);

15. [...]

16. }

17. return iterations;

18. }

19. };

V-B-1 - Lors d'un appel bloquant

Lorsque l'on effectue un appel bloquant, par exemple en mettant le thread courant en pause dans une boucle, il est important de vérifier l'état de la tâche une fois que le thread courant est à nouveau actif. En effet, le thread peut se réveiller du fait que la tâche est passée dans l'état CANCELLED.

1. return new Task<Integer>() {

2. @Override

3. protected Integer call() throws Exception {

4. final int maxIterations = 1000000;

5. int iterations = 0;

6. for (iterations = 0; iterations < maxIterations; iterations++) {

7. // Arrêt de la boucle si la tâche est annulée.

8. if (isCancelled()) {

9. break;

10. }

11. // On envoie un nouveau message.

12. updateMessage("iteration "+ (iterations+1));

13. // On change l'avancement de la tâche.

14. updateProgress(iterations, maxIterations);

15. [...]

16. try {

17. Thread.sleep(100);

18. } catch (InterruptedException ie) {

19. if (isCancelled()) {

20. break;

21. }

22. }

23. }

24. return iterations;

25. }

26. };

V-C - État d'une tâche

Il est également possible de superviser les changements d'état d'une tâche. À cet effet, il est possible de surcharger les méthodes scheduled(), running(), failed(), cancelled(), et succeeded() de la classe Task.

- scheduled() - la tâche va être exécutée.

- running() - la tâche démarre son exécution.

- failed() - la tâche a échoué.

- cancelled() - la tâche a été annulée.

- succeeded() - la tâche s'est correctement terminée.

Il n'existe pas de méthode pour l'état READY, car il s'agit de l'état par défaut d'une tâche à sa création.

Figure 4 - Les divers états d'une tâche.

Toutes ces méthodes sont appelées depuis le JavaFX Application Thread une fois que les propagations aux listeners ont été terminées.

V-D - Produire des résultats partiels

V-D-1 - JDK 7

Dans le JDK7, Task ne produit qu'un seul et unique résultat : son résultat final. Cependant, il est assez facile d'ajouter la capacité de produire des résultats partiels en utilisant des propriétés JavaFX sur la tâche.

1. public class IterationsTask extends Task<Integer> {

3. private final IntegerProperty partialResult = new SimpleIntegerProperty(this, "partialResult", -1);

5. public final int getPartialResults() {

6. return partialResult.get();

7. }

9. public final ReadOnlyIntegerProperty partialResultProperty() {

10. return partialResult;

11. }

13. [...]

14. }

Il faudra cependant, dans la méthode call(), prendre des précautions pour que les valeurs soient settées dans le JavaFX Application Thread et non pas dans le thread de la tâche. En effet, généralement des résultats partiels ne sont intéressants que dans le cadre d'un affichage dans une UI. Or, une fois affichées dans une scène, les propriétés des contrôles JavaFX doivent être uniquement accédées via le JavaFX Application Thread.

1. @Override

2. protected Integer call() throws Exception {

3. final int maxIterations = 1000000;

4. int iterations = 0;

5. for (iterations = 0; iterations < maxIterations; iterations++) {

6. // Arrêt de la boucle si la tâche est annulée.

7. if (isCancelled()) {

8. break;

9. }

10. // ! La propriété doit être manipulée dans la JavaFX Application Thread !

11. final int it = iterations;

12. Platform.runLater(new Runnable() {

13. @Override

14. public void run() {

15. partialResult.set(it);

16. }

17. });

18. }

19. return iterations;

20. }

Il faut faire également attention au fait qu'ici, il n'y a pas de fusion ou agrégation des envois de valeurs partielles sur le JavaFX Application Tread. Cela peut donc provoquer une monopolisation ou une surcharge du thread par la tâche à cause du trop grand nombre de requêtes à Platform.runLater() et nous ramener à notre problème de blocage initial. Ici, cet exemple ne fait pas moins de 1 000 000 de requêtes à Platform.runLater() dans une boucle rapide sans aucune pause ! Cela peut faire que l'application cesse de répondre, ne puisse plus se redimensionner ou plante carrément, bref on perdrait tous les avantages liés à l'utilisation d'un service !

Il faudra donc penser à aménager des temps de repos en appelant Thread.sleep() à intervalles réguliers de manière à ce que le JavaFX Application Thread puisse continuer à s'exécuter et évacuer les événements en attente.

1. @Override

2. protected Integer call() throws Exception {

3. final int maxIterations = 1000000;

4. int iterations = 0;

5. for (iterations = 0; iterations < maxIterations; iterations++) {

6. // Arrêt de la boucle si la tâche est annulée.

7. if (isCancelled()) {

8. break;

9. }

10. // ! La propriété doit être manipulée dans la JavaFX Application Thread !

11. final int it = iterations;

12. Platform.runLater(new Runnable() {

13. @Override

14. public void run() {

15. partialResult.set(it);

16. }

17. });

18. // Interragir ainsi avec le JavaFX Application Thread demande de lui laisser un peu le temps de respirer.

19. try {

20. Thread.sleep(50);

21. } catch (InterruptedException ie) {

22. if (isCancelled()) {

23. break;

24. }

25. }

26. }

27. return iterations;

28. }

On pourrait également modifier le code pour faire en sorte que les mises à jour de l'UI arrivent tous les 1000 éléments par exemple, de manière à réduire le nombre de mises à jour effectuées.

V-D-2 - JDK 8

Le JDK 8 a ajouté la possibilité d'invoquer la méthode updateValue() dans le corps de la tâche pour mettre à jour le contenu de la propriété value de l'objet Task (et de son service parent). Il suffira alors de placer un écouteur de type InvalidationListener ou ChangeListener sur cette propriété pour être mis au courant de ses changements de valeur au cours du calcul.

La méthode updateValue() peut être invoquée depuis n'importe quel thread même celui dans lequel s'exécute la tâche. Tout comme les autres méthodes updateXXX(), les appels sont fusionnés lorsqu'ils sont trop nombreux, ce qui fait que certains changements de valeur peuvent être omis.

Code JDK8

@Override
protected Integer call() throws Exception {
final int maxIterations = 1000000;
int iterations = 0;
for (iterations = 0; iterations < maxIterations; iterations++) {
// Arrêt de la boucle si la tâche est annulée.
if (isCancelled()) {
break;
}
// ! La propriété doit être manipulée dans la JavaFX Application Thread !
final int it = iterations;
updateValue(it);
}
}
return iterations;
}

V-E - Utiliser une tâche dans un contexte non JavaFX

Vous pouvez être amené à utiliser une tâche dans un contexte non JavaFX, par exemple, dans le but d'effectuer des tests unitaires. De plus, la classe Task<V> hérite des interfaces Runnable et RunnableFuture<V>, il est donc tout à fait possible de la passer à un Thread Java normal pour exécution :

1. final Thread imageLoadingThread = new Thread(imageLoadingTask);

2. imageLoadingThread.start();

Il est également possible de passer une tâche à un ExecutorService :

1. anExecutorService.submit(imageLoadingTask);

Cependant, vous serez rarement amené à utiliser ces cas de figure.

Attention, si vous utilisez Task dans un contexte où le JavaFX Application Thread n'a pas été initialisé, il ne faut pas appeler updateMessage() ou updateProgress() ou n'importe quelle autre méthode updateXXX() sous peine de générer des exceptions. De plus, cette approche peut ne pas fonctionner non plus avec des tâches produisant des résultats partiels telles que celles que nous avons montrées pour le JDK 7 puisqu'elles invoquent la méthode Platform.runLater() qui suppose que les runtimes JavaFX ont été correctement initialisés.

VI - Utiliser un service répétable

La classe ScheduledService, introduite dans le JDK8, permet de créer un service qui s'exécute de manière cyclique. Cette classe s'utilise exactement comme la classe Service, mais introduit de nouveaux concepts et propriétés. Lorsque le service s'exécute correctement, il redémarre automatiquement après un temps d'attente. Au contraire, lorsque le service échoue, le programmeur peut décider s'il se relance en fonction de plusieurs critères. Étant donné que le service se répète automatiquement de manière cyclique, il repasse régulièrement par tous les états possibles au cours de son exécution.

Attention, la gestion des durées par cette classe est indicative ; il ne faut pas utiliser ce genre de service pour des tâches demandant une précision extrême dans la gestion du temps.

VI-A - Délai avant l'exécution

Tout d'abord, la propriété delay permet de spécifier un temps d'attente avant le démarrage initial du service.

1. service.setDelay(Duration.seconds(5));

VI-B - Période d'exécution

La propriété period permet de définir le temps d'attente entre deux exécutions sans erreur. C'est-à-dire le temps qui s'écoule entre chaque moment dans lequel l'état du service entre en l'état RUNNING.

1. service.setPeriod(Duration.seconds(1));

Si la tâche prend plus de temps à s'exécuter que la durée spécifiée dans la période, ou si la période est vide (ex. : Duration.ZERO ou une durée indéterminée), le service se relance alors immédiatement après la fin de la tâche sans plus attendre. De plus, le service ne peut interrompre une tâche en cours si elle dépasse le temps d'attente indiqué pour la période.

VI-C - Gestion des échecs

Par défaut, en cas d'échec, le service se termine immédiatement. Il est possible de changer ce comportement en modifiant la valeur de la propriété restartOnFailure pour indiquer si le service doit se relancer en cas d'échec. Il est également possible de modifier la valeur de la propriété maxFailureCount pour indiquer le nombre maximal d'échecs supportés. La propriété en lecture seule currentFailureCount permet de connaitre le nombre d'échecs encourus par le service jusqu'à présent.

1. service.setRestartOnFailure(true); // On continue l'exécution en cas d'erreur.

2. service.setMaxFailureCount(100); // Et au maximum on acceptera 100 échecs.

La propriété backoffStrategy permet de spécifier une fabrique à durées de type Callback<ScheduledService<?>, Duration> qui sera invoquée lorsque le service a échoué. Cela permet par exemple, d'augmenter, le temps d'attente entre chaque échec successif. Cette valeur sera ajoutée à la période du service lors du temps d'attente avant la prochaine tentative d'exécution. Cette valeur peut être plafonnée en utilisant la propriété maximumCumulativePeriod.

1. service.setBackoffStrategy(service -> Duration.seconds(service.getCurrentFailureCount() * 5)); // On ajoute 5 secondes d'attente entre chaque échec.

2. service.setMaximumCumulativePeriod(Duration.minutes(10)); // Au maximum on attendra 10 minutes avant la prochaine tentative.

VI-D - Derniers bons résultats

La valeur de la propriété value du service sera réinitialisée à null avant chaque fois que le service se relance.

Il est donc plus intéressant de se pencher sur la propriété en lecture seule lastValue, qui contient la dernière valeur générée lors d'une exécution correcte de la tâche. Elle est initialement à la valeur null et ne sera mise à jour que lorsque la tâche s'exécute correctement. Il est donc possible de suivre ses changements de valeur en lui ajoutant un écouteur de type InvalidationListener ou ChangeListener.

VII - Conclusion

Vous savez désormais comment lancer une tâche de fond en JavaFX, ceci vous permettra de lancer tous vos traitements longs en arrière-plan de l'UI sans pour autant geler votre interface graphique.

VIII - Remerciements

Je tiens à remercier toute l'équipe du forum Développez ainsi que Mickael Baron et Gueritarish pour leurs suggestions et leurs relectures du présent article. Je tiens également à remercier Claude Leloup pour ses corrections orthographiques.

IX - Liens

- Page javadoc de l'interface Worker<V>.

- Page javadoc de la classe Service<V>.

- Page javadoc de la classe ScheduledService<V>.

- Page javadoc de la classe Task<V>.