9. Intercepteurs

9.1. Introduction

Les intercepteurs sont une nouveauté de JEE5, étendue en JEE6. Fonctionnellement, un intercepteur est une classe Java classique, qui possède une méthode appelée juste avant l'appel à une méthode métier particulière. Elle reçoit les paramètres de la méthode interceptée, et s'exécute dans le même contexte transactionnel. Elle connaît le type de retour de la méthode interceptée, et peut décider ou non de l'appeler finalement ou pas. Dans le cas où elle décide de ne pas appeler cette méthode, elle doit retourner une valeur du bon type en lieu et place de cette méthode. Cette technique est très puissante, et permet de faire au moins deux choses.
  • De la journalisation : un intercepteur peut enregistrer dans des fichiers de journalisation tous les appels de toute ou partie des méthodes métier d'un EJB, éventuellement avec les paramètres d'appel.
  • De la sécurité : il est très facile de vérifier si les paramètres transmis à la méthode métier sont valides ou non. S'ils ne le sont pas, alors une erreur peut être générée dès l'intercepteur, avant l'invocation finale de la méthode métier de l'EJB.
Un intercepteur est censé pouvoir déclarer des dépendances injectées. C'est ce qui est écrit dans la spécification ; malheureusement, l'expérience prouve que ce point n'est pas toujours respecté.

9.2. Aperçu général

D'un point de vue technique, un intercepteur est une classe Java classique, qui doit posséder un constructeur vide. Un intercepteur doit posséder une unique méthode annotée par @AroundInvoke, dans sa propre classe ou dans une de ses super classes. Cette méthode ne peut être ni static ni final, en revanche elle peut être indifféremment private, protected ou public. Cette méthode doit avoir la signature suivante.

Exemple 72. Exemple d'intercepteur

public  class LoggingInterceptor {
    
     @AroundInvoke
     private Object checArguments(InvocationContext context) {
        
         // corps de la méthode
    }
    
     // reste de la classe
}

9.3. Cycle de vie d'un intercepteur

Une instance d'intercepteur partage exactement le même cycle de vie que l'EJB qu'elle intercepte. En particulier, dans le cas d'un EJB avec état, cette instance peut être passivée en même temps que l'EJB, et effacée le cas échéant. Un intercepteur peut également porter des méthodes annotées @PostConstruct et @PostActivate pour la construction et l'activation, ainsi que @PreDestroy et @PreActivate pour l'effacement et la passivation. Ces méthodes annotées doivent prendre l'objet InvocationContext en paramètre, et invoquer sa méthode proceed() pour que les autres méthodes annotées de la même manière, que ce soit dans d'autres intercepteurs ou dans l'EJB final soient également appelées.

9.4. Object InvocationContext

Une instance de cette interface est transmise à la méthode appelée dans l'intercepteur à chaque interception. Elle propose six méthodes particulières.
  • getContextData() : retourne une Map qui porte les données associées au contexte d'invocation.
  • getMethod() : retourne une référence sur la méthode interceptée.
  • getParameters() : retourne un tableau d' Object qui porte les paramètres passés à cette méthode.
  • getTarget() : retourne une référence sur l'instance de l'EJB intercepté.
  • setParameters(Object []) : l'appel à cette méthode permet de modifier les paramètres envoyés à la méthode interceptée.
  • proceed() : cette méthode doit être appelée pour continuer l'exécution normale du processus d'interception.
Comme on le voit, l'intercepteur a la main sur de nombreuses choses, notamment sur les valeurs des paramètres envoyées à la méthode interceptée, qu'il peut modifier. La méthode proceed() a un statut particulier. C'est le fait de l'invoquer qui permet d'invoquer la méthode interceptée, ou l'intercepteur suivant s'il y en a un. Donc, l'intercepteur dans lequel on se trouve a le choix, de poursuivre le processus d'exécution nominal, et d'exécuter la méthode métier, ou de l'interrompre et donc d'empêcher son exécution. Une méthode d'interception suit donc le modèle suivant.

Exemple 73. Écriture d'un intercepteur

public  class SecurityInterceptor {

