image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Introduction aux annotations

Les annotations ne font rien en elles-mêmes. Elles ne sont que des indications fournies sur le code à destination d'outils externes travaillant sur ce code.

Création d'une nouvelle annotation

Les annotations sont des interfaces (prefixées par @) avec des méthodes sans argument indiquant leur propriétés.

On peut indiquer une clause default pour specifier une valeur par défaut si la propriété n'est pas définie lorsque l'on utilise l'annotation.

Déclarons une annotation :

package fr.upem.jacosa.annotations;

@interface StringConstraint
{
	boolean nullable();
	String regex() default "";
	int minLength() default 0;
	int maxLength() default Integer.MAX_VALUE;
}

... et utilisons-là pour un champ d'une classe :

public class Contact
{
	...
	/** The phone number contains only digits */
	@StringConstraint(nullable=false, regex="[0-9]*", minLength=10)
	private String phoneNumber;
	...
}

La machine virtuelle ne va pas tester automatiquer la contrainte. Nous pouvons ensuite écrire une classe utilitaire avec une méthode statique que nous allons appeler dans le setter pour valider la contrainte :

package fr.upem.jacosa.annotations;

import java.lang.reflect.Field;
import java.util.regex.Pattern;

public class ConstraintChecker
{
	public static void checkConstraints(Field field, Object value)
	{
		StringConstraint sc = field.getAnnotation(StringConstraint.class);
		if (sc != null)
		{
			if (value == null && ! sc.nullable()) 
				throw new ConstraintViolation("nullability constraint not respected");
			if (value != null)
			{
				String stringValue = value.toString();
				if (stringValue.length() < sc.minLength())
					throw new ConstraintViolation("minimum length not respected with " + value);
				if (stringValue.length() > sc.maxLength())
					throw new ConstraintViolation("maximum length not respected with " + value);
				if (! Pattern.matches(sc.regex(), stringValue))
					throw new ConstraintViolation(stringValue + " does not match the regexp " + sc.regex());
			}
		}
	}
	
	/** Calling this method raise an exception if there is a constraint violation */
	public static void checkConstraints(Class<?> klass, String fieldName, Object value)
	{
		try {
			checkConstraints(klass.getField(fieldName), value);
		} catch (NoSuchFieldException | SecurityException e) {
			throw new RuntimeException(e); // transform to an unchecked exception
		}
	}
}

Nous pouvons ensuite implanter le setter dans Contact :

package fr.upem.jacosa.annotations;

public class Contact
{
	@StringConstraint(nullable=false, minLength=1)
	private final String name;
	
	public Contact(String name)
	{
		ConstraintChecker.checkConstraints(getClass(), "name", name);
		this.name = name;
	}
	
	@StringConstraint(nullable=false, regex="[0-9]*", minLength=10)
	private String phoneNumber;
	
	public void setPhoneNumber(String phoneNumber)
	{
		ConstraintChecker.checkConstraints(getClass(), "phoneNumber", phoneNumber);
		this.phoneNumber = phoneNumber;
	}
}

Quelques frameworks de manipulation de bytecode :

Exemple d'utilisation du framework Javassist avec une classe permettant d'introduire un méta-objet capturant les appels de méthodes, de lecture et d'écriture de champ :

package fr.upem.jacosa.annotations;

import javassist.tools.reflect.*;

public class ConstraintMetaObject extends Metaobject 
{
    public ConstraintMetaObject(Object self, Object[] args) 
    {
        super(self, args);
        System.out.println("** constructed: " + self.getClass().getName());
    }

    public Object trapFieldRead(String name) 
    {
        System.out.println("** field read: " + name);
        return super.trapFieldRead(name);
    }

    public void trapFieldWrite(String name, Object value) 
    {
		ConstraintChecker.checkConstraints(getObject().getClass(), name, value);
        System.out.println("** field write: " + name);
        super.trapFieldWrite(name, value);
    }

    public Object trapMethodCall(int identifier, Object[] args) throws Throwable 
    {
        System.out.println("** trap: " + getMethodName(identifier) + "() in "
                + getClassMetaobject().getName());
        return super.trapMethodcall(identifier, args);
    }
}

Une étape supplémentaire à la compilation est nécessaire pour modifier le bytecode et introduire la vérification de contrainte :

javac *.java # to compile the Java files as usual
# do not forget to put the javassist.jar in the classpath priorly to this command
java javassist.tools.reflect.Compiler Contact -m ConstraintMetaObject # to transform the Contact class and introduce the contraint checkings
java ContactMain # to execute the Contact main as usual

package fr.upem.jacosa.annotations;

