[JAVA] Élément 39: Préférez les annotations aux modèles de dénomination

39. Choisissez une annotation dans le modèle de dénomination

Les outils et les frameworks ont souvent changé leur traitement en fonction du ** modèle de dénomination **. Par exemple, dans JUnit avant la version 4, le nom de la méthode de test doit commencer par test. Cette méthode présente quelques inconvénients. Si vous faites une faute de frappe, vous échouerez implicitement. Par exemple, si vous tapez tsetSafetyOverride au lieu de testSafetyOverride, JUnit3 n'échouera pas, mais le test ne s'exécutera pas. En outre, il n'existe aucun moyen de garantir que le modèle de dénomination est utilisé uniquement pour les éléments de programme appropriés. Un autre inconvénient est qu'il n'y a pas de bon moyen d'associer la valeur de l'argument à l'élément de programme.

Les annotations éliminent ces lacunes (des annotations ont été introduites depuis JUnit 4). Dans ce chapitre, nous allons créer un cadre de test simple pour voir comment fonctionnent les annotations. Tout d'abord, examinons les annotations qui fonctionnent automatiquement et échouent si une erreur est envoyée.

/**
 * Indicates that the annotated method is a test method.
 * Use only on parameterless static methods.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

Dans la définition de l'annotation Test, @ Retention '' et @ Target '' sont ajoutés, mais on les appelle des méta-annotations.

@Retention(RetentionPolicy.RUNTIME)Indique que l'annotation Test est conservée au moment de l'exécution.



#### **`@Target(ElementType.METHOD)Indique que l'annotation Test n'est valide que dans les déclarations de méthode (non applicable aux champs ou aux classes).`**
```METHOD)Indique que l'annotation Test n'est valide que dans les déclarations de méthode (non applicable aux champs ou aux classes).

 Le commentaire dans la déclaration d'annotation Test indique «Utiliser uniquement sur des méthodes statiques sans paramètre», mais cela ne peut pas être détecté par le compilateur à moins que vous n'écriviez un processeur d'annotations. Voir la documentation `` javax.annotation.processing '' pour plus d'informations.
 Vous trouverez ci-dessous comment l'annotation Test est réellement utilisée.

```java
// Program containing marker annotations
public class Sample {
    @Test public static void m1() { }  // Test should pass
    public static void m2() { }
    @Test public static void m3() {     // Test should fail
        throw new RuntimeException("Boom");
    }
    public static void m4() { }
    @Test public void m5() { } // INVALID USE: nonstatic method
    public static void m6() { }
    @Test public static void m7() {    // Test should fail
        throw new RuntimeException("Crash");
    }
    public static void m8() { }
}

Les annotations de test sont appelées ** annotations marqueurs ** car elles n'ont pas d'arguments et marquent simplement l'élément. Si vous tapez Test ou utilisez des annotations Test autres que des déclarations de méthode, vous obtiendrez une erreur de compilation. sampleParmi les classes, m1 réussit, m3 et m7 échouent, m5 n'est pas valide car il ne s'agit pas d'une méthode statique et les quatre autres méthodes ne sont pas annotées avec l'annotation de test et sont ignorées par le cadre de test. L'annotation Test n'affecte pas directement la sémantique de la classe `` Sample '', elle ne donne des informations qu'aux programmes qui utilisent l'annotation Test. Autrement dit, l'annotation n'affecte pas la sémantique du code donné, mais le traitement d'outils tels que l'exemple de testeur ci-dessous.

package tryAny.effectiveJava;

//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }

}

L'outil Test Runner prend le FQDN comme argument et exécute la méthode avec l'annotation Test en appelant Method.invoke```. Si la méthode de test lève une exception, la fonction de réflexion encapsule l'exception et lève une ```InvocationTargetException```. Cet outil intercepte cette exception et obtient l'exception de cause avec la méthode getCause ''. La deuxième clause catch intercepte l'exception en raison de l'abus de l'annotation Test et affiche le message approprié. La sortie lorsque la classe `` Sample '' est appliquée à l'outil de test est la suivante.

public static void tryAny.effectiveJava.Sample.m3() failed: java.lang.RuntimeException: Boom
Invalid @Test: public void tryAny.effectiveJava.Sample.m5()
public static void tryAny.effectiveJava.Sample.m7() failed: java.lang.RuntimeException: Crash
Passed: 1, Failed: 3

Créons maintenant une nouvelle annotation qui réussira si une exception particulière est levée.

package tryAny.effectiveJava;

//Annotation type with a parameter
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 *  * Indicates that the annotated method is a test method that  * must throw
 * the designated exception to succeed.  
 */

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

Un jeton borné (Item33) appelé Class <? Extends Throwable> est utilisé comme argument de cette annotation, et une classe qui hérite de `` Throwable '' peut être prise comme argument. Veux dire. Voici des exemples spécifiques d'utilisation.

package tryAny.effectiveJava;

//Program containing annotations with a parameter
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() { // Test should pass
        int i = 0;

        i = i / i;
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m2() { // Should fail (wrong exception)
        int[] a = new int[0];
        int i = a[1];
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m3() {
    } // Should fail (no exception)
}

De plus, l'outil d'exécution de test sera modifié comme suit pour correspondre à cela.

package tryAny.effectiveJava;

//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }

            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (InvocationTargetException wrappedEx) {
                    Throwable exc = wrappedEx.getCause();
                    Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
                    if (excType.isInstance(exc)) {
                        passed++;
                    } else {
                        System.out.printf("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc);
                    }
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }

        }

        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }

}