     // méthode appelée par l'interception
     @AroundInvoke
     private Object enforce(InvocationContext context)  throws Exception {

         // la méthode validate vérifie que le context
         // répond bien aux exigences de sécurité
         if (validate(context)) {
        
             // cet appel invoque la méthode métier interceptée
            Object returnedObject = context.proceed() ;
             // on récupère l'objet retourné, que l'on pourrait modifier
             return returnedObject ;

        }  else {
        
             // on décide ici de ne pas appeler la méthode métier finale, 
             // puisque la méthode valide() a retourné false
            Method method = context.getMethod() ;
            Class<?> returnedType = method.getReturnType() ;
            Object returnedObject = getDefaultInstance(returnedType) ;
             return returnedObject ;
        }
    }
}

Dans le cas où l'on décide de ne pas appeler la méthode finale, les quelques lignes de traitement permettent de construire un objet par défaut, de même type que le type de retour de la méthode interceptée. Plutôt que d'invoquer cette méthode, de récupérer l'objet qu'elle retourne et de le renvoyer à notre tour, on décide de retourner cet objet. Dans ce passage, on n'invoque donc pas la méthode métier de notre EJB. Le client qui a invoqué cette méthode, lui, ne se rend compte de rien. Il reçoit en retour un objet par défaut, conforme aux contrats de cette méthode métier.

9.5. Interception d'un EJB ou d'une méthode métier

Intercepter toutes les méthodes métier d'un EJB, ou une unique méthode, se fait en posant l'annotation @Interceptors sur la classe de cet EJB, ou sur une méthode particulière. On peut déclarer plusieurs intercepteurs, auquel cas on les déclare dans un tableau, attribut de @Interceptors. Voyons ceci sur un exemple.

Exemple 74. Déclaration d'intercepteurs sur un EJB

@Interceptors({
    SecurityInterceptor.class, 
    LoggingInterceptor.class
})
 @Stateless(mappedName="MarinService")
 @Remote(MarinService.class)
 public  class MarinServiceImpl  implements MarinService {

     @Interceptors(ValidationInterceptor.class)
     @Override
     public  long createMarin(String nom) {
        
         // corps de la méthode
    }
    
     // reste de la classe
}

Les trois classes utilisées dans cet exemple doivent donc chacune avoir une méthode annotée @AroundInvoke. Lors de l'appel à la méthode createMarin(String), le serveur d'application se rend compte que trois intercepteurs sont définis. Il va donc les invoquer, dans l'ordre dans lequel ils sont écrits, en commençant pas les intercepteurs définis sur la classe. Dans notre exemple, cet ordre sera donc : SecurityInterceptor.class, LoggingInterceptor.class et ValidationInterceptor.class. Pour chacun de ces intercepteurs, il va invoquer la méthode annotée par @AroundInvoke. Il passera en paramètre de cette méthode un objet de type InvocationContext construit à partir des informations de la méthode interceptée.

9.6. Exemple de mise en œuvre d'un intercepteur

Montrons comment il est possible de créer un intercepteur qui code la logique de validation d'accès à une méthode. Prenons l'exemple de notre méthode createMarin(String). Nous allons créer un annotation @StringNotNull, qui, une fois posée sur notre méthode, permettra à un intercepteur de tester si le paramètre est nul ou pas, et s'il l'est, de ne pas appeler cette méthode.

9.6.1. Écriture d'une annotation @StringNotNull

Voici le code de cette annotation.

Exemple 75. Annotation @StringNotNull

@Target(ElementType.PARAMETER)
 @Retention(RetentionPolicy.RUNTIME)
 public  @interface StringNotNull {
}

Une annotation est déclarée par le mot-clé @interface (notons le caractère @ avant le mot-clé interface). Une annotation est un élément ajouté au byte code d'une classe, qui peut être posé sur différents éléments d'une classe, tels que la classe elle-même, ses champs, ses constructeurs, ses méthodes, ou encore les paramètres de ses méthodes. Ici, cette annotation est présente dans le byte code , et chargée par la JVM. Elle peut être posée sur les paramètres d'une méthode.

9.6.2. Utilisation de l'annotation @StringNotNull

On peut donc modifier le code de notre EJB MarinServiceImpl pour utiliser cette annotation.

Exemple 76. Utilisation de l'annotation @StringNotNull

@Stateless(mappedName="MarinService")
 @Remote(MarinService.class)
 public  class MarinServiceImpl  implements MarinService {

     @PersistenceContext(unitName="cours-ear-pu")
     private EntityManager em ;

     @Override
     public  long createMarin(@StringNotNull String nom) {

        Marin marin =  new Marin() ;
        marin.setNom(nom) ;
        em.persist(marin) ;

         return marin.getId() ;
    }
}

