AOP et Swing : un duo élégant

Ce n’est pas la nouveauté de l’année mais Swing, bien que présent en entreprise, n’évolue que très peu. Le kit de développement offre nativement toujours aussi peu de composants évolués (tableaux triables…) même s’il faut avouer que certaines librairies commerciales compensent à merveille ces manques. Les APIs et le développement Swing est toujours aussi verbeux et finalement assez peu productif (de mon humble avis). Et ce n’est malheureusement pas les quelques JSR en stand by qui vont y changer quoique ce soit : Beans Binding est au statut inactif, Beans Validation, drafté en 2008 n’est toujours pas inclus dans le JDK (peut-être pour la version 7 ?) et la JSR 296 (qui définit au travers de 4 callback le cycle de vie standard d’une application) ne représente pas non plus la plus importante des améliorations qu’ait connu un framework (si si je vous jure j’aime Swing…)

Alors il est nécessaire de compenser ces manques…

A côté de cela, je suis un fan d’AOP depuis les premières versions d’AspectJ. Je trouve ce paradigme de développement d’une rare élégance et il est aujourd’hui suffisamment outillé (intégration aux IDEs, intégration à Maven…) pour pouvoir parsemer quelques aspects de ci de là.

Voilà donc quelques problématiques Swing récurrentes qu’il est possible d’outiller avec AOP.

Injection de services

L’objectif est simple : reproduire l’IOC au niveau de l’IHM et notamment dans les actions (en fait les ActionListener branchés sur les boutons). Dès lors on souhaite injecter une implémentation du service (définie par exemple comme un bean Spring) sur toute propriété qui aurait l’annotation custom @Inject.

Par exemple, injecter le service myService dans l’action suivante (on peut faire mieux mais dans cet exemple, le bean est injecté sur la base du nom et non du type) :

public class MyActionListener implements ActionListener{
	@Inject
	private MyService myService

	...
}

L’aspect suivant permet l’injection du Bean Spring nommé myService