Ce code extrait l'argument de l'annotation ExceptionTest et vérifie si l'exception levée est du même type. Le fait que le programme de test soit compilé indique que le type d'exception spécifié comme argument de l'annotation est valide. Cependant, le testeur lance une `` TypeNotPresentException '' si le type d'exception particulier qui était au moment de la compilation n'est pas au moment de l'exécution.

Nous avons encore amélioré le code de ce test d'exception pour vous permettre de tester si l'une des exceptions spécifiées est levée. Cette amélioration peut être facilement effectuée en faisant du type d'argument ExceptionTest un tableau.

// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}

Désormais, l'annotation ExceptionTest peut prendre un ou plusieurs arguments. Lors de la description de plusieurs exceptions, c'est comme suit.

// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
                 NullPointerException.class })
public static void doublyBad() {
    List<String> list = new ArrayList<>();
 // The spec permits this method to throw either
    // IndexOutOfBoundsException or NullPointerException
    list.addAll(5, null);
}

exceptiontestAvec la nouvelle version, l'outil d'exécution de test est le suivant.

package tryAny.effectiveJava;

//Program to process marker annotations
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }

            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (InvocationTargetException wrappedEx) {
                    Throwable exc = wrappedEx.getCause();
                    int oldPasses = passed;
                    Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
                    for (Class<? extends Throwable> excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPasses) {
                        System.out.printf("Test %s failed: %s%n", m, exc);
                    }
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }

        }

        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }

}

Dans Java8, les annotations qui acceptent plusieurs arguments peuvent être implémentées de différentes manières. Au lieu de déclarer une annotation avec un argument de tableau, utilisez la méta-annotation @ Repeatable '' pour permettre à un élément d'être annoté plusieurs fois. Cette méta-annotation @ Répétable '' prend un argument. Son argument est un objet de classe qui a un tableau appelé contenant un type de rotation comme argument. Ce qui suit est un exemple.

package tryAny.effectiveJava;

//Annotation type with a parameter
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 *  * Indicates that the annotated method is a test method that  * must throw
 * the designated exception to succeed.  
 */

// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Exception> value();

}
package tryAny.effectiveJava;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

La méthode d'ajout d'annotations est la suivante.

// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }

@repeatableDes précautions doivent être prises lors du traitement. Une annotation qui est utilisée à plusieurs reprises est créée en combinaison avec une annotation qui la stocke. getannotationsbytypeLa méthode masque cela et donne accès aux annotations répétées et non répétées. D'autre partisannotationspresentLa méthode détermine que l'annotation qui est utilisée à plusieurs reprises est l'annotation qui la stocke. Autrement dit, le type d'annotation qui est utilisé à plusieurs reprisesisannotationspresentLa méthode ne peut pas déterminer qu'il s'agit d'un type récurrent. Afin de détecter à la fois les annotations utilisées à plusieurs reprises et les annotations non répétées, il est nécessaire de vérifier à la fois l'annotation stockée et l'annotation d'élément. Ce qui suit est une réécriture pour résister à l'utilisation répétée d'annotations.

// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
    tests++;
    try {
        m.invoke(null);
        System.out.printf("Test %s failed: no exception%n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class);
        for (ExceptionTest excTest : excTests) {
            if (excTest.value().isInstance(exc)) {
                passed++;
                break;
            }
        }
        if (passed == oldPassed)
            System.out.printf("Test %s failed: %s %n", m, exc);
    }
}

@repeatableVous devez l'utiliser si vous pensez que son utilisation améliore la lisibilité, mais vous devez garder à l'esprit que son utilisation complique le traitement des annotations utilisées à plusieurs reprises.

Bien que le cadre de test de ce chapitre soit simple, il a montré un avantage par rapport aux modèles de dénomination à l'aide d'annotations.

La plupart des programmeurs ne définissent pas eux-mêmes les annotations. Vous devez utiliser des annotations Java standard ou des annotations fournies par les IDE et les outils.

Recommended Posts

Élément 39: Préférez les annotations aux modèles de dénomination
Élément 28: Préférer les listes aux tableaux
Rubrique 65: Préférez les interfaces à la réflexion
Rubrique 43: Préférez les références de méthode aux lambdas
Point 42: Préférez les lambdas aux classes anonymes
Point 85: Préférez les alternatives à la sérialisation Java
Point 68: Adhérer aux conventions de dénomination généralement acceptées
Point 58: Préférez les boucles for-each aux boucles for traditionnelles
Élément 23: Préférez les hiérarchies de classes aux classes balisées
Point 61: Préférez les types primitifs aux primitives encadrées
Élément 81: Préférez les utilitaires de concurrence pour attendre et notifier
Élément 80: Préférez les exécuteurs, les tâches et les flux aux threads
Élément 47: Préférez la collecte au flux comme type de retour
Introduction aux modèles de conception (introduction)
Introduction aux modèles de conception (Builder)
Introduction aux modèles de conception (composite)
Introduction aux modèles de conception (poids mouche)
Introduction au prototype de modèles de conception
Introduction aux modèles de conception (Iterator)
Introduction aux modèles de conception (stratégie)