Pour le moment, le comportement de notre EJB ne change pas, il faut encore définir un intercepteur dont le rôle sera d'analyser les paramètres des méthodes métier, de regarder si certains sont annotés par @StringNotNull, et d'appliquer la règle de non-nullité. Écrivons cet intercepteur.

Exemple 77. Intercepteur associé à @StringNotNull

public  class StringNotNullInterceptor {

     // méthode permettant de retourner une valeur par
     // défaut à partir d'un type
     private Object getDefaultValue(Class<?> type) {

         // cette méthode retourne true si le type 
         // correspond en fait à un type primitif Java
         if (type.isPrimitive()) {

             if (byte.class.equals(type)) {
                 return  0 ;
            }  else  if (short.class.equals(type)) {
                 return  0 ;
            }  else  if (int.class.equals(type)) {
                 return  0 ;
            }  // etc... ne sont pas traités : long, boolean, char, 
             // float, double
        }

         return null ;
    }

     @AroundInvoke
     private Object validateStringNotNull(InvocationContext context)  throws Exception {

        Method method = context.getMethod() ;

         // lecture des types des paramètres de la méthode interceptée
        Class<?>[] parameterTypes = method.getParameterTypes() ;
        
         for (int index =  0 ; index < parameterTypes.length ; index++) {
             // analyse des paramètres de type String
             if (String.class.equals(parameterTypes[index])) {
                 // lecture des annotations de ce paramètre
                Annotation[] annotations = method.getParameterAnnotations()[index] ;
                 for (Annotation annotation : annotations) {
                     // l'annotation StringNotNull existe-t-elle ?
                     if (StringNotNull.class.equals(annotation.annotationType())) {
                         // ici l'on regarde un paramètre de type String,
                         // annoté par StringNotNull
                        String stringParameter = (String)context.getParameters()[index] ;
                         if (stringParameter == null) {
                             // on a un paramètre de type String, annoté, et null
                             // on ne doit donc pas appeler la méthode
                            Object returnedObject = 
                                  getDefaultValue(method.getReturnType()) ;
                             return returnedObject ;
                        }  else {
                             // ici le paramètre est non nul, on continue donc
                             // à explorer les autres paramètres de la méthode
                        }
                    }
                }
            }
        }

         // si nous sommes arrivés ici, c'est qu'aucun paramètre de type String
         // annoté et nul n'a été trouvé, on peut donc appeler la méthode
         // interceptée
         return context.proceed() ;
    }
}

Comme on le voit, le code de cette classe est un peu complexe, et surtout s'appuie énormément sur le mécanisme de reflection qui permet de lire le contenu d'une méthode.

9.6.3. Interception de l'EJB

Il ne reste plus qu'à annoter notre EJB pour que toutes ces méthodes soient interceptées par cet intercepteur.

Exemple 78. Interception finale de l'EJB MarinServiceImpl

@Interceptors({
    StringNotNullInterceptor.class,
})
 @Stateless(mappedName="MarinService")
 @Remote(MarinService.class)
 public  class MarinServiceImpl  implements MarinService {
    
     // reste de la classe
}