public class ContactMain
{
	public static void main(String[] args)
	{
		Contact contact = new Contact("foo");
		contact.setPhoneNumber("1234"); // should fail since it does not respect the constraint
	}
}

Les annotations peuvent ainsi être utilisées pour la programmation orientée aspect (AOP). Ce paradigme de programmation traite des fonctionnalités transversales d'un projet logiciel telles que :

Exemple d'utilisation d'annotation pour l'injection de dépendance

L'injection de dépendances est un mécanisme qui permet de séparer le code construisant les instances dans un fichier de configuration (ici une classe Java de configuration mais cela peut aussi être un fichier XML).

Nous créeons une classe Customer avec des annotations pour injecter automatiquement tous les champs (nous laissons un constructeur par défaut sans argument pour faciliter le travail de l'injecteur).

package fr.upem.jacosa.annotations;

import com.google.inject.Inject;
import com.google.inject.name.Named;

/** A simple customer for an online shop */
public class Customer
{
	@Inject @Named("customerName")
	private String name = null;
	
	@Inject @Named("customerAddress")
	private String address = null;
	
	@Inject
	private ShoppingBasket basket;
	
	public String getName()
	{
		return name;
	}
	
	public String getAddress()
	{
		return address;
	}
	
	public ShoppingBasket getBasket()
	{
		return basket;
	}
	
	@Override
	public String toString()
	{
		return "Customer[" + name + "@" + address + "]";
	}
}

Cette classe dispose d'annotations d'injection pour les champs name, ''address`` et ``ShoppingBasket``.

ShoppingBasket est une interface. Nous implantons une classe concrète ShoppingBasketImpl que l'on peut instancier.

Ensuite nous mettons en place le fichier de configuration pour l'injection (le module d'injection) :

package fr.upem.jacosa.annotations;

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;

/** Module to configure the injection for the customer */
public class MyInjectionModule extends AbstractModule
{
	@Override
	protected void configure()
	{
		bind(Customer.class).to(Customer.class);
		bind(ShoppingBasket.class).to(ShoppingBasketImpl.class);
		bind(String.class).annotatedWith(Names.named("customerName")).toInstance("Santa Claus");
		bind(String.class).annotatedWith(Names.named("customerAddress")).toInstance("Laponia");
	}

}

On peut ensuite procéder à l'instanciation automatique de la classe Customer en utilisant le module précédemment écrit avec la bibliothèque d'injection de dépendances Guice :

package fr.upem.jacosa.annotations;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class CustomerMain
{
	public static void main(String[] args)
	{
		Injector injector = Guice.createInjector(new MyInjectionModule());
	    Customer customer = injector.getInstance(Customer.class);
	    System.out.println(customer);
	    // ... we can add code here
	}
}

Avantage de l'approche :

Inconvénient de l'approche :

Les propriétés d'une annotation

On déclare les propriétés d'une annotation avec des méthodes. Une valeur par défaut peut être indiquée : dans ce cas la propriété n'a pas à être obligatoirement indiquée lors de son usage. Seulement certains types immutables peuvent être employés pour les annotations :

Les valeurs par défaut doivent être des constantes ; elles ne peuvent pas être calculées à l'exécution. Pour une String, la valeur par défaut ne peut pas être null.

Il est possible de déclarer une unique propriété que l'on nommera value. Dans ce cas l'usage de l'annotation est simplifié car on n'a pas à indiquer le nom de la propriété (on spécifie uniquement sa valeur).

Pour les types tableau, il est possible d'indiquer une unique valeur sans spécifier d'accolade ou plusieurs valeurs avec des accolades.

Les emplacements des annotations

Lorsque l'on déclare une annotation, on annote l'annotation avec la méta-annotation @Target(...) pour indiquer la ou les cibles d'une annotation (endroits où elle peut être utilisée).

En Java, il est possible d'annoter :

Depuis Java 1.8, les annotations peuvent également accompagner des types (préfixage) lors :

La rétention d'une annotation

La politique de rétention se définit avec la méta-annotation @Retention(...) à appliquer sur l'annotation. Elle indique comment conserver l'annotation

Récupération d'annotations par introspection

Un objet de type Class, Field, Constructor ou Field dispose des méthodes suivantes pour récupérer les annotations declarées (implantation de l'interface AnnotatedElement) :

Une fois une annotation récupérée, on peut appeler ces méthodes pour obtenir les informations qu'elle embarque.

Les annotations de l'API

L'API Java Standard Edition embarque déjà un certains nombres d'annotations utiles :

Les méta-annotations de l'API

Les méta-annotations sont des annotations d'annotations. Elles s'appliquent sur les annotations pour indiquer certaines propriétés. L'API standard en propose quelques une que voilà :