Thématiques principales

lundi 12 mars 2018

Les annotations


Nous avions parlé des annotations lorsque nous avions traité des evolutions Java 5 [1]. Aujourd’hui je vous propose de nous intéresser à en faire quelques unes et de réaliser le ou les processeurs d’annotations qui vont bien pour les traiter dans les contextes de la compilation puis at runtime. Nous tâcherons de traiter les différents cas que nous pourrions rencontrer en définissant des annotations sur des cibles variées.

Tout d’abord, premier point, une annotation c’est quoi? Alors pour présenter cela, je vais reprendre l’exemple que nous avions vu alors dans [1]:

@SuppressWarnings(value = { "static-method" })
public <S> S functionGen(S s)
{
 return s;
}

Comme vous l’avez deviné, l’annotation ici est évidemment SuppressWarnings. Elle est identifiable via un @ et prend ici une liste de paramètres réduits à un seul. Le détail nous importe peu, ce qui est intéressant est l’information que l’annotation va transporter.

Une annotation a un nom, potentiellement des variables, et si ce n’est pas visible ici, possèdes des informations sur son contexte d’utilisation, et de sa porté. Ces deux derniers éléments sont positionnés lors de la définition de l’annotation.

Son contexte d’utilisation permet de préciser où celle ci pourra être utilisé. Ici dans l’exemple, l’annotation est positionné sur la méthode mais celle-ci peut être positionné sur une classe, une donnée membre ou même un package, tous ces éléments n'étant pas en exclusion mutuelle. Potentiellement une même annotation peut être utilisé sur plusieurs type de structures différentes.

De façon complète, il est possible de positionner une annotation (sauf définit contrairement) sur:

  • les classes 
  • les méthodes 
  • les interfaces 
  • les variables 
  • les packages


Au delà du cadre de son utilisation, la porté a par contre plus d’importance sur l’utilisation fonctionnelle de l’annotation. En effet, la porté définit à quel moment l’annotation doit être traitée comme une information utile au programme java. Celle ci peut être soit intégré qu’au code source (et ne sera plus visible ensuite, on utilise ce type d’annotation essentiellement pour donner de l’information au développeur ou aux analyseurs de code), soit intégré au code binaire (permettant au compilateur de réaliser des tâches supplémentaires) soit intégrée pour être traité au runtime afin de permettre d'adjonction de comportement dynamique (beaucoup de framework utilise aujourd’hui ce type d’API)

Bien sur ces différents cas d’utilisation des annotations nécessitent des mécanismes et des traitements différents. Autant on comprend vite comment va fonctionner les traitements d’annotations associé au code source qui auront surtout un rôle informatif. Par contre lorsque les annotations sont traité lors de la phase de compilation, il sera nécessaire d’utiliser le mécanisme des processeurs d’annotations (l’API Pluggable Annotation Processor). Enfin lorsque celles ci sont définies pour être traité at runtime, il faudra user de l’introspection et de certains type de pattern afin de profiter intelligemment des possibilités des annotations.

Mais avant de pouvoir traiter les annotations, je vous propose de reprendre tous ces points en définissant notre propre annotation.

Prenons d’abord l’exemple d’une annotation fournissant un lien documentaire explicitant la référence fonctionnelle ou technique amont. Pour la définir, nous allons créer une pseudo interface en utilisant le caractère @ avant le mot clef interface. Nous y déclarent une propriété document et une propriété chapitre et une propriété page.

Ces informations portent le côté fonctionnel, il faut maintenant définir le côté technique pour spécifier le contexte d’utilisation et de traitement.

Pour cela il faut utiliser des méta-annotations (une annotation pour annotations):

  • @Documented : Permet de préciser la présence de l’annotation dans la java doc de l'élément portant celle ci 
  • @Inherit : Permet de transmettre les propriétés informationnelles de l’annotation aux classes filles de l'élément portant celle-ci 
  • @Retention : précise le niveau de traitement (source, bitcode ou runtime) 
  • @Target: précise la ou les cibles sur lesquels l’annotation peut être apposé.


Dans le contexte très basique de cet exemple, nous nous limiterons a simplement a specifier la Retention a source.

@Retention(RetentionPolicy.SOURCE)
public @interface Reference
{
 String document();
 String chapitre();
 int page();
}

Voilà dans nos classes nous pourrons alors par exemple préciser à quelle partie de la documentation de la spécification cliente les classes font références:

@Reference(document=”docAmont.doc”,chapitre=”Service Client”,page=44)
public class ServiceClient
{
 public void service();
}