JPA & EJB
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction
1. Objet du mapping objet / relationnel
2. Un peu d'histoire
Un premier exemple
1. Introduction
2. Création de l'environnement technique
2.1. Introduction
2.2. Création de la base Derby
2.3. Création du projet NetBeans et d'une première entité
2.4. Structure d'un projet persistant
2.5. Une première classe persistante
2.6. Un premier fichier persistence.xml
3. Utilisation de ce premier exemple
3.1. Écriture du code d'utilisation
3.2. Exécution de notre premier exemple
3.3. Modification de la class Marin
3.4. Opérations CRUD
Mettre un jeu de classes en base
1. Introduction
2. Définition d'une entité JPA
2.1. Écriture de l'entité
2.2. Annotation de l'entité
2.3. Annotations des champs
2.4. Exemple d'utilisation
3. Opérations sur les entités
3.1. Introduction
3.2. Opération PERSIST
3.3. Opération REMOVE
3.4. Opération REFRESH
3.5. Opération DETACH
3.6. Opération MERGE
4. Mise en relation d'entités
4.1. Introduction
4.2. Relations unidirectionnelles et bidirectionnelles
4.3. Relation 1:1
4.4. Relation 1:p
4.5. Relation p:1
4.6. Relation n:p
4.7. Comportement cascade
4.8. Effacement des entités orphelines
5. Charger des entités et leurs relations
6. Objets inclus
6.1. Introduction
6.2. Déclaration d'un objet inclus
6.3. Utilisation d'objets inclus
6.4. Cas où l'objet inclus est nul
6.5. Renommer les colonnes incluses
6.6. Collections d'objets inclus
L'API Collection en base
1. Introduction
2. Enregistrer une collection d'entités
2.1. Enregistrement d'une collection simple
2.2. Enregistrement d'un Set
2.3. Enregistrement d'une List
3. Enregistrer une collection de types de base
4. Enregistrement d'une Map
4.1. Table de hachage de type (type de base, entité)
4.2. Cas où la clé est un champ de la valeur
4.3. Cas d'une table (entité, entité)
Héritage
1. Introduction
2. Enregistrement d'une hiérarchie de classes
2.1. Entité et super-classe non enregistrée
2.2. Position du problème
2.3. Trois façons de faire
3. Stratégie SINGLE_TABLE
3.1. Fonctionnement
3.2. Mise en place
3.3. Limitations
4. Stratégie JOINED
4.1. Fonctionnement
4.2. Mise en place
4.3. Limitations
5. Stratégie TABLE_PER_CLASS
5.1. Fonctionnement
5.2. Mise en place
5.3. Limitations
Requêtes
1. Introduction
2. Un premier exemple
2.1. Écriture d'une première requête
2.2. Exécution d'une première requête
2.3. Exécution d'une première requête d'agrégation
3. Définition de requêtes
3.1. Requêtes dynamiques
3.2. Requêtes paramétrées
3.3. Requêtes nommées
3.4. Requêtes natives
4. Exécution, analyse du résultat
4.1. Exécution d'une requête dynamique
4.2. Exécution d'une requête nommée
4.3. Analyse du résultat
4.4. Cas des résultats de grande taille
4.5. Remarques
5. Clause From
5.1. Définition des entités
5.2. Jointures dans la clause From
5.3. Remarque finale sur les jointures en JPQL
6. Clause Where
6.1. Variables et chemins dans une clause where
6.2. Expressions conditionnelles et opérateurs
6.3. Requêtes imbriquées
6.4. Opérateurs any, all et some
6.5. Expressions fonctionnelles
7. Clauses Group By et Having
8. Opérations Update et Delete
EJB
1. Introduction
2. Un premier exemple
2.1. Introduction
2.2. Installation dans Glassfish à l'aide de Netbeans
2.3. Création d'un premier EJB
2.4. Déploiement de notre premier EJB
2.5. Création d'un client
3. Mise en oeuvre du pattern session facade
3.1. Introduction
3.2. Modèle objet
3.3. Définition de l'unité de persistance
3.4. Assemblage de notre application
3.5. Assemblage et déploiement
3.6. Utilisation du client
4. Opération de persistance en façade
4.1. Introduction
4.2. Enrichissement du service
4.3. Création de la méthode findMarinById(long)
4.4. Ajout de la méthode findAllMarins()
4.5. Utilisation dans un code client
5. Types d'EJB
5.1. Introduction
5.2. Qu'est-ce qu'un EJB ?
5.3. Écriture d'un EJB session
5.4. Qu'est-ce qu'une méthode métier ?
5.5. EJB avec ou sans état
5.6. Gestion des transactions
5.7. Restrictions
6. Cycle de vie d'un EJB
6.1. Cas des EJB sans état
6.2. Cas des EJB avec état
6.3. Injection de dépendances
7. Transaction gérée par l'EJB
7.1. Introduction
7.2. Déclaration du mode transactionnel
7.3. Gestion de la transaction
7.4. Fonctionnement de la transaction
7.5. Cas des EJB avec état
8. Transaction gérée par le serveur
8.1. Introduction
8.2. Déclaration du mode transactionnel
8.3. Gestion de la transaction
8.4. Fonctionnement de la transaction
8.5. Remarques
9. Intercepteurs
9.1. Introduction
9.2. Aperçu général
9.3. Cycle de vie d'un intercepteur
9.4. Object InvocationContext
9.5. Interception d'un EJB ou d'une méthode métier
9.6. Exemple de mise en œuvre d'un intercepteur