🍍

8.3 アノテーションの仕組みとAPI(注釈、カスタムアノテーション、リフレクションAPIなど)~Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

8.3 アノテーション(注釈)の仕組みとAPI

チャプターの概要

このチャプターでは、クラスやメソッドなどに「注釈」として付与するアノテーションについて学びます。
Java SEとして提供される基本的なアノテーションの用法から、独自にアノテーションを開発する方法までを取り上げます。

8.3.1 アノテーション

アノテーションとは

アノテーションとは、クラス、メソッド、フィールドなどの要素に対して「注釈」として付与するメタデータ記法のことで、「注釈インタフェース」と呼ばれることもあります。アノテーションの代表的なものに、@Overrideアノテーションや、チャプター4.1で登場した@FunctionalInterfaceアノテーションがあります。
アノテーションは、ソースコード上においてオブジェクトとして存在しているにも関わらず、プログラム本体の振る舞いには関係しません。何らかのツールや別のソフトウェアに解釈されることによって、はじめて機能します。特にJakarta EE (旧Java EE)やSpringBootといった、サーバーサイドのアプリケーション実行環境では、アノテーションは中核となる技術としてふんだんに活用されています。
各アノテーションには、「保持ポリシー」や「付与対象」といった仕様があります。またアノテーションを利用するときは、「注釈パラメータ」と呼ばれる属性を定義することも可能です。

アノテーションの記述方法

ここではアノテーションをどのように記述するのか、具体例によって説明します。なお以下のクラスにはいくつかのアノテーションが登場しますが、これらは実装イメージを確認するためのダミーであり、特に何らかの機能を持つものではありません。

@FooAnnotation //【1】
public class Greeting {
    @BarAnnotation("名前") //【2】
    private String personName;
    public Greeting(String personName) {
        this.personName = personName;
    }
    @BazAnnotation(value = "挨拶する", order = 1) //【3】
    @QuxAnnotation(value = {"JAPAN", "JAPANESE"}) //【4】
    public void sayHello(
            @HogeAnnotation("年齢") int age) { //【5】
        System.out.println("私は" + personName + "、" + age + "歳です。");
    }
}

アノテーションはクラス、フィールド、メソッド、引数などに指定することができますが、どういった要素に付与することができるのかは、アノテーションの種類ごとに異なります。この例では、様々なアノテーションをクラス【1】、フィールド【2】、メソッド【3、4】、引数【5】に、それぞれ付与しています。【3、4】のように、1つの要素に複数のアノテーションを付与することも可能です。
アノテーションが属性を持つ場合、アノテーションの後ろに( )を記述し、その中に属性を指定します。( )内では、valueという属性が1つの場合に限って、属性名を省略することができます【2】。value以外の属性も合わせて指定する場合は、【3】のように属性ごとに「属性名 = 属性値」と記述し、それぞれを,で区切ります。また属性値が配列になる場合は、【4】のように「{属性値1, 属性値2, .... }」といった形式で、{ }内に属性値を,で区切って列挙します。

アノテーションの「保持ポリシー」

各アノテーションには「保持ポリシー」という仕様があります。これは当該アノテーションを、クラスのライフサイクルのどのタイミングまで保持するか、という意味です。
「保持ポリシー」はjava.lang.annotation.RetentionPolicy列挙型によって表されますが、以下の三種類があります。

  • SOURCE … コンパイラによって評価され、コンパイル時に破棄される。
  • CLASS … コンパイル後にクラスファイルに記録されるが、実行時には廃棄される。
  • RUNTIME … 実行時まで保持される。リフレクションAPIでアノテーションの情報を取得可能。

【図8-3-1】アノテーションの「保持ポリシー」
image.png

例えば@Overrideアノテーションの「保持ポリシー」はSOURCEなので、コンパイラのためのアノテーションです。@Overrideアノテーションの情報は、ひとたびコンパイルされると廃棄されます。また@FunctionalInterfaceアノテーションの「保持ポリシー」はRUNTIMEなので、実行時まで保持されます。ただしこのアノテーションはコンパイラが評価しており、関数型インタフェースのルールに違反していると、コンパイルエラーを検知します。

アノテーションの「付与対象」

