novembre
2010
Il y a quelques temps, j’étais tombé sur une série de billet anglophone proposant un SwingWorker
amélioré, tel que celui-ci sur le blog de Baptiste Wicht : A better SwingWorker without exception swallowing
Pour rappel cette classe permet d’exécuter des tâches en arrière-plan (via la méthode doInBackground()
) afin de ne pas bloquer l’EDT (le thread gérant l’affichage graphique). Et à la fin du traitement, la méthode done()
nous permet de mettre à jour l’interface graphique.
Le problème vient du fait que par défaut, les exceptions remontées par la méthode doInBackground()
sont « perdu ». En fait il faut les récupérer et les traiter explicitement via l’appel de la méthode get()
une fois la tâche terminée.
Bien sûr le code correspondant n’est pas bien méchant :
@Override
protected void done() {
try {
get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.getCause().printStackTrace();
}
}
Mais cela peut se révéler assez lourd et fastidieux… surtout que l’on n’a pas forcément besoin d’implémenter la méthode done()
.
Pour pallier à cela, la proposition du « BetterSwingWorker » encapsule en fait le vrai SwingWorker, afin d’en proposer une version simplifié.
Un peu trop simplifié à mon goût…
En effet je n’aime pas trop cette solution, que l’on peut retrouver dans son intégralité dans le code ci-dessous :
public abstract class BetterSwingWorker {
private final SwingWorker<Void, Void> worker = new SimpleSwingWorker();
public void execute() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
before();
}
});
worker.execute();
}
protected void before() {
//Nothing by default
}
protected abstract void doInBackground() throws Exception;
protected abstract void done();
private class SimpleSwingWorker extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() throws Exception {
BetterSwingWorker.this.doInBackground();
return null;
}
@Override
protected void done() {
try {
get();
} catch (final InterruptedException ex) {
throw new RuntimeException(ex);
} catch (final ExecutionException ex) {
throw new RuntimeException(ex.getCause());
}
BetterSwingWorker.this.done();
}
}
}
Ce « BetterSwingWorker » encapsule le SwingWorker afin de proposer une version amélioré, en se basant sur trois points :
- Les exceptions sont automatiquement traitées sans que l’on n’ait rien à faire.
- La classe n’est plus paramétrable via les Generics, dans le but de simplifier son utilisation.
- On dispose d’une méthode
before()
permettant d’effectuer des traitements dans l’EDT avant le début du traitement.
Toutefois j’y vois plusieurs défauts, causé en partie par l’encapsulation du SwingWorker et la perte du paramétrage :
- On ne peut plus utiliser la valeur de retour de la méthode
doInBackground()
. - On ne peut plus récupérer les éventuelles exceptions remontées par
doInBackground()
. Impossible donc de savoir si notre tâche s’est déroulé correctement ou pas… - La classe n’est plus paramétrable du tout, et on ne peut pas publier de valeur intermédiaire via la méthode
publish()
. - On ne peut plus utiliser la méthode
setProgress()
afin de signaler l’avancement de la tâche via les listeners. - On ne peut pas interrompre la tâche.
- On ne peut pas exécuter la tâche via un autre Executors (par défaut les SwingWorkers sont exécutés via un pool de 10 threads).
Bien sûr l’idée consiste à simplifier son utilisation, mais je trouve que l’on perd un peu trop au change…
Il y a moyen de faire mieux !
Plus simple, plus complet
Mon objectif serait donc de proposer une autre implémentation proposant la majorité des points apportés par ce « BetterSwingWorker » tout en conservant toutes les possibilités du SwingWorker original.
Je vais juste ignorer la méthode before()
dont l’intérêt me semble assez réduit puisqu’on peut faire cela avant la création du SwingWorker…
On n’encapsule pas, on hérite
Pour débuter, on va donc opter pour un héritage pure et simple à la place de l’encapsulation. Cela nous permettra de conserver toutes les possibilités offertes par le SwingWorker
. On va également opter pour conserver le paramétrage Generics (pour le moment – voir plus loin).
public abstract class BetterWorker<T, V> extends SwingWorker<T, V> {
}
Fin de tâche et exceptions
On va gérer les exceptions en découpant la méthode done()
en trois méthodes :
- onSuccess(T) sera appellée uniquement lorsque doInBackground() se sera terminée normalement, et recevra en paramètre la valeur de retour.
- onError(Throwable) sera appellée uniquement doInBackground() sera interrompu, soit en remontant une exception, soit via une interruption de la tâche.
- Enfin onEnd() s’exécutera dans tous les cas à la fin de la tâche, après onSuccess(T) ou onError(Throwable) selon le cas.
Par défaut les méthodes onSuccess(T) et onEnd() ne font rien, tandis que onError(Throwable) affichera l’erreur via le handler par défaut. Bien sûr on peut redéfinir ces méthodes selon nos besoins.
Ce qui nous donne grosso-modo le code suivant :
public abstract class BetterWorker<T, V> extends SwingWorker<T, V> {
protected void onSuccess(T result) {
// Nothing by default
}
protected void onError(Throwable cause) {
final Thread current = Thread.currentThread();
final UncaughtExceptionHandler handler = current
.getUncaughtExceptionHandler();
if (handler != null) {
handler.uncaughtException(current, cause);}
else {
cause.printStackTrace();
}
}
protected void onEnd() {
// Nothing by default
}
@Override
protected final void done() {
try {
onSuccess(get());
} catch (ExecutionException e) {
onError(e.getCause());
} catch (Throwable t) {
onError(t);
} finally {
onEnd();
}
}
}
Ainsi, plutôt que de redéfinir la méthode done(), on aura la possibilité de redéfinir (ou pas) ces trois méthodes selon notre besoin.
Etat de la progression
Le SwingWorker original permet de gérer l’état d’avancement de notre tâche via la méthode setProgress(), mais cela nécessite toutefois d’enregistrer un listener qui sera chargé de mettre à jour une barre de progression.
Cela ne représente pas vraiment un code bien complexe en soit, mais il s’agit quand même d’un besoin basique qui devrait bénéficier d’une solution un peu plus souple. Autant gérer cela le plus simplement du monde directement au sein de notre classe via une méthode reportProgressTo() qui se chargera de tout cela :
public SwingWorker<T, V> reportProgressTo(JProgressBar progressBar) {
reportProgressTo(progressBar.getModel());
return this;
}
public SwingWorker<T, V> reportProgressTo(final BoundedRangeModel model) {
addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
final String property = event.getPropertyName();
if ("state".equals(property)
&& event.getNewValue() instanceof SwingWorker.StateValue) {
switch ((SwingWorker.StateValue) event.getNewValue()) {
case STARTED:
model.setValue(model.getMinimum());
break;
case DONE:
model.setValue(model.getMaximum());
break;
}
} else if ("progress".equals(property)
&& event.getNewValue() instanceof Integer) {
int value = (Integer) event.getNewValue();
model.setValue(value);
}
}
});
return this;
}
On se retrouve donc avec un SwingWorker gérant automatiquement les exceptions par défaut (tout en nous laissant les traiter plus spécifiquement si besoin) grâce à un découpage plus précis de la méthode done() et un appel implicite à get(), et qui permet de lier en toute simplicité l’état d’avancement de la tâche avec une barre de progression…
Mais surtout via l’héritage on conserve également toutes les fonctionnalités de base du SwingWorker :
- Une valeur en résultat de l’exécution de la méthode doInBackground() (via le premier paramètre Generics).
- La publication de données intermédiaires grâce aux méthodes publish()/process() (via le second paramètre Generics).
- La gestion de l’état de progression via setProgress() (que l’on peut désormais facilement associer à une barre de progression).
- L’interruption de la tâche via la méthode cancel(true).
- La possibilité de passer la tâche à un autre ExecutorService que celui par défaut, qui pourrait être plus adapté à nos besoins.
Generics ou pas ? Les deux mon colon !
Mais contrairement à la proposition original du « BetterSwingWorker« , notre « BetterWorker » conserve le double paramétrage Generics, ce qui peut s’avérer assez verbeux, en particulier dans les cas où on ne l’utilise pas.
Pour pallier à cela, on va passer par deux nouvelles classes filles :
- La classe ResultWorker se contentera d’un seul paramétrage pour le type de retour de doInBackground(), ce qui permettra de simplifier son utilisation lorsqu’on ne souhaite pas utiliser les méthodes publish()/process(), mais que l’on souhaite quand même utiliser le type de retour de la méthode doInBackground() :
public abstract class ResultWorker<T> extends BetterWorker<T, Void> {
@Override @Deprecated
protected final void process(List<Void> chunks) {
// Empty
}
}On en profite pour rendre la méthode process()
final
et deprecated (désormais il n’y a plus de raison de la redéfinir). - La classe SimpleWorker ne sera pas paramétrée du tout, et on se passera alors du type de retour de la méthode doInBackground() :
public abstract class SimpleWorker extends ResultWorker<Void> {
protected abstract void inBackground() throws Exception;
protected void onSuccess() {
// Nothing by default
}
@Override
protected final Void doInBackground() throws Exception {
inBackground();
return null;
}
@Override
protected final void onSuccess(Void result) {
onSuccess();
}
}On en profite pour redéfinir doInBackground() et onSuccess(Void) afin de renvoyer vers les nouvelles méthodes inBackground() et onSuccess() qui disposent d’une signature un peu plus adaptée qui nous épargne de ces Void disgracieux…
On dispose ainsi de toutes les alternatives possibles :
- BetterWorker<T, V> correspond à la version la plus complète.
- ResultWorker<T> fait l’impasse sur la publication des données intermédiaire.
- SimpleWorker ne propose aucun résultat.
De quoi s’adapter à tous les besoins…
N’hésitez pas à critiquer cette proposition, que vous pouvez télécharger directement ici : better-workers-dev.jar (archive comprenant le binaire + les sources)
5 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- Classes, méthodes private
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- [ fuite ] memoire
- jre 1.5, tomcat 6.0 et multi processeurs
- Possibilité d'accéder au type générique en runtime
- Recuperation du nom des parametres
- Définition exacte de @Override
- Difference de performances Unix/Windows d'un programme?
[…] Swing : « Better SwingWorkers » par adiGuba (10/11/2010 12:03) Il y a quelques temps, j’étais tombé sur une série de billet anglophone proposant un SwingWorker amélioré, tel que celui-ci sur le blog de Baptiste Wicht : A better SwingWorker without exception swallowing Pour rappel cette classe permet d’exécuter des tâches en arrière-plan (via la méthode doInBackground()) afin de ne pas bloquer l’EDT (le thread gérant l’affichage graphique). Et à la fin du traitement, la méthode done() nous permet de mettre à jour l’interface graphique. […] […]
@dingoth : les deux solutions sont possibles…
Le fait de traiter l’exception avec onError() permet justement de centraliser les erreurs.
Maintenant onSuccess() est exécuté dans l’EDT et ne permet que de remonter des unchecked-exceptions : ces exceptions seront simplement loggué sur System.err (opération par défaut dans l’EDT).
a++
Je ne suis pas tout à fait d’accord avec l’utilisation faite du onSuccess dans le « BetterWorker ».
En effet, si une exception survient dans la méthode onSuccess(), elle est gérée comme ayant raté et activera la méthode onError().
Donc, je propose de réécrire la méthode done() comme ceci :
protected final void done() { <br />
try { <br />
boolean success = false; <br />
T result = null; <br />
try { <br />
result = get(); <br />
success = true; <br />
} catch (ExecutionException e) { <br />
onError(e.getCause()); <br />
} catch (Throwable t) { <br />
onError(t); <br />
} <br />
if (success) { <br />
onSuccess(result); <br />
} <br />
} finally { <br />
onEnd() <br />
} <br />
}
Maintenant, j’avoue ne pas savoir comment on pourrait traiter une exception dans le onSuccess(T)…
Pour le nom, j’ai simplement repris celui de l’article original, en le raccourcissant un peu…
a++
PS : et pour info, « colon » est un raccourci souvent utiliser pour désigner un « colonel ».
Bien entendu je ne parlais pas de l’organe digestif
Sympathique, le seul défaut qui me choque est le nom de la classe BetterWorker qui fait un peu pompeux. J’aurais choisi un nom plus neutre comme CheckedWorker.
Les deux mon colon !il doit manquer deux lettres à la fin. Personnellement, je ne m’adresse que très rarement a mon colon…