[JAVA] Punkt 39: Bevorzugen Sie Anmerkungen gegenüber Namensmustern

39. Wählen Sie eine Anmerkung aus dem Namensmuster

Tools und Frameworks haben ihre Behandlung je nach ** Namensmuster ** häufig geändert. In JUnit vor Version 4 muss der Name der Testmethode beispielsweise mit test beginnen. Diese Methode hat einige Nachteile. Wenn Sie einen Tippfehler machen, schlagen Sie implizit fehl. Wenn Sie beispielsweise tsetSafetyOverride anstelle von testSafetyOverride eingeben, schlägt JUnit3 nicht fehl, der Test wird jedoch nicht ausgeführt. Es kann auch nicht garantiert werden, dass das Namensmuster nur für die richtigen Programmelemente verwendet wird. Ein weiterer Nachteil ist, dass es keine gute Möglichkeit gibt, den Argumentwert dem Programmelement zuzuordnen.

Anmerkungen beseitigen diese Nachteile (Anmerkungen wurden seit JUnit 4 eingeführt). In diesem Kapitel erstellen wir ein einfaches Testframework, um zu sehen, wie Anmerkungen funktionieren. Schauen wir uns zunächst die Anmerkungen an, die automatisch funktionieren und fehlschlagen, wenn ein Fehler gesendet wird.

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

In der Definition der Testanmerkung werden @ Retention``` und @ Target``` hinzugefügt, diese werden jedoch als Meta-Annotationen bezeichnet.

@Retention(RetentionPolicy.RUNTIME)Gibt an, dass die Testanmerkung zur Laufzeit beibehalten wird.



#### **`@Target(ElementType.METHOD)Gibt an, dass die Testanmerkung nur in Methodendeklarationen gültig ist (gilt nicht für Felder oder Klassen).`**
```METHOD)Gibt an, dass die Testanmerkung nur in Methodendeklarationen gültig ist (gilt nicht für Felder oder Klassen).

 Der Kommentar in der Testanmerkungsdeklaration lautet "Nur für parameterlose statische Methoden verwenden". Dies kann jedoch vom Compiler nur erkannt werden, wenn Sie einen Anmerkungsprozessor schreiben. Weitere Informationen finden Sie in der Dokumentation zu javax.annotation.processing.
 Im Folgenden wird beschrieben, wie die Testanmerkung tatsächlich verwendet wird.

```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() { }
}

Testanmerkungen werden als ** Markierungsanmerkungen ** bezeichnet, da sie keine Argumente enthalten und das Element einfach markieren. Wenn Sie Test eingeben oder andere Testanmerkungen als Methodendeklarationen verwenden, wird ein Kompilierungsfehler angezeigt. sampleVon den Klassen ist m1 erfolgreich, m3 und m7 sind nicht erfolgreich, m5 ist ungültig, da es sich nicht um eine statische Methode handelt, und die anderen vier Methoden werden nicht mit der Testanmerkung versehen und vom Testframework ignoriert. Die Testanmerkung wirkt sich nicht direkt auf die Semantik der `` `Sample``` -Klasse aus, sondern gibt nur Informationen an Programme weiter, die die Testanmerkung verwenden. Das heißt, die Annotation wirkt sich nicht auf die Semantik des angegebenen Codes aus, sondern auf die Verarbeitung von Werkzeugen wie dem folgenden Beispieltestläufer.

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);
    }

}

Das Test-Runner-Tool verwendet den FQDN als Argument und führt die Methode mit der Test-Annotation aus, indem es `Method.invoke``` aufruft. Wenn die Testmethode eine Ausnahme auslöst, schließt die Reflection-Funktion die Ausnahme um und löst eine InvocationTargetException aus. Dieses Tool fängt diese Ausnahme ab und ruft die Ursachenausnahme mit der Methode getCause``` ab. Die zweite catch-Klausel fängt die Ausnahme aufgrund des Missbrauchs der Testanmerkung ab und druckt die entsprechende Nachricht aus. Die Ausgabe, wenn die `` Sample``` -Klasse auf das Testwerkzeug angewendet wird, ist wie folgt.

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

Jetzt erstellen wir eine neue Anmerkung, die erfolgreich ist, wenn eine bestimmte Ausnahme ausgelöst wird.

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();
}

Als Argument dieser Annotation wird ein begrenztes Token (Item33) mit dem Namen `` Class <? Extends Throwable> verwendet, und die Klasse, die `Throwable``` erbt, kann als Argument verwendet werden. Meint. Das Folgende sind spezifische Anwendungsbeispiele.

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)
}

Darüber hinaus wird das Testläufer-Tool wie folgt geändert.

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);
    }

}