各アノテーションには「付与対象」という仕様があります。これは当該アノテーションをどういった要素に対して付与することができるか、ということを表しています。
「付与対象」はjava.lang.annotation.ElementType列挙型によって表されますが、主要なものに以下のような種類があります。

  • TYPE … クラス、インタフェース、列挙型など
  • METHOD … メソッド
  • FIELD … フィールド
  • CONSTRUCTOR … コンストラクタ
  • LOCAL_VARIABLE … ローカル変数
  • PARAMETER … メソッド引数

例えば@Overrideアノテーションの「付与対象」はMETHODなので、付与する要素はメソッドになります。また@FunctionalInterfaceアノテーションの「付与対象」はTYPEなので、付与する要素はインタフェースになります。

Java SEのアノテーション

ここでは既出のものも含めて、Java SEのクラスライブラリによって提供される主なアノテーションを一覧で示します。

【表8-3-1】Java SEの主要なアノテーション(いずれもjava.langパッケージ)

アノテーション 保持ポリシー 付与対象 説明
@Override SOURCE METHOD オーバーライドメソッドに付与し、コンパイラにオーバーライドのルールに違反していることを検知させる。
@FunctionalInterface RUNTIME TYPE 関数型インタフェースに付与し、コンパイラに関数型インタフェースのルールに違反していることを検知させる。
@Deprecated RUNTIME TYPE、METHOD、
FIELD、
CONSTRUCTOR、
LOCAL_VARIABLE、
PARAMETERなど
「非推奨」扱いの要素に付与し、コンパイラに警告を出させる。
@SuppressWarnings SOURCE TYPE、METHOD、
FIELD、
CONSTRUCTOR、
LOCAL_VARIABLE、
PARAMETERなど
様々な要素に付与し、コンパイラによって発せられる警告を抑制する。

この中で、@Overrideアノテーションと@FunctionalInterfaceアノテーションについては既出のため、説明は割愛します。
次項以降では、それ以外のアノテーションの使用法について説明します。

「非推奨」と@Deprecatedアノテーション

アプリケーションは、リリース後のライフサイクルの中で機能が追加されたり仕様が変更になったりしますが、そのような過程の中で、一度開発した機能を「非推奨」扱いにするケースがあります。
具体的には以下のようなケースです。

  • リリース済みのある機能に何らかな不備があり、別の機能への代替を推奨するようなケース
  • リリース済みのある機能が不必要になったが、いきなり削除するのではなく、その機能を利用している他の機能への影響を考慮して、一定期間残存させるようなケース

このようなケースでは、当該機能に該当するクラスやクラスのメンバーを「非推奨」扱いにします。クラスやクラスのメンバーを「非推奨」扱いにする場合は、「APIリファレンス」においてJavadocの@deprecatedタグで明示するのが一般的です。ただしドキュメントへの記載に留まらず、@Deprecatedアノテーションを付与すると、当該要素にアクセスしているクラスがあった場合、コンパイル時に警告を出すことが可能になります。
@Deprecatedアノテーションは、String型のsinceと、boolean型のforRemovalという、2つの属性を持ちます。sinceは任意の属性ですが、「非推奨」扱いになったバージョン番号を指定します。またforRemovalも任意の属性ですが、当該要素が将来的に削除予定の場合は、この属性にtrueを指定します。

コンパイラの警告と@SuppressWarningsアノテーション

コンパイラは、コンパイルエラーにはしないまでも、何らかの問題があると判断したコードに対して警告を発します。
コンパイラが警告を発する典型的なケースには、以下のようなものがあります。

(1)privateなメンバーが宣言されているが、同クラス内で未使用である
(2)未使用のローカル変数が宣言されている
(3)Listインタフェースなどのジェネリックタイプにおいて、型パラメータの指定がない
(4)スタティックなメンバーに対して、インスタンスを経由してアクセスしている
(5)@Deprecatedアノテーションが付与された「非推奨」の要素にアクセスしている

開発者がこれらの警告を何らかの理由で抑制したい場合は、当該の要素に@SuppressWarningsアノテーションを付与します。
なお@SuppressWarningsアノテーションは、valueという名前のString配列型の属性を持ちます。この属性には、以下のような文字列を配列の形で複数同時に指定可能であり、それぞれに対応する警告を抑制します。

【表8-3-2】@SuppressWarningsアノテーションに指定可能な文字列

