AOP et Swing : un duo élégant

le 30/03/2010 par Olivier Mallassi
Tags: Software Engineering

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?