Dieser Code extrahiert das Argument der Annotation `` `ExceptionTest``` und prüft, ob die aufgetretene Ausnahme vom gleichen Typ ist. Das Kompilieren des Testprogramms zeigt an, dass der als Argument der Anmerkung angegebene Ausnahmetyp gültig ist. Der Testläufer löst jedoch eine "TypeNotPresentException" aus, wenn der bestimmte Ausnahmetyp, der zur Kompilierungszeit war, nicht zur Laufzeit ist.

Wir haben den Code für diesen Ausnahmetest weiter verbessert, damit Sie testen können, ob eine der angegebenen Ausnahmen ausgelöst wird. Diese Verbesserung kann leicht erreicht werden, indem der Argumenttyp "ExceptionTest" zu einem Array gemacht wird.

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

Jetzt kann die ExceptionTest-Annotation entweder ein oder mehrere Argumente annehmen. Bei der Beschreibung mehrerer Ausnahmen ist dies wie folgt.

// 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);
}

exceptiontestMit der neuen Version sieht das Testrunner-Tool wie folgt aus.

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);
    }

}

In Java8 können Anmerkungen, die mehrere Argumente enthalten, auf unterschiedliche Weise implementiert werden. Anstatt eine Annotation mit einem Array-Argument zu deklarieren, verwenden Sie die Meta-Annotation `@ Repeatable```, damit ein Element mehrmals mit Annotationen versehen werden kann. Diese `@ Repeatable``` Meta-Annotation benötigt ein Argument. Das Argument ist ein Klassenobjekt mit einem Array namens Anotationstyp als Argument. Das Folgende ist ein Beispiel.

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();
}

Das Verfahren zum Hinzufügen von Anmerkungen ist wie folgt.

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

@repeatableBei der Verarbeitung ist Vorsicht geboten. Eine Anmerkung, die wiederholt verwendet wird, wird in Kombination mit einer Anmerkung erstellt, in der sie gespeichert ist. getannotationsbytypeDie Methode verbirgt dies und ermöglicht den Zugriff auf wiederholte und nicht wiederholte Anmerkungen. Auf der anderen SeiteisannotationspresentDie Methode bestimmt, dass die Anmerkung, die wiederholt verwendet wird, die Anmerkung ist, in der sie gespeichert ist. Das heißt, die Art der Anmerkung, die wiederholt verwendet wirdisannotationspresentDie Methode kann nicht feststellen, dass es sich um einen wiederkehrenden Typ handelt. Um sowohl wiederholt verwendete als auch nicht wiederholte Anmerkungen zu erkennen, müssen sowohl die gespeicherte Anmerkung als auch die Elementanmerkung überprüft werden. Das Folgende ist eine Neufassung, um der wiederholten Verwendung von Anmerkungen standzuhalten.

// 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);
    }
}

@repeatableSie sollten es verwenden, wenn Sie der Meinung sind, dass die Verwendung die Lesbarkeit verbessert. Beachten Sie jedoch, dass die Verwendung die Verarbeitung wiederholt verwendeter Anmerkungen erschwert.

Obwohl das Testframework in diesem Kapitel einfach ist, hat es einen Vorteil gegenüber der Benennung von Mustern mithilfe von Anmerkungen gezeigt.

Die meisten Programmierer definieren selbst keine Anmerkungen. Sie sollten Standard-Java-Annotationen oder Annotationen verwenden, die von IDEs und Tools bereitgestellt werden.

Recommended Posts

Punkt 39: Bevorzugen Sie Anmerkungen gegenüber Namensmustern
Punkt 28: Listen Arrays vorziehen
Punkt 65: Schnittstellen der Reflexion vorziehen
Punkt 43: Bevorzugen Sie Methodenverweise auf Lambdas
Punkt 42: Bevorzugen Sie Lambdas gegenüber anonymen Klassen
Punkt 85: Bevorzugen Sie Alternativen zur Java-Serialisierung
Punkt 68: Beachten Sie die allgemein anerkannten Namenskonventionen
Punkt 58: Bevorzugen Sie für jede Schleife herkömmliche Schleifen
Punkt 23: Bevorzugen Sie Klassenhierarchien gegenüber markierten Klassen
Punkt 61: Bevorzugen Sie primitive Typen gegenüber primitiven Boxen
Punkt 81: Bevorzugen Sie Parallelitätsdienstprogramme, um zu warten und zu benachrichtigen
Punkt 80: Ziehen Sie Executoren, Aufgaben und Streams Threads vor
Punkt 47: Sammlung als Rückgabetyp dem Stream vorziehen
Einführung in Entwurfsmuster (Einführung)
Einführung in Entwurfsmuster (Builder)
Einführung in Entwurfsmuster (Composite)
Einführung in Designmuster (Fliegengewicht)
Einführung in Entwurfsmuster Prototyp
Einführung in Entwurfsmuster (Iterator)
Einführung in Entwurfsmuster (Strategie)