指定可能な文字列 抑制する警告 対応するケース
unused 未使用なprivateメンバーやローカル変数 (1)、(2)
unchecked ジェネリックタイプで型パラメータの指定がない (3)
rawtypes ジェネリックタイプで型パラメータの指定がない
※"unchecked"とどちらが使われるかはコンパイラによって異なる
(3)
static-access スタティックなメンバーへのインスタンス経由でのアクセス (4)
deprecation @Deprecatedアノテーション(ただしforRemovalはfalse)が付与された「非推奨」なメンバーへのアクセス (5)
removal @Deprecatedアノテーション(ただしforRemovalはtrue)が付与された「非推奨」なメンバーへのアクセス (5)

@SuppressWarningsアノテーションの具体例

ここでは、コンパイラによって発せられる警告を、@SuppressWarningsアノテーションによって抑制する方法を具体的に見ていきます。
以下のコードを見てください。

pro.kensait.java.advanced.lsn_8_3_2.javase.Main_JavaSE_Annotation
public class Main_JavaSE_Annotation {
    @SuppressWarnings("unused") //【1】
    private int tmp1 = 0;
    public static void main(String[] args) throws Exception {
        @SuppressWarnings("unused") //【2】
        int tmp2 = 0;
        @SuppressWarnings("rawtypes") //【3】
        List list = new ArrayList();
        System.out.println(list);
        Integer val1 = null;
        @SuppressWarnings({ "unused", "static-access" }) //【4】
        int val2 = val1.parseInt("1234");
    }
}

まずprivateフィールドであるtmp1は、このクラス内からアクセスされておらず、未使用のため警告が出ます。同じくローカル変数であるtmp2も、宣言されたメソッド内で一度も使われていないため、警告が出ます。これらを抑制するために、当該のフィールドまたはローカル変数に@SuppressWarningsアノテーションを付与し、属性値として"unused"を指定します【1、2】。
次にリストの生成処理において、Listインタフェースに型パラメータを指定していないため、これも警告対象です。これを抑制するために、当該の処理に@SuppressWarningsアノテーションを付与し、属性値として"rawtypes"(または環境によっては"unchecked")を指定します【3】。
最後に、IntegerクラスのスタティックメソッドであるparseInt()メソッドによって文字列を解析していますが、本来はInteger.parseInt()とするべきなので、警告対象になります。またparseInt()メソッドの結果であるローカル変数val2は、メソッド内で未使用です。従ってここでは、当該の処理に@SuppressWarningsアノテーションを付与し、属性値として、配列の形で"unused"と"rawtypes"を指定します【4】。

このように@SuppressWarningsアノテーションによって、コンパイラの警告を抑制することができます。ただし、本来的にコンパイラは、そのコードに何らかの問題があるからこそ、警告を発しているわけです。コンパイラの警告を敢えて抑制するのであれば、抑制しても本当に問題ないかという点を、開発者自身がしっかりと見極めないといけない、というのは言うまでもありません。

@Deprecated@SuppressWarningsの組み合わせ

ここでは「非推奨」扱いの要素があった場合の、@Deprecatedアノテーションと@SuppressWarningsアノテーションの挙動を、具体的に説明します。
まず以下のようなCalculatorクラスがあり、計算機能を提供する汎用的なライブラリとして、他のプログラムから利用されているものとします。

pro.kensait.java.advanced.lsn_8_3_2.javase.Calculator
public class Calculator {
    @Deprecated //【1】
    public static int add(int x, int y) {
        return x + y;
    }
    @Deprecated(forRemoval = true) //【2】
    public static int subtract(int x, int y) {
        return x - y;
    }
}

このクラスには、加算処理のadd()メソッドと、減算処理のsubtract()メソッドがありますが、いずれも「非推奨」扱いにすることが決まりました。さらにsubtract()メソッドの方は、近い将来の削除が予定されています。
そこでこのクラスの利用者に、これらの両メソッドが「非推奨」扱いになったことを示すために、@Deprecatedアノテーションを付与します【1】。subtract()メソッドの方は、「削除予定」であることを明示するために、forRemoval属性にtrueを指定します【2】。
次に、このクラスを利用する側のクラスのコードです。

snippet (pro.kensait.java.advanced.lsn_8_3_2.javase.Main_CalcClient)
@SuppressWarnings("deprecation") //【1】
int result1 = Calculator.add(30, 10);
System.out.println(result1);
@SuppressWarnings("removal") //【2】
int result2 = Calculator.subtract(30, 10);
System.out.println(result2);