public aspect SpringInjectAspect {
    pointcut fieldsInjection() : within(com.mypackage..*)
	    execution(public void java.awt.event.ActionListener+.actionPerformed(..));

    before() : fieldsInjection() {
        Class currentClass = thisJoinPoint.getThis().getClass();
        Object currentObject = thisJoinPoint.getThis();
        //to inject bean spring even in super class property
        while (!currentClass.equals(ActionListener.class)) {
             injectBean(currentClass, currentObject);
             //for te next step
             currentClass = currentClass.getSuperclass();
        }
    }

    private void injectBean(Class currentClass, Object currentObject) {
        Field[] fields = currentClass.getDeclaredFields();

        for (int i = 0; i < fields.length; i++) {
            Annotation associatedAnnotation = fields[i].getAnnotation(Inject.class);
            if (associatedAnnotation != null) {
                try {
					//get the bean by the field name
                    Object implementationObject = SpringHelper.getApplicationContext().getBean(
                            fields[i].getName());
                    try {
                        fields[i].setAccessible(true);
                        fields[i].set(currentObject, implementationObject);
                    }
                    catch (IllegalArgumentException e) {
                       ... do what you need to do
                }
                catch (NoSuchBeanDefinitionException e) {
					//log the fact the asked bean do not exist
                    throw e;
                }
            }
        }
    }
}

Outillage du Binding (notamment dans le cadre d’une utilisation avec jGoodies)

L’utilisation d’un mécanisme de binding type jGoodies peut-être intéressant mais quoiqu’il en soit, il passe souvent par l’écriture de code supplémentaire notamment dans les setter :

public void setBooleanValue(boolean newValue) {
  boolean oldValue = booleanValue;
  booleanValue = newValue;
  changeSupport.firePropertyChange("booleanValue", oldValue, newValue);
 }

Dans cet exemple, il est nécessaire de rajouter dans chaque setter, l’appel à la méthode fireProperyChange en précisant nouvelle et ancienne valeur et surtout le nom de la propriété (ce qui bien entendu sera source d’erreur puisque ce code sera copié-collé de setter en setter…).

Là encore AOP nous aide en permettant d’enrichir un setter « classique » (i.e. qui ne fait que mettre à jour la propriété). Ainsi le setter se définit classiquement :

public void setBooleanValue(boolean newValue) {
  booleanValue = newValue;
 }

et l’aspect suivant se charge du reste :

public aspect jGoodiesBindingAspect {
    pointcut propertyEnhancement() : within(com.mypackage..*)
		&& execution (public void com.jgoodies.binding.beans.Model+.set*(..))

    void around() : propertyEnhancement() {
        Object[] args = thisJoinPoint.getArgs();
        String propertyName = thisJoinPoint.getSignature().getName().substring(3);

        Object oldValue = null;
        try {
            Method propertyGetter = thisJoinPoint.getThis().getClass()
                    .getMethod("get" + propertyName);
            oldValue = propertyGetter.invoke(thisJoinPoint.getThis());
            Object newValue = args[0];

            proceed();
            // if no error occurs
            Method firePropertyChangeMethod = com.jgoodies.binding.beans.Model.class.getDeclaredMethod(
                    "firePropertyChange", String.class, Object.class, Object.class);
            firePropertyChangeMethod.setAccessible(true);
            propertyName = org.apache.commons.lang.StringUtils.uncapitalize(propertyName);
            firePropertyChangeMethod.invoke(thisJoinPoint.getThis(), propertyName, oldValue, newValue);
        }
        catch (NoSuchMethodException
			...do what you need to do
    }
}

Dans ce cas, le pointcut est défini sur toutes les méthodes qui commencent par set de n’importe quelle classe héritant de com.jgoodies....Model.

Habilitation sur la fonction et la donnée au niveau de l’IHM

Il s’agit là encore d’une problématique hyper classique : comment rendre « inaccessible » (comprendre non modifiable, non utilisable) des fonctions (des boutons, des menus…) d’une IHM Swing. On parle donc ici de « griser » (ie. myComponent.setEnabled(...)) les composants graphiques soumis à une habilitation.

La première étape consiste à définir une annotation permettant de définir les rôles sur un JComponent. Rien de génial là dedans. Il est possible de l’utiliser ainsi et de définir que myComboBox est accessible pour les utilisateurs ayant les rôles « consultation-level-1 » et « consultation-level-2 »

@Authorize(roles={"consultation-level-1, consultation-level-2"})
private JComponent myComboBox;

L’aspect suivant prend en charge le « grisage/dégrisage » des widgets graphiques suivant le rôle

public aspect AuthorizationAspect {
    public pointcut authorized_gui() :  within(com.mypackage..*)
        && set(@Authorize * *..*.*);

    after() : authorized_gui() {
        String pointCutKind = thisJoinPoint.getKind();
        if (JoinPoint.FIELD_SET.equals(pointCutKind)) {
            java.lang.reflect.Field field = ((FieldSignature) thisJoinPoint.getSignature()).getField();
            try {
                field.setAccessible(true);
                Object theField = field.get(thisJoinPoint.getThis());
                if (theField instanceof JComponent) {
                    Authorize authorization = field.getAnnotation(Authorize.class);
                    String[] roles = authorization.roles();
					//ask your security context holder the role the user have
                    ((JComponent) theField).setEnabled(SecurityContextHolder.getInstance()
                            .isUserInRole(roles));
                }
            }
            catch (IllegalAccessException e) {
                throw new TechnicalException(e);
            }
        }
    }
}

La subtilité dans cet aspect réside dans le set(@Authorize * *..*.*) qui permet d’attraper l’instanciation (et non pas l’appel au constructeur) de toutes les propriétés qui ont l’annotation spécifiée.

Voilà donc quelques problématiques qu’il est possible de régler de façon assez simple, élégante et peu intrusive sur le code grâce à AOP. D’autres utilisations existent. La plus courante est certainement de gérer de façon automatique toutes les exceptions non typées – qui dans le cas d’une IHM donne typiquement lieu à l’affichage (dans une popup ou non) d’un message du type « une erreur a eu lieu, veuillez contacter votre administrateur ». D’autres utilisations encore peu répandues visent à valider certaines règles de codage propre à votre projet (via l’utilisation de declare warning et declare error).

Et vous, voyez vous d’autres problématiques qu’il serait intéressant de gérer de la sorte?

4 commentaires sur “AOP et Swing : un duo élégant”

  • Pour pousser un tout petit peu plus loin, tu pourrais même rajouter la sécurité Spring Security par annotations sur ton dernier composant. Excellent article qui permet d'alléger le code Swing qui se retrouve très très souvent bien lourd ...
  • Très intéressant ! Je n'avais jamais pensé à assembler de cette manière l'IOC, l'AOP et Swing. C'est effectivement une piste intéressante à explorer.
  • Alors justement, par rapport à Spring Security. C'est une question que je me suis posé : utiliser ou non Spring Security. J'en suis arrivé à la conclusion que j'allais utiliser uniquement l'annotation (@Secured de mémoire) et finalement rien d'autre de Spring Security (puisque finalement côté client, j'ai uniquement besoin de conserver la liste des rôles de l'utilisateur courant). @Jean-Philippe : est ce que j'ai loupé un truc? est ce que tu peux nous en dire plus? Qu'est ce qu'on pourrait faire d'autre pour alléger le code Swing? Le binding (si on utilise des outils type jGoodies) pourrait être intéressant à réaliser de la sorte mais pas forcément très simple à mettre en oeuvre. Genre plutôt que d'écrire Binding.bind(monwidget, mypropertyValueModel) on pourrait essayer de mettre une annotation sur le widget genre @binding(myproperty) et l'aspect aurait à charge la définition des ValueModel et la cablage. d'autres trucs?
  • Tu as raison , je trouve DOMMAGE qu'un si grand langage comme JAVA n'implémente pas nativement certains concepts comme le databinding et que les composants SWING soient peu avancés ; quand nous voyons la puissance de DOTNET en matière de développement d'interfaces graphiques riches ( databinding , Winforms et surtout WPF et Silverlight (xaml)). J'avais comme l'impression que les développeurs de Java s'en foutaient royalement ( si si j'aime JAVA) jusqu'au jour ou j'ai découvert qu'ils essayaient d'y rémédier en créant SwingX (une librairie du projet SwingLabs qui se veut etre une évolution de Swing ). C'est déjà un premier pas mais encore suffisant. Espérons que JAVA 7 nous apportera des nouveautés concernant cela . Je me disais aussi que "Sun d'Oracle" pourrait faire pareil que Microsoft , cest à dire créer un langage xml (comme xaml de Microsoft) destiné au développement de GUI riches en java . Ce serait bien. Merçi pour l'article
    1. Laisser un commentaire

      Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha