1. ホーム
  2. Java

Javaの基本 - アノテーションの仕組みの説明

2022-02-27 08:19:20

アノテーションの基本

アノテーションとは、JDKバージョン1.5で導入された、パッケージ、クラス、インターフェース、フィールド、メソッドパラメータ、ローカル変数などに注釈を付けることができるコード記述のための機能です。主に4つの目的で使用されます。

  • ドキュメントを生成する。コードで特定されたメタデータから javadoc ドキュメントを生成する。
  • コンパイル時に、コードで特定したメタデータでコンパイラが検証を行う「コンパイルチェック」。
  • コンパイル時に動的な処理を行う。コードで特定されたメタデータは、動的なコード生成など、コンパイル時に動的に処理される。
  • 実行時の動的処理。コード内で特定されたメタデータが実行時に動的に処理される。例えば、インスタンスを注入するためにリフレクションを使用する場合など。

これはかなり抽象的なので、アノテーションの一般的なカテゴリを見てみましょう。

  • Javaには、標準的なアノテーションがあります。 は、@Override、@Deprecated、@SuppressWarnings が含まれ、それぞれメソッドをオーバーライドする、クラスやメソッドを非推奨とする、警告を無視するといったマーク付けに使用され、これらのアノテーションでマークされるとコンパイラーによってチェックされます。
  • メタアノテーション。 メタアノテーションとは、アノテーションを定義するためのアノテーションで、@Retention、@Target、@Inherited、@Documentedがあり、@Retentionはアノテーションの予約段階、@Targetはアノテーションの使用範囲、@Inheritedはアノテーションが継承できること、@ Documentedはjavadocドキュメントを生成するかどうかを表すのに使用されるアノテーションです。
  • カスタムアノテーションです。 ニーズに応じてアノテーションを定義し、カスタムアノテーションにメタアノテーションを付与することができます。

次に、このカテゴリカルなレンズを通してアノテーションを理解しましょう。

Java組み込みアノテーション

まず、最も一般的なJavaの組み込みアノテーションについて、次のコードを見てみましょう。

class A{
    public void test() {
        
    }
}

class B extends A{

    /**
        * Override the test method of the parent class
        */
    @Override
    public void test() {
    }

    /**
        * Deprecated methods
        */
    @Deprecated
    public void oldMethod() {
    }

    /**
        * Ignore alerts
        * 
        * @return
        */
    @SuppressWarnings("rawtypes")
    public List processList() {
        List list = new ArrayList();
        return list;
    }
}


Java 1.5以降に付属する標準的なアノテーションで、@Override、@Deprecated、@SuppressWarningsがあります。

  • オーバーライド:現在のメソッド定義が親クラスのメソッドをオーバーライドすることを示します。
  • Deprecated: 非推奨であることを示し、@Deprecated のアノテーションが付いたコードを使用するとコンパイラーは警告します。
  • SuppressWarnings : コンパイラの警告をオフにすることを示します。

これらの組み込みアノテーションの中で、メタアノテーションの定義を通じてメタアノテーションを引き出しながら、より具体的に見ていきましょう。

組み込みアノテーション - @Override

まず、このアノテーションタイプの定義から見ていきましょう。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}


その定義からわかるように、このアノテーションはメソッドを修正するために使用され、コンパイル時にのみ有効で、コンパイル後はクラスファイルには存在しなくなります。このアノテーションの目的は、変更されるメソッドが親クラスにある同じシグネチャのメソッドのオーバーライドであることをコンパイラに伝えることであり、コンパイラはこれをチェックして、メソッドが親クラスに存在しないか別のシグネチャで存在するとわかったらエラーを報告することです。

組み込みアノテーション - @Deprecated

このアノテーションは以下のように定義されています。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}


その定義からわかるように、実行時に保存されるように文書化されており、コンストラクタ、プロパティ、ローカル変数、メソッド、パッケージ、パラメータ、型を変更することができます。このアノテーションの目的は、変更されたプログラム要素が "非推奨" であることをコンパイラに伝えることであり、ユーザーによる使用はもはや推奨されません。

組み込みアノテーション - @SuppressWarnings

このアノテーションもよく使われるものなので、その定義を見てみましょう。

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}


修正できるプログラム要素には、型、プロパティ、メソッド、パラメータ、コンストラクタ、ローカル変数があり、String[]の値を取るときのみソースコード上で存続できます。その目的は、指定された警告メッセージを無視するようにコンパイラに指示することであり、取ることのできる値は以下の通りです。

<テーブル パラメータ 役割 原文の説明 すべて すべての警告を抑制する すべての警告を表示しないようにするには ボクシング ボックス化およびアンボックス化操作時の警告を抑制します。 箱詰め/箱出しの操作に関連する警告を抑制するために キャスト マッピング関連の警告を抑制する キャスト操作に関連する警告を抑制する デップアン アノテーションが有効な警告を抑制する 非推奨のアノテーションに関連する警告を表示しないようにします。 衰弱 期限切れのメソッドの警告を抑制する 非推奨に関連する警告を抑制するために フォールスルー スイッチの切れ目を見逃す警告を抑制する switch 文の改行がないことに関連する警告を抑制します。 ついに finallyモジュールで返されない警告を抑制します。 を返さない finally ブロックに関連する警告を抑制します。 隠蔽 隠し変数の領域変数に関連する警告を抑制する 変数()を隠しているローカルに関連する警告を抑制するために 未完成スイッチ 完全でない switch 文は無視する switch ステートメントに含まれない項目に関する警告を抑制する (enum の場合) nls nls以外のフォーマット文字を無視する nls 以外の文字列リテラルに関連する警告を抑制します。 ヌル nullに対する操作を無視する NULL解析に関連する警告を抑制する ロータイプ ジェネリックを使用する場合、対応する型がないことを無視する を使用する場合、指定されていない型に関連する警告を抑制することができます。 制限 推奨しない、または禁止されているリファレンスの使用に関連する警告を抑制する の使用に関する警告を抑制します。 シリアル serialVersionUID変数がシリアライズ可能なクラスで宣言されていないことは無視する。 シリアライザブルクラスのserialVersionUIDフィールドの欠落に関連する警告を抑制するために 静的アクセス 不正な静的アクセス方法の警告を抑制する 不正な静的アクセスに関する警告を抑制します。 合成アクセス 最適な方法で内部クラスにアクセスしないサブクラスに関する警告を抑制します。 内部クラスからの最適化されていないアクセスに関する警告を抑制するために チェックなし 型チェック操作が行われなかった場合の警告を抑制する を使用すると、チェックされていない操作に関する警告を抑制することができます。 unqualified-field-access アクセス権限のないドメインに対する警告を抑制する フィールドアクセスに関連する警告を抑制するために、未修飾の 未使用 未使用のコードに対する警告を抑制する 未使用のコードに関連する警告を抑制するために

メタ・アノテーション

上記の組み込みアノテーションの定義では、いくつかのメタアノテーション(アノテーションタイプによってアノテーションされたアノテーションクラス)を使用しています。JDK 1.5では、@Target、@Retention、@Documented、@Inheritedの4つの標準メタアノテーションが、JDK 1.8では@Repeatableと@Nativeの2つのメタアノテーションが提供されています ...

メタアノテーション - @Target

Targetアノテーションの目的は、アノテーションの使用範囲(変更したアノテーションを使用できる場所)を記述することである。

Targetアノテーションは、それがアノテーションするクラスによって修正できるオブジェクトの範囲を記述するために使用されます。アノテーションは、パッケージ、型(クラス、インターフェース、列挙、アノテーションされたクラス)、クラスメンバー(メソッド、コンストラクター、メンバー変数、列挙値)、メソッドパラメーター、ローカル変数(ループ変数、キャッチパラメーターなど)を修正するために使用でき、@ Targetを使用すると、どのオブジェクトが修正に使用できるかが明確になり、その値の範囲はElementType列挙で定義されています。

public enum ElementType {
 
    TYPE, // class, interface, enumeration class
 
    FIELD, // member variables (including: enumeration constants)
 
    METHOD, // member method
 
    PARAMETER, // method parameter
 
    CONSTRUCTOR, // Construct method
 
    LOCAL_VARIABLE, // local variable
 
    ANNOTATION_TYPE, // annotation class
 
    PACKAGE, // can be used to modify: package
 
    TYPE_PARAMETER, // type parameter, new in JDK 1.8
 
    TYPE_USE // anywhere the type is used, new in JDK 1.8
 
}


メタアノテーション - @Retention & @RetentionTarget

Retentionアノテーションが行うことは、アノテーションが保持される時間範囲(つまり、記述されたアノテーションが修正するクラスで保持できる時間)を記述することです。

Retentionアノテーションは、それがアノテーションしたクラスが他のクラスにアノテーションされている限り、どのアノテーションを保持できるかを修飾するために使用されます。全部で3つのポリシーがあり、RetentionPolicy列挙で定義されます。

public enum RetentionPolicy {
 
    SOURCE, // source file retention
    CLASS, // compile-time retention, default value
    RUNTIME // run-time retention, can be reflected to get annotation information
}


3つの戦略を適用するアノテーションクラスの違いを検証するために、各戦略に1つずつアノテーションクラスを定義してテストします。

@Retention(RetentionPolicy.SOURCE)
public @interface SourcePolicy {
 
}
@Retention(RetentionPolicy.CLASS)
public @interface ClassPolicy {
 
}
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimePolicy {
 
}


定義された3つのアノテーションのそれぞれでメソッドにアノテーションを付けます。

public class RetentionTest {
 
	@SourcePolicy
	public void sourcePolicy() {
	}
 
	@ClassPolicy
	public void classPolicy() {
	}
 
	@RuntimePolicy
	public void runtimePolicy() {
	}
}


javap -verbose RetentionTestコマンドを実行して得られるRetentionTestのクラスバイトコードは、次のようになります。

{
  public retention.RetentionTest();
    flags: ACC_PUBLIC
    code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public void sourcePolicy();
    flags: ACC_PUBLIC
    flags: ACC_PUBLIC
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0

  public void classPolicy();
    flags: ACC_PUBLIC
    flags: ACC_PUBLIC
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 11: 0
    RuntimeInvisibleAnnotations:
      0: #11()

  public void runtimePolicy();
    flags: ACC_PUBLIC
    code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 15: 0
    RuntimeVisibleAnnotations:
      0: #14()
}


RetentionTestのバイトコードの内容から、2つのことを結論付けることができます。

  • コンパイラは、sourcePolicy()メソッドのアノテーション情報を記録しない。
  • コンパイラーは、RuntimeInvisibleAnnotations属性とRuntimeVisibleAnnotations属性を使用して、それぞれclassPolicy()メソッドとruntimePolicy()メソッドに対するアノテーションを記録する。

メタアノテーション - @Documented

Documented アノテーションの目的は、javadoc ツールを使用してクラスのヘルプ・ドキュメントを生成する際に、そのアノテーション情報を保持するかどうかを記述することです。

次のコードは、Javadoc ツールを使用する際に @TestDocAnnotation アノテーション情報を生成します。

import java.lang.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation;
 
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface TestDocAnnotation {
 
	public String value() default "default";
}

@TestDocAnnotation("myMethodDoc")
public void testDoc() {

}


@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface TestInheritedAnnotation {
    String [] values();
    int number();
}


メタアノテーション - @Inherited

Inheritedアノテーションが行うこと。このアノテーションによって変更されたアノテーションは、継承性を持つようになります。あるクラスが@Inheritedによって変更されたアノテーションを使用すると、そのサブクラスは自動的にそのアノテーションを持つようになります。

アノテーションをテストしてみましょう。

  • Inheritedアノテーションを定義します。
@TestInheritedAnnotation(values = {"value"}, number = 10)
public class Person {
}

class Student extends Person{
	@Test
    public void test(){
        Class clazz = Student.class;
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation.toString());
        }
    }
}


  • このアノテーションを使用する
xxxxxxx.TestInheritedAnnotation(values=[value], number=10)


  • 出力
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)

Studentクラスは表示上は@TestInheritedAnnotationのアノテーションがなくても、親クラスのPersonにはアノテーションがあり、@TestInheritedAnnotationには@Inheritedのアノテーションがあるので、Studentクラスは自動的にそのアノテーションを持っているのです。

メタアノテーション - @Repeatable (Java8)

Repeatableについては、以下を参照してください。 Java 8 - 繰り返し可能なアノテーション

メタ・アノテーション - @Native (Java8)

Nativeアノテーションを使用してメンバ変数を変更すると、その変数がネイティブ コードから参照できることを意味し、コード生成ツールでよく使用されます。Nativeアノテーションはあまり使われず、理解されています。

アノテーションとリフレクションインターフェース

アノテーションを定義した後、どのようにアノテーションの内容を取得するのでしょうか。Reflectionパッケージjava.lang.reflectの下にあるAnnotatedElementインターフェイスはこれらのメソッドを提供します。ここで注意してください。アノテーションがRUNTIMEとして定義された後でのみ、アノテーションは実行時に見ることができ、クラスファイルに保存されたアノテーションは、クラスファイルがロードされたときに仮想マシンによって読み取られます。

AnnotatedElementインターフェースは、すべてのプログラム要素(クラス、メソッド、コンストラクタ)の親インターフェースであり、プログラムがリフレクションによってクラスのAnnotatedElementオブジェクトを取得すると、プログラムはそのオブジェクトのメソッドを呼び出してAnnotation情報にアクセスできるようになる。具体的なファーストオフインタフェースを見てみましょう。

  • <T extends Annotation> T getAnnotation(Class<T> annotationClass)

このプログラム要素が指定されたタイプのアノテーションを含むかどうかを判断し、存在すればtrueを、そうでなければfalseを返す。 注:このメソッドは、アノテーションに対応するアノテーションコンテナを無視する。

  • Annotation[] getAnnotations()

このプログラム要素に存在する指定された型のアノテーション、 またはその型のアノテーションが存在しない場合は null を返します。

  • <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)

このプログラム要素に存在するすべてのアノテーション、 またはアノテーションが存在しない場合は長さ0の配列を返します。

  • <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)

このプログラム要素に存在する、指定された型のアノテーションの配列を返します。該当する型のアノテーションが存在しない場合は、長さ0の配列を返す。このメソッドの呼び出し元は、他の呼び出し元から返された配列に影響を与えることなく、返された配列を自由に変更できます。 getAnnotationsByTypeメソッドは、getAnnotationと異なり、アノテーションに対応する重複するアノテーションのコンテナを検出する。プログラム要素がクラスで、アノテーションが現在のクラスで見つからず、アノテーションが継承可能である場合、親クラスに移動して対応するアノテーションを検出します。

  • <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)

この要素に直接存在するすべてのアノテーションを返します。このインターフェースの他のメソッドと異なり、このメソッドは継承されたアノテーションを無視します。この要素に直接存在するアノテーションがない場合、null を返します。

  • Annotation[] getDeclaredAnnotations()

この要素に直接存在するすべてのアノテーションを返します。このインターフェイスの他のメソッドと異なり、このメソッドは継承されたアノテーションを無視します。

  • package com.pdai.java.annotation; import java.lang.annotation.ElementType; import java.lang.annotation; RetentionPolicy; import java.lang.annotation; RetentionPolicy; import java.lang.annotation; @Target(ElementType.) @Retention(RetentionPolicy.RUNTIME) public @interface MyMethodAnnotation { public String title() default ""; public String description() default ""; }

この要素に直接存在するすべてのアノテーションと、 そのアノテーションに対応する重複したアノテーションコンテナを返します。このインターフェースの他のメソッドと異なり、このメソッドは継承されたアノテーションを無視します。この要素に直接存在するアノテーションがない場合は、長さ0の配列が返される。このメソッドの呼び出し元は、他の呼び出し元から返される配列に影響を与えることなく、返される配列を自由に変更することができます。

カスタムアノテーション

組み込みアノテーション、メタアノテーション、そしてアノテーションを取得するためのリフレクションインターフェースを理解したら、アノテーションのカスタマイズを始めましょう。この例では、上記のすべてを組み込み、コードもシンプルにしています。

  • 独自のアノテーションを定義する
package com.pdai.java.annotation;

import java.io.FileNotFoundException;
import java.lang.annotation;
import java.lang.reflect;
import java.util.ArrayList;
import java.util.List;

public class TestMethodAnnotation {

    @Override
    @MyMethodAnnotation(title = "toStringMethod", description = "override toString method")
    public String toString() {
        return "Override toString method";
    }

    @Deprecated
    @MyMethodAnnotation(title = "old static method", description = "deprecated old static method")
    public static void oldMethod() {
        System.out.println("old method, don't use it.");
    }

    @SuppressWarnings({"unchecked", "deprecation"})
    @MyMethodAnnotation(title = "test method", description = "suppress warning static method")
    public static void genericsTest() throws FileNotFoundException {
        List l = new ArrayList();
        l.add("abc");
        oldMethod();
    }
}



  • アノテーションの使用
public static void main(String[] args) {
    try {
        // Get all methods
        Method[] methods = TestMethodAnnotation.class.getClassLoader()
                .loadClass(("com.pdai.java.annotation.TestMethodAnnotation"))
                .getMethods();

        // Iterate through
        for (Method method : methods) {
            // whether the method has a MyMethodAnnotation annotation on it
            if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
                try {
                    // Get and iterate over all annotations on the method
                    for (Annotation anno : method.getDeclaredAnnotations()) {
                        System.out.println("Annotation in Method '"
                                + method + "' : " + anno);
                    }

                    // Get the MyMethodAnnotation object information
                    MyMethodAnnotation methodAnno = method
                            .getAnnotation(MyMethodAnnotation.class);

                    System.out.println(methodAnno.title());

                } catch (Throwable ex) {
                    ex.printStackTrace();
                }
            }
        }
    } catch (SecurityException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}


  • リフレクションインターフェースでアノテーション情報を取得する

テスト用のTestMethodAnnotationにMainメソッドを追加します。

Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.oldMethod()' : @java.lang.
Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.oldMethod()' : @com.pdai.java.annotation. MyMethodAnnotation(title=old static method, description=deprecated old static method)
old static method
Annotation in Method 'public static void com.pdai.java.annotation.TestMethodAnnotation.genericsTest() throws java.io.FileNotFoundException ' : @com.pdai.java.annotation.MyMethodAnnotation(title=test method, description=suppress warning static method)
test method
Annotation in Method 'public java.lang.String com.pdai.java.annotation.TestMethodAnnotation.toString()' : @com.pdai.java.annotation. MyMethodAnnotation(title=toStringMethod, description=override toString method)
toStringMethod


  • テストの出力
@Repeatable

アノテーションを深く理解する

次に、アノテーションを他の視点から深く見ていきます。

Java 8 では、どのような新しいアノテーションが提供されるのでしょうか。

  • ElementType.TYPE_USE

をご参照ください。 Java 8 - 繰り返しアノテーション

  • ElementType.TYPE_PARAMETER

をご参照ください。 Java 8 - 型アノテーション

  • // Customize the ElementType.TYPE_PARAMETER annotation @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_PARAMETER) public @interface MyNotEmpty { } // Customize the ElementType.TYPE_USE annotation @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_USE) public @interface MyNotNull { } // test class public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{ // use TYPE_PARAMETER type, it will not compile // public @MyNotEmpty T test(@MyNotEmpty T a){ // new ArrayList<@MyNotEmpty String>(); // return a; // } // use TYPE_USE type, compile through public @MyNotNull T test2(@MyNotNull T a){ new ArrayList<@MyNotNull String>(); return a; } }

TYPE_USE(この型は型宣言と型パラメーターの宣言を含み、設計者による型チェックを容易にすることを目的としている)にはElementType.TYPE(クラス、インターフェース(注釈付き型を含む)、列挙型の宣言)およびElementType.TYPE(型パラメーターの宣言を含み、設計者による型チェックを容易にすることを目的としている)が含まれる。別の例を見てみましょう。

public class HelloWorld {
 	
 	public void sayHello(){
 		System.out.println("hello.... ");
 		throw new NumberFormatException();
 	}
 	
 	public void sayWorld(){
 		System.out.println("world.... ");
 	}
 	
 	public String say(){
 		return "hello world!";
 	}
 	
}



アノテーションは継承に対応していますか?

アノテーションは継承をサポートしない

extendsというキーワードで@interfaceから継承することはできませんが、アノテーションはコンパイルされ、コンパイラが自動的にjava.lang.annotationから継承するようになります。

デコンパイルすると、アノテーションはAnnotationインターフェースを継承していることがわかりますが、Javaのインターフェースは多重継承を実装できるにもかかわらず、アノテーションの定義時にExtendsキーワードを使って@interfaceから継承することができないことを覚えておいてください。

アノテーションの継承とは異なり、アノテーションを持つクラスのサブクラスは@Inheritedで親アノテーションを継承します。@Inheritedで変更したアノテーションをクラスが使用すると、そのサブクラスは自動的にそのアノテーションを持つことになります。

アノテーションの実装はどのように行われるのですか?

参考

アノテーションの活用シーン

最後に、実際の開発現場におけるアノテーションのシナリオをいくつか見てみましょう。

設定からアノテーションへ - フレームワークの進化

Spring Frameworkのコンフィギュレーションからアノテーションへの移行。

継承された実装からアノテーションされた実装へ - Junit3からJunit4へ

モジュールのパッケージングは、継承や結合といったパターンで実装する人が多いと思いますが、アノテーションを取り入れることで実装のエレガントさが大きく向上します(カップリングも減ります)。そして、Junit3からJunit4への進化は、その好例と言えるでしょう。

  • テストされるクラス
public class HelloWorldTest extends TestCase{
 	private HelloWorld hw;
 	
 	@Override
 	protected void setUp() throws Exception {
 		super.setUp();
 		hw=new HelloWorld();
 	}
 	
 	/ / 1. Test no return value
 	public void testHello(){
 		try {
 			hw.sayHello();
 		} catch (Exception e) {
 			System.out.println("Exception occurred ..... ");
 		}
 		
 	}
 	public void testWorld(){
 		hw.sayWorld();
 	}
 	// 2. Test methods with return values
 	// return string
 	public void testSay(){
 		assertEquals("testFailed", hw.say(), "hello world!");
 	}
 	// return object
 	public void testObj(){
 		assertNull("Test object is not null", null);
 		assertNotNull("Test object is empty",new String());
 	}
 	@Override
 	protected void tearDown() throws Exception {
 		super.tearDown();
 		hw=null;
 	}	
}


  • UTのJunit 3実装

TestCaseを継承し、初期化は親クラスのメソッドをOverrideし、テストはtestのメソッドをprefixして取得することで実装されています。

public class HelloWorldTest {
 	private HelloWorld hw;
 
 	@Before
 	public void setUp() {
 		hw = new HelloWorld();
 	}
 
 	@Test(expected=NumberFormatException.class)
 	// 1. test no return value, different from the use of junit3, more convenient
 	public void testHello() {
 		hw.sayHello();
 	}
 	@Test
 	public void testWorld() {
 		hw.sayWorld();
 	}
 	
 	@Test
 	// 2. Test methods with return values
 	// return string
 	public void testSay() {
 		assertEquals("testFailed", hw.say(), "hello world!");
 	}
 	
 	@Test
 	// return object
 	public void testObj() {
 		assertNull("Test object is not null", null);
 		assertNotNull("Test object is empty", new String());
 	}
 
 	@After
 	public void tearDown() throws Exception {
 		hw = null;
 	}
 
}


  • Junit 4 は UT を実装しています。

これは、@Before、@Test、@Afterなどのアノテーションを定義することで実現されます。

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * Module 
     */
    public String title() default "";

    /**
     * Function
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * The operator category
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * Whether to save the requested parameters
     */
    public boolean isSaveRequestData() default true;
}


ここでは、アノテーションを付けることで、よりエレガントな方法でユニットテストを実装していることがわかります。それでもまだ、Junit4がどのように実行されるように実装されているかを学びたい場合はどうすればよいでしょうか?この記事を読んでみてください。 JUnit4のソースコード解析で仕組みがわかる

カスタムアノテーションとAOP - 接線によるデカップリング

最も一般的な実装は、Spring AOPのタンジェントである 操作ログの一元管理 アノテーションによってどのようにデカップリングが実現されるのか、オープンソースプロジェクトからの例(メインコードのみ表示)をここに紹介します。

  • カスタムログアノテーション
@Aspect
@Component
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    /**
     * Configure weave-in point - package path for custom annotations
     * 
     */
    @Pointcut("@annotation(com.xxx.aspectj.lang.annotation.Log)")
    public void logPointCut() {
    }

    /**
     * Execute after processing the request
     *
     * @param joinPoint cut point
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * Intercept exception operation
     * 
     * @param joinPoint tangent point
     * @param e Exception
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // Get the annotation
            Log controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null) {
                return;
            }

            // Get the current user
            User currentUser = ShiroUtils.getSysUser();

            // *======== database log =========*//
            OperLog operLog = new OperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // The address of the request
            String ip = ShiroUtils.getIp();
            operLog.setOperIp(ip);
            // return parameters
            operLog.setJsonResult(JSONObject.toJSONString(jsonResult));

            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
            if (currentUser ! = null) {
                operLog.setOperName(currentUser.getLoginName());
                if (StringUtils.isNotNull(currentUser.getDept())
                        && StringUtils.isNotEmpty(currentUser.getDept().getDeptName())) {
                    operLog.setDeptName(currentUser.getDept().getDeptName());
                }
            }

            if (e ! = null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // Set the method name
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + ". " + methodName + "() ");
            // Set the request method
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // Handle setting the parameters on the annotation
            getControllerMethodDescription(controllerLog, operLog);
            // Save the database
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        } catch (Exception exp) {
            // Log local exception log
            log.error("==Pre-notification exception==");
            log.error("Exception message:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * Get information about the description of the method in the annotation Used for Controller level annotations
     * 
     * @param log 日log
     * @param operLog operation log
     * @throws Exception
     */
    public void getControllerMethodDescription(Log log, OperLog operLog) throws Exception {
        // Set the action action
        operLog.setBusinessType(log.businessType().ordinal());
        // Set the title
        operLog.setTitle(log.title());
        // Set the operator category
        operLog.setOperatorType(log.operatorType().ordinal());
        // whether to save the request, parameters and values
        if (log.isSaveRequestData()) {
            //

  • ロギングタンジェントを実装し、カスタムアノテーションされたログをタンジェントとしてインターセプトする。

すなわち、@Logでアノテーションされたメソッドに対するカットポイントのインターセプトです。

@Controller
@RequestMapping("/system/dept")
public class DeptController extends BaseController {
    private String prefix = "system/dept";

    @Autowired
    private IDeptService deptService;
    
    /**
     * Add a new save department
     */
    @Log(title = "Department Management", businessType = BusinessType.INSERT)
    @RequiresPermissions("system:dept:add")
    @PostMapping("/add")
    @ResponseBody
    public AjaxResult addSave(@Validated Dept dept) {
        if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
            return error("Add department'" + dept.getDeptName() + "'Failed, department name already exists");
        }
        return toAjax(deptService.insertDept(dept));
    }

    /**
     * Save
     */
    @Log(title = "Department Management", businessType = BusinessType.UPDATE)
    @RequiresPermissions("system:dept:edit")
    @PostMapping("/edit")
    @ResponseBody
    public AjaxResult editSave(@Validated Dept dept) {
        if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
            return error("Modify department'" + dept.getDeptName() + "'Failed, department name already exists");
        } else if(dept.getParentId().equals(dept.getDeptId())) {
            return error("ModifyDepartment'" + dept.getDeptName() + "'Failed, parent department cannot be self");
        }
        return toAjax(deptService.updateDept(dept));
    }

    /**
     * Delete
     */
    @Log(title = "Department Management", businessType = BusinessType.DELETE)
    @RequiresPermissions("system:dept:remove")
    @GetMapping("/remove/{deptId}")
    @ResponseBody
    public AjaxResult remove(@PathVariable("deptId") Long deptId) {
        if (deptService.selectDeptCount(deptId) > 0) {
            return AjaxResult.warn("subordinate department exists,not allowed to delete");
        }
        if (deptService.checkDeptExistUser(deptId)) {
            return AjaxResult.warn("Department exists user, not allowed to delete");
        }
        return toAjax(deptService.deleteDeptById(deptId));
    }

  // ...
}


  • Logアノテーションの使用

単純なCRUD操作の例として、コードの一部を紹介します。"department"に対して操作が行われるたびに、その操作のログが生成されてデータベースに格納されます。

@Controller
@RequestMapping("/system/dept")
public class DeptController extends BaseController {
    private String prefix = "system/dept";

    @Autowired
    private IDeptService deptService;
    
    /**
     * Add a new save department
     */
    @Log(title = "Department Management", businessType = BusinessType.INSERT)
    @RequiresPermissions("system:dept:add")
    @PostMapping("/add")
    @ResponseBody
    public AjaxResult addSave(@Validated Dept dept) {
        if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
            return error("Add department'" + dept.getDeptName() + "'Failed, department name already exists");
        }
        return toAjax(deptService.insertDept(dept));
    }

    /**
     * Save
     */
    @Log(title = "Department Management", businessType = BusinessType.UPDATE)
    @RequiresPermissions("system:dept:edit")
    @PostMapping("/edit")
    @ResponseBody
    public AjaxResult editSave(@Validated Dept dept) {
        if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
            return error("Modify department'" + dept.getDeptName() + "'Failed, department name already exists");
        } else if(dept.getParentId().equals(dept.getDeptId())) {
            return error("ModifyDepartment'" + dept.getDeptName() + "'Failed, parent department cannot be self");
        }
        return toAjax(deptService.updateDept(dept));
    }

    /**
     * Delete
     */
    @Log(title = "Department Management", businessType = BusinessType.DELETE)
    @RequiresPermissions("system:dept:remove")
    @GetMapping("/remove/{deptId}")
    @ResponseBody
    public AjaxResult remove(@PathVariable("deptId") Long deptId) {
        if (deptService.selectDeptCount(deptId) > 0) {
            return AjaxResult.warn("subordinate department exists,not allowed to delete");
        }
        if (deptService.checkDeptExistUser(deptId)) {
            return AjaxResult.warn("Department exists user, not allowed to delete");
        }
        return toAjax(deptService.deleteDeptById(deptId));
    }

  // ...
}


同様に、権限管理も同様のアノテーション(@RequiresPermissions)の仕組みで実装されていることがわかる。つまり、アノテーション+AOPの最終的な目標は、モジュールをデカップリングすることであることがわかります。

参考記事

  • https://blog.csdn.net/javazejian/article/details/71860633
  • https://blog.csdn.net/qq_20009015/article/details/106038023
  • https://www.zhihu.com/question/47449512
  • https://www.race604.com/annotation-processing/
  • https://www.runoob.com/w3cnote/java-annotation.html