Imaginons maintenant une nouvelle annotation nécessitant un traitement plus profond à effectuer lors de la compilation. Par exemple une annotation @Relation qui associe a une classe permettra la production d’une table du même nom dans une bd (pour faire simple, nous passerons sur la production des attributs de relation mais on imagine facilement qu’il faudrait faire une annotation pour les données membres de la classe)

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Relation
{
}

Ainsi, sur une classe de notre application, nous placerons tout simplement notre annotation ainsi:

@Relation
public class HelloWorld
{
}

Alors bien sur si ceci nous permet de sémantiquement faire un lien entre notre classe et la relation d’une BD, il nous faut procéder à la mise en oeuvre d’un Processeur d’Annotation dont le but sera de récupérer toutes les classes postant celle-ci et exécutera un CREATE TABLE en sql sur la BD en question.

Pour réaliser un processeur d’annotation, il n’y a rien de complexe, il suffit de produire un simple artifact maven de type jar dans lequel nous trouverons:

  • une classe RelationProcessor dérivant la classe AbstractProcessor portant les annotations SupportedAnnotationTypes (précisant quelles annotations nous traitons) et SupportedSourceVersion (précisant dans quel version java la compilation aura lieu) 
  • un fichier de conf dans le répertoire META-INF/services nommé javax.annotation.processing.Processor et contenant le nom complet de notre classe processor.

A noter qu’il sera nécessaire de désactiver l’utilisation des processeurs d’annotations pour le packaging de notre propre processeur (sinon celui ci tentera se traiter lui même), il s’agit de l’option de configuration -proc:none du maven-compiler-plugin. Ensuite il suffira de faire une dépendance à la compilation pour que notre logiciel, lors de sa compile trouve par inspection le fichier de conf de notre jar contenant le processeur et l'exécute.

Voyons comment cela se presente:

Tout d’abord nous allons definir un pom module pour plus de simplicité:

   <modules>
    <module>tc-annotation</module>
    <module>tc-processor</module>
    <module>tc-helloworld</module>
   </modules>


Le premier module contient nos annotation, le second contiendra le processeur et le troisieme sera notre classe helloworld

Le module d’annotation n’a rien de special, c’est un projet maven classique produisant un jar (donc l’utilisation du plugin maven-compiler-plugin est largement suffisant) et contenant la définition de notre annotation:

package annot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Relation {}



Le module processor est un peu plus complexe, comme nous l’avons vu il faut definir une classe heritant de la classe AbstractProcessor. Cette classe va recuperer l’ensemble des elements portant notre annotation et envera une requete en BD pour creer la table associée.

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({ "annot.Relation" })
public class HWProcessor extends AbstractProcessor {

    public static final String JDBC_DRIVER = "org.postgresql.Driver";
    public static final String DB_URL = "jdbc:postgresql://localhost:5432/test";
    public static final String USER = "postgres";
    public static final String PASS = "";
    
    public static String CREATE="CREATE TABLE ";

    @Override
    public boolean process(Set arg0, RoundEnvironment arg1) {
        System.out.println("HWProcessor running");
        Connection conn =null;
        try {
            Class.forName(JDBC_DRIVER);
            conn = this.getConnection();
        
        for (Element e : arg1.getRootElements()) {
            if (e.getAnnotation(Relation.class) != null) {
                this.createRelation(e,conn);
            }
        }
        } catch ( SQLException | ClassNotFoundException e1) {
            System.out.println(e1);
            return false;
        }
        return true;
    }
    
    public void createRelation(Element e,Connection conn) throws SQLException
    {
        PreparedStatement statement=conn.prepareStatement(this.CREATE+ e.getSimpleName().toString()+"();");
        System.out.println("Mon element : "+e.getSimpleName().toString());
        statement.executeQuery();
    }
    
    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(DB_URL, USER, PASS);
       }

}


a noter de ne pas oublier d’ajouter le fichier javax.annotation.processing.Processor

au niveau du build maven, il vous faudra ajouter la ligne -proc:none a la configuration du plugin maven-compiler-plugin. Ne pas oublié non plus de bien sur ajouter les dependances a l’artifact contenant l’annotation et ici en plus a postgres puisqu’il s’agit de la BD que nous utilisons.

 <build>
  <plugins>
   <plugin>
    <groupid>org.apache.maven.plugins</groupid>
    <artifactid>maven-compiler-plugin</artifactid>
    <version>3.7.0</version>
    <configuration>
     <verbose>true</verbose>
     <source></source>1.8
     <target>1.8</target>
     <compilerargument>-proc:none</compilerargument>
    </configuration>
   </plugin>
  </plugins>
 </build>

 <dependencies>
  <dependency>
   <groupid>org.tc.test</groupid>
   <artifactid>tc-annotation</artifactid>
   <version>0.1.0-SNAPSHOT</version>
  </dependency>
  <dependency>
   <groupid>postgresql</groupid>
   <artifactid>postgresql</artifactid>
   <version>9.1-901-1.jdbc4</version>
  </dependency>
 </dependencies>


Enfin il reste a utiliser notre annotation dans notre projet. Donc evidement on depose l’annotation sur la classe:

import annot.Relation;

@Relation
public class HelloWorld {}


Enfin il faudra bien sur configurer notre build. Alors soit on precise dans le plugin maven-compiler-plugin que l’on va utiliser un artifact contenant un processeur d’annotation en eventuellement precisant le processeur :

   <plugin>
    <groupid>org.apache.maven.plugins</groupid>
    <artifactid>maven-compiler-plugin</artifactid>
    <version>3.7.0</version>
    <configuration>
     <verbose>true</verbose>
     <source></source>1.8
     <target>1.8</target>
     <testsource>1.8</testsource>
     <testtarget>1.8</testtarget>
     <annotationprocessorpaths>
      <path>
       <groupid>org.tc.test</groupid>
       <artifactid>tc-processor</artifactid>
       <version>0.1.0-SNAPSHOT</version>
      </path>
     </annotationprocessorpaths>
     <!-- Soit les deux lignes suivantes soit le fichier dans le repertoire 
      services du jar contenant le processor -->
     <annotationprocessors>
      <annotationprocessor>proc.HWProcessor</annotationprocessor>
     </annotationprocessors>
     <!-- Ne pas faire de fork quand on utilise un processeur -->
     <!-- <fork>true</fork> -->
    </configuration>
   </plugin>



Mais cela, on peut aussi se reposer sur la capacité du jdk a decouvrir le processeur et ne declarer l’artifact que comme une dependance de compilation et ne pas mettre les champs annotationProcessors et annotationProcessorPaths (simplifiant largement la complexité du pom au passage). on prefere donc:

<dependencies>
  <dependency>
   <groupid>org.tc.test</groupid>
   <artifactid>tc-annotation</artifactid>
   <version>0.1.0-SNAPSHOT</version>
  </dependency>
  <dependency>
   <groupid>org.tc.test</groupid>
   <artifactid>tc-processor</artifactid>
   <version>0.1.0-SNAPSHOT</version>
   <scope>compile</scope>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupid>org.apache.maven.plugins</groupid>
    <artifactid>maven-compiler-plugin</artifactid>
    <version>3.7.0</version>
    <configuration>
     <verbose>true</verbose>
     <source></source>1.8
     <target>1.8</target>
    </configuration>
   </plugin>
  </plugins>
 </build>



Voila il ne reste plus qu’a builder notre module:

mvn clean install


ok ca marche:

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] tc-annotation-0.1.0-SNAPSHOT ....................... SUCCESS [  4.078 s]
[INFO] tc-processor-0.1.0-SNAPSHOT ........................ SUCCESS [  1.929 s]
[INFO] tc-helloworld-0.1.0-SNAPSHOT ....................... SUCCESS [  1.223 s]
[INFO] tc-annotation-module-0.1.0-SNAPSHOT ................ SUCCESS [  0.038 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------



Voyons voir les log de notre processeur:

HWProcessor running
Mon element : HelloWorld
org.postgresql.util.PSQLException: Aucun résultat retourné par la requête.


Ok donc ca a l’air de s’etre bien passé vérifions la BD:

 psql -U postgres -w -d test -c "SELECT table_name from information_schema.tables where table_name='helloworld';"
 table_name
------------
 helloworld
(1 ligne)


Bonne nouvelle notre table est la dans le schéma!!

Bien voila, nous venons de passer en revu les deux ca d’utilisation que sont les annotations sur les sources et sur les classes. Leur objectifs ne sont clairement pas les même et si le premier cas ne nécessite pas beaucoup de travail, on peut voir qu’il faudra en fournir un peu plus sur le second en élaborant des processeur d’annotations.

Maintenant il reste un dernier cas d’utilisation, les annotations utilisés at runtime. Cependant désolé mais je ne traiterai pas de leur utilisation dans cet article. En effet, d’une part car cela relève globalement de la même approche introspective que pour la conception d’un processeur mais en employant plutôt quelques patterns bien senti comme l’Invocation Handler. Ce dernier est d’ailleur l’objet d’un prochain article et nous en profiterons alors pour jouer avec les dites annotations at runtime et nous verrons que le gros de travail sur le sujet a globalement été réalisé ici.

Références :

[1] Evolution Java 5

Aucun commentaire:

Enregistrer un commentaire