Calculatorクラスのadd()メソッドは、@Deprecatedアノテーションにより「非推奨」であることが明示されたため、コンパイラによって警告が出ます。もしこれを抑制する必要がある場合は、当該のメソッド呼び出しに@SuppressWarningsアノテーションを付与し、属性値として"deprecation"を指定します【1】。
また同じくsubtract()メソッドは、@Deprecatedアノテーションにより「非推奨」かつ「削除予定」であることが明示されたため、コンパイラによって警告が出ます。もしこれを抑制する必要がある場合は、当該のメソッド呼び出しに@SuppressWarningsアノテーションを付与し、属性値として"removal"を指定します【2】。「削除予定」の要素に対しては、属性値"deprecation"では警告は消えず、"removal"を指定して初めて警告が抑制されます。

8.3.2 カスタムアノテーションの作成とリフレクションAPIによる操作

カスタムアノテーションの作成方法

このレッスンでは、開発者自身が独自のアノテーション(カスタムアノテーション)を作成し、それをリフレクションによって活用する方法について見ていきます。
アノテーションは以下のように宣言します。

【構文】アノテーションの宣言
@interface アノテーション名 {
    型 属性名() default;
    ........
}

アノテーションは「注釈インタフェース」と呼ばれることからもわかるように、広い意味でのインタフェースの1つであり、構文もインタフェースによく似ています。
アノテーションの宣言では、@interfaceアノテーションと、その後ろにアノテーション名を記述します。
またインタフェースが抽象メソッドを定義できるのと同じように、アノテーションも属性を持つことができます。属性の宣言では、型と属性名を記述します。型として指定可能なのは、プリミティブ型、Stringクラス、Classクラス、列挙型、他のアノテーションと、それらの配列です。属性は内部的には抽象メソッドとして扱われるため、後ろに( )が必要です。
属性には、デフォルト値を定義することができます。そのためにはdefaultキーワードに続けて、デフォルト値を指定します。
このようにして宣言されたアノテーションは、それを利用する側からどのように見えるのか、Java SEのアノテーションを例に説明します。
まず@Overrideアノテーションのように、属性がないアノテーションは、対象の要素に対して@Overrideと付与するだけです。
また@Deprecatedアノテーションは、sinceとforRemovalという2つの属性を持ちますが、いずれもデフォルト値が定義されているためこれらの指定は任意です。すなわち、単に@Deprecatedと記述することもできれば、必要に応じて@Deprecated(forRemoval = true)と記述することもできます。
@SuppressWarningsアノテーションはvalueという単一の属性を持ちますが、デフォルト値は定義されていないため指定は必須です。ただし属性の名前がvalueであり、必須の属性が1つの場合に限り、属性名を省略して@SuppressWarnings("〇〇")と記述することができます。

メタアノテーションとは

メタアノテーションとは、アノテーションに付与するためのアノテーションです。
よく使われるメタアノテーションには、以下のような種類があります。

  • @java.lang.annotation.Retention … 当該アノテーションの「保持ポリシー」を表す
  • @java.lang.annotation.Target … 当該アノテーションの「付与対象」(付与可能な要素)を表す
  • @java.lang.annotation.Documented … 当該アノテーションがドキュメント対象であることを表す

まず@Retentionアノテーションには、当該アノテーションの「保持ポリシー」をjava.lang.annotation.RetentionPolicy列挙型で指定します。このアノテーションが省略されると、「保持ポリシー」はCLASSになります。また@Targetアノテーションには、当該アノテーションの「付与対象」となる要素をjava.lang.annotation.ElementType列挙型の配列で指定します。このアノテーションが省略されると、「付与対象」はすべての要素になります。「保持ポリシー」と「付与対象」の種類については前のレッスンで説明済みなので、ここでは割愛します。
最後に@Documentedアノテーションは、当該アノテーションが(Javadocコマンドによる「APIリファレンス」など)ドキュメント化の対象であることを表します。

Java SEアノテーションの内部実装

ここではカスタムアノテーションを作成する前に、Java SEによって提供される既出のアノテーションが、どのような内部実装になっているのかを確認してみましょう。
まず@Overrideアノテーションは、以下のような実装になっています。

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

このアノテーションには、@Retentionアノテーション、@Targetアノテーションといったメタアノテーションが付与されています。@Retentionアノテーションから「保持ポリシー」がSOURCEであること、@Targetアノテーションから「付与対象」がMETHODであることが分かります。

また@Deprecatedアノテーションは、以下のような実装になっています。

@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,
        MODULE, PACKAGE})
@Documented
public @interface Deprecated {
    String since() default "";
    boolean forRemoval() default false;
}

このアノテーションには、@Retentionアノテーション、@Targetアノテーション、@Documentedアノテーションという3つのメタアノテーションが付与されています。またsinceとforRemovalという2つの属性を持ち、それぞれについてデフォルト値が定義されていることが分かります。

カスタムアノテーションの情報取得

カスタムアノテーションは、基本的には実行時にリフレーションAPIによってその情報を取得し、それに対して何らかの処理を行うために作成します。その場合、カスタムアノテーションには@Retentionアノテーションを付与し、「保持ポリシー」として「RetentionPolicy.RUNTIME」を指定します。
カスタムアノテーションには、@Retentionアノテーション以外にも、@Targetアノテーション、@Documentedアノテーションも合わせて、3つまとめて付与するケースがほとんどです。
それではカスタムアノテーションとして、クラス、メソッド、フィールド、メソッド引数に対して付与し、それらの要素のメタ情報を定義するための、MyMetaInfoアノテーションを作成してみましょう。

pro.kensait.java.advanced.lsn_8_3_2.maxlength.MyMetaInfo
@Retention(RetentionPolicy.RUNTIME) //【1】
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD,
        ElementType.PARAMETER}) //【2】
@Documented
public @interface MyMetaInfo {
    String value(); //【3】
    String elemName() default ""; //【4】
}

このアノテーションは実行時にリフレクションAPIによって情報を取得されるため、@Retentionアノテーションには、RetentionPolicy.RUNTIMEを指定します【1】。またこのアノテーションはクラス、メソッド、フィールド、メソッド引数といった要素に付与するため、@Targetアノテーションでそれを指定します【2】。このアノテーションの属性は、value、elemNameの2つです。valueは必須の属性で、何らかのメタ情報を文字列で定義します【3】。elemNameはデフォルト値を持つ任意の属性で、当該要素の名前を明示するために使用します【4】。

アノテーションのためのリフレクションAPI概要

前述したように、アノテーションの「保持ポリシー」がRUNTIMEの場合、アノテーションの情報は、実行時にリフレクションAPIによって取得することができます。
アノテーション情報を取得するためのリフレクションAPIは、Classクラス、Methodクラス、Fieldクラス、Constructorクラス、Parameterクラスなどに定義されています。
これらのクラスには共通の親クラスを持つものもあり、各クラス間で同じシグネチャになっているため、以下にまとめて示します。

API(メソッド) 説明
<A extends Annotation> A getAnnotation(Class<A>) 指定されたアノテーションが付与されている場合は、当該のAnnotationインスタンスを返し、そうでない場合はnull値を返す。
<A extends Annotation> A getDeclaredAnnotation(Class<A>) 指定されたアノテーションが直接付与されている場合は、当該のAnnotationインスタンスを返し、そうでない場合はnull値を返す。
Annotation[] getAnnotations() この要素に付与されたアノテーションを、Annotation型の配列で返す。
Annotation[] getDeclaredAnnotations() この要素に直接付与されたアノテーションを、Annotation型の配列で返す。

カスタムアノテーションのクラスへの適用

それでは既出のカスタムアノテーション(@MyMetaInfoアノテーション)を、クラスに対して適用する具体例を、コードで見ていきましょう。
以下のようなGreetingクラスを作成し、@MyMetaInfoアノテーションをクラスやメンバーに付与します。

pro.kensait.java.advanced.lsn_8_3_2.maxlength.Greeting
@MyMetaInfo("挨拶クラス") //【1】
public class Greeting {
    @MyMetaInfo("名前") //【2】
    private String personName;
    public Greeting(String personName) {
        this.personName = personName;
    }
    @MyMetaInfo("挨拶する") //【3】
    public void sayHello(
            @MyMetaInfo(value = "年齢", elemName = "age") int age) { //【4】
        System.out.println(
                "こんにちは!私は" + personName + "、" + age + "歳です。");
    }
}

このクラスを見ると、クラス、フィールド、メソッドに対して@MyMetaInfoアノテーションが付与されているのが分かります【1~3】。@MyMetaInfoアノテーションは、何らかのメタ情報を文字列で指定するためのものですが、前述したとおり属性の名前がvalueであり、必須の属性が1つの場合に限り、属性名を省略することができます。
また、メソッド引数に対しても@MyMetaInfoアノテーションが付与されています【4】。ここでは属性を2つ指定しているため、属性名"value"は省略できません。elemName属性には、このメソッド引数の名前として"age"を指定しています。

リフレクションAPIによるカスタムアノテーション情報の取得

ここでは、クラスやメンバーに付与されたアノテーションの情報を、実行時にリフレクションAPIで取得するための方法を、具体的に説明します。前項で登場したGreetingクラスには、すでに@MyMetaInfoアノテーションが付与されていますが、リフレクションAPIによってこのアノテーションの情報を取得してみましょう。
まずクラスに対して付与されたアノテーションの情報は、以下のように取得します。

snippet (pro.kensait.java.advanced.lsn_8_3_2.metainfo.Main_Custom_1)
MyMetaInfo classAnno = Greeting.class.getAnnotation(MyMetaInfo.class); //【1】
System.out.println("MyMetaInfo#value => " + classAnno.value()); //【2】

GreetingクラスのClassオブジェクトに対してgetAnnotation()メソッドを呼び出し、取得したいアノテーションのClassオブジェクトを渡すと、当該アノテーションのインスタンスを取得できます【1】。取得したアノテーションインスタンス(変数classAnno)は、属性をメソッドとして保持しています。ここではvalue()メソッドを呼び出すことにより、value属性に指定された値("挨拶クラス")を取り出すことが可能です【2】。

次に、フィールドに対して付与されたアノテーション情報です。

snippet (pro.kensait.java.advanced.lsn_8_3_2.metainfo.Main_Custom_2)
Field[] fields = Greeting.class.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
    Field field = fields[i];
    MyMetaInfo fieldAnno = field.getAnnotation(MyMetaInfo.class); //【1】
    if (fieldAnno != null) {
        System.out.println("name => " + field.getName());
        System.out.println("MyMetaInfo#value => " + fieldAnno.value()); //【2】
    }
}

フィールドに対して付与されたアノテーションの情報も、基本的にクラスと同じように取得します。
Fieldオブジェクトに対してgetAnnotation()メソッドを呼び出し、取得したいアノテーションのClassオブジェクトを渡すと、当該アノテーションのインスタンス(変数fieldAnno)を取得できます【1】。
アノテーションインスタンスを取得したら、value()メソッドを呼び出すことで、value属性に指定された値("名前")を取り出します【2】。

次にメソッドと、メソッド引数に対して付与されたアノテーション情報です。

snippet (pro.kensait.java.advanced.lsn_8_3_2.metainfo.Main_Custom_3)
Method[] methods = Greeting.class.getMethods();
for (int i = 0; i < methods.length; i++) {
    Method method = methods[i];
    MyMetaInfo methodAnno = method.getAnnotation(MyMetaInfo.class);
    if (methodAnno != null) {
        System.out.println("===== Method =====");
        System.out.println("name => " + method.getName());
        System.out.println("MyMetaInfo#value => " + methodAnno.value());
    }
    Parameter[] params = method.getParameters();
    for (int j = 0; j < params.length; j++) {
        Parameter param = params[j];
        MyMetaInfo paramAnno = param.getAnnotation(MyMetaInfo.class);
        if (paramAnno != null) {
            System.out.println("===== Parameter =====");
            System.out.println("name => " + param.getName()); //【1】
            System.out.println("MyMetaInfo#value => " + paramAnno.value());
            System.out.println("MyMetaInfo#elemName => " + paramAnno.elemName());
        }
    }
}

これらのアノテーションの情報も、基本的にはクラスやフィールドと同じような方法で取得可能なため、詳細は割愛します。

メソッド引数に関してのみ補足します。リフレクションAPIでは、メソッド引数はParameterクラスによって表されます。実はParameterクラスのgetName()メソッド呼び出しにより、メソッド引数の名前を取得しようとしても【1】、このコードでは"arg0"といった具合に開発者が意図していない名前が返されます。そこでGreetingクラスでは、sayHello()メソッドのメソッド引数に@MyMetaInfo(value = "年齢", elemName = "age") int ageとすることで、"age"という名前を付けています。このようにメソッド引数に対して開発者が意図した名前を付け、それをリフレクションAPIで操作したい場合は、アノテーションによって名前を付けるという手法を用いる必要があります。

アノテーションを活用したインスタンスへの汎用的な処理

前項の例では、インスタンス化されていない、クラスの静的な情報をアノテーションによって取得するための方法を取り上げました。
リフレクションAPIとアノテーションを利用すれば、アノテーションに定義された情報を、生成されたインスタンスに対して、実行時に動的に突き合わせることができます。
このようなアノテーションの用途として最も典型的なのが、いわゆるバリデーションです。バリデーションとはすなわち、インスタンスのフィールド値が、事前にアノテーションに定義された情報に違反していないかを実行時に検証し、違反があった場合に例外をスローする、といった処理です。
このような処理を実現するために、まず文字列長の最大値を定義するためのカスタムアノテーション(@MaxLengthアノテーション)を作成します。

snippet (pro.kensait.java.advanced.lsn_8_3_2.maxlength.MaxLength)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface MaxLength {
    int value();
}

そして前項で登場したGreetingクラスのpersonNameフィールドに対して、このアノテーションを付与します。

pro.kensait.java.advanced.lsn_8_3_2.maxlength.Greeting
public class Greeting {
    @MaxLength(10)
    private String personName;
    ........
}

ここでは@MaxLengthアノテーションの属性値として10を指定しているので、「このフィールドの文字列長は最大10文字である」と開発者が明示したことになります。
さて、ここからが本題です。バリデーションの処理として、生成された任意のインスタンスに対して、内包する文字列フィールドの長さと、そこに付与された@MaxLengthアノテーションに指定された属性値の比較を行います。具体的には、今回の例ではGreetingクラスのインスタンスにセットされたpersonNameフィールドの文字列長が、@MaxLengthアノテーションに指定された10文字を超えていないかをチェックする、という処理になります。
ただしこのような処理は、Greetingクラスに特化したものではなく、@MaxLengthアノテーションが付与されたクラスであれば、どのようなクラスにも適用可能な汎用的なものにするべきです。
このような要件を実現するために、以下のようなAnnotationProcessorクラスを作成します。

pro.kensait.java.advanced.lsn_8_3_2.maxlength.AnnotationProcessor
public class AnnotationProcessor {
    public static void checkMaxLength(Object object) {
        Field[] fields = object.getClass().getDeclaredFields(); //【1】
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            MaxLength fieldAnno = field.getAnnotation(MaxLength.class);
            if (fieldAnno == null) continue; //【2】
            try {
                field.setAccessible(true);
                String str = (String) field.get(object); //【3】
                int max = fieldAnno.value(); //【4】
                if (max < str.length()) { //【5】
                    throw new RuntimeException(
                            "文字列長が指定された長さを超えています"); //【6】
                }
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

このクラスのcheckMaxLength()メソッドは、受け取った任意の型のインスタンスに対し、まずそのすべてのフィールドを配列として抽出します【1】。そしてフィールド配列に対してループ処理を行い、各フィールドに@MaxLengthアノテーションが付与されているかをチェックします【2】。@MaxLengthアノテーションが付与されていた場合は、実際にフィールドにセットされた文字列【3】と、アノテーションの属性値【4】をそれぞれ取り出し、その大小関係を判定します【5】。判定の結果、アノテーションの属性値よりも文字列長の方が長かった場合は、バリデーション違反として例外を送出します【6】。
これにて、クラスの種類を問わない、@MaxLengthアノテーションによる汎用的な文字列長チェックロジックが完成です。それでは実際にGreetingクラスのインスタンスを生成し、このチェックを行ってみましょう。

snippet (pro.kensait.java.advanced.lsn_8_3_2.maxlength.Main)
Greeting greeting = new Greeting("Foooooooooo"); // 11文字
AnnotationProcessor.checkMaxLength(greeting);

このコードを実行すると、文字列長のバリデーション違反により、意図したとおりに例外が発生します。
このようにリフレクションAPIとアノテーションを活用すると、汎用性の高い機能を効率的に実現することができます。特にフレームワークとして提供される機能において、アノテーションが多用されている理由が、お分かりいただけたのではないでしょうか。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. アノテーションとはクラス、メソッド、フィールドなどの要素に対して「注釈」として付与するメタデータ記法であること。
  2. アノテーションの「保持ポリシー」と「付与対象」の種類や特徴について。
  3. Java SEのクラスライブラリによって提供される主なアノテーションや、@Deprecated@SuppressWarningsの用法について。
  4. カスタムアノテーションの作成方法について。
  5. カスタムアノテーションの情報をリフレーションAPIによって取得する方法について。

Discussion