10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

8.3 アノテーションの仕組みとAPI~新Java Advanced編 / AI対応版

10
Last updated at Posted at 2023-02-04

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、約30年にわたって企業システムの開発に携わってきました。現在も現役のアーキテクトとして、重要システムの技術設計やソリューションの選定を担当しています。直近では、システム開発へのAI適用(AI駆動開発)という全社的な施策でリーダーを務めています。
セミナーでの登壇や雑誌への技術記事の寄稿を行ってきたほか、以下の書籍も執筆しています。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『新 Java Advanced編 / AI対応版』の一部の範囲をカバーしたものです。『新 Java Advanced編 / AI対応版』はこちらのリンクから購入できます。

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

  • Javaの基礎を学び終え、設計とレビューができるエンジニアを目指す方
  • AIが書いた並行処理や入出力のコードに潜むリスクを、仕組みから見抜けるようになりたい方
  • フレームワークの裏で使われるリフレクションやアノテーションの仕組みを理解したい方
  • 「Oracle認定Javaプログラマ」Goldの範囲を、仕組みから理解したい方

AIとチャットできるオールインワンの学習環境をリリースしました

Udemy講座の演習を動かすための学習環境として、Windows向けのデスクトップアプリ「Codinable(コーディナブル)」をリリースしました。
エディタ、実行結果、テスト結果、SQL、Webブラウザ、そしてAIチャットを1つの画面にまとめた、オールインワンの学習環境です。

Codinableの画面構成

学習に必要なものは、すべてCodinableの中で完結します。

  • 演習の実行環境(Java 25、Gradle、Bashなど)が一式同梱されており、JDKのインストールや環境変数の設定は不要です
  • インストールして「新規コース開始・再開」からコースを選ぶだけで、講座のすべての演習がそのまま動きます
  • 書き換えて動かなくなっても、「初期化」で配布時の状態に戻せます
  • 開いている演習のファイルと実行結果を踏まえて、AIに質問したり(Askモード)、ファイルの書き換えを指示したり(Agentモード)できます

AIに書き換えを指示し、差分を確かめてから実行する、というAI駆動開発の基本の流れを、Codinableの中だけで安全に体験できます。

AIチャットに書き換えを指示し、差分を確かめて実行する

AIチャットは、受講者ご自身が生成AIサービス(Anthropic、OpenAI、Googleのいずれか)のAPIキーを登録して使うBYOK(Bring Your Own Key)方式です。利用料金は各社から直接請求されます。APIキーを登録しなくても、AIチャット以外の機能はすべて使えます。

Codinableのインストーラは、以下のGitHubのReleasesページからダウンロードできます(64ビット版のWindows 10/11、管理者権限は不要)。

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

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

8.3 アノテーションの仕組みとAPI

チャプターの概要

チャプター8.2では、クラスのメタ情報をもとに処理を行うリフレクションを取り上げました。本チャプターでは、クラスやメソッドなどの要素に「注釈」としてメタデータを付けるアノテーションを取り上げます。アノテーションを誰がいつ読むのかという仕組み、Java SEのアノテーションとコンパイラの警告の関係、そしてカスタムアノテーションをリフレクションで読み取り、汎用的なバリデーションを実現する仕組みを、図で順に整理します。

Jakarta EEやSpring Bootのアプリケーションは、アノテーションで埋め尽くされています。AIはそれらを正しい位置に付けたコードをすぐに書きますが、付けたアノテーションが実行時に本当に読まれるのか、警告を抑制してよいのかは、仕組みを知る人が判断する必要があります。本チャプターの目標は、アノテーションの裏で何が起きているかを説明できるようになることです。

8.3.1 アノテーションの仕組み

アノテーションは、それ自体では何も実行しません。本レッスンでは、アノテーションを「誰が」「いつ」読むのかを押さえ、付けたアノテーションが効くかどうかを判断できるようにします。

アノテーションを読むもの

アノテーションは、クラス、メソッド、フィールド、メソッドの引数などに付けるメタデータの記法です。ソースコード上に書かれていても、プログラムの振る舞いを直接は変えません。コンパイラ、ツール、フレームワークといった別のソフトウェアが読み取って、はじめて機能します。属性を持たせて@BazAnnotation(value = "挨拶する", order = 1)のように値を指定でき、属性がvalue1つだけなら@BarAnnotation("名前")と名前を省略できます。

アノテーションを読み取る3種類のソフトウェアを図で示します。

アノテーションを読み取るソフトウェアと読み取る時点

  • 中央に、@Override・@Deprecated・@Test・@MaxLengthなどのアノテーションが付いたソースコードがある
  • 1つ目の矢印は「コンパイラ」へ向かい、コンパイル時に@Overrideのルール違反をエラーにし、@Deprecatedの要素の利用に警告を出す
  • 2つ目の矢印は「ツール」へ向かい、Javadocの生成やコードの自動生成などに使われる
  • 3つ目の矢印は「フレームワーク」へ向かい、実行時にリフレクションでアノテーションを読み取り、テストの実行(JUnitの@Test)や入力の検証(Bean Validationの@Size)などを行う
  • 下部に「アノテーション自体は何も実行しない。読み取るものがあって初めて働く」という注記がある

保持ポリシーと付与対象

アノテーションには、どの段階まで残るかを表す保持ポリシー(RetentionPolicy)と、どの要素に付けられるかを表す付与対象(ElementType)が決まっています。実行時にフレームワークが読み取るアノテーションは、保持ポリシーがRUNTIMEでなければなりません。

【表】保持ポリシーと主な付与対象

区分 値 意味
保持ポリシー SOURCE ソースファイルにだけ存在し、コンパイル時に破棄される(例: @Override)
保持ポリシー CLASS クラスファイルに記録されるが、実行時には読み込まれない(指定を省略したときの既定)
保持ポリシー RUNTIME 実行時まで保持され、リフレクションで取得できる(例: @Deprecated)
付与対象 TYPE、METHOD、FIELD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE など クラス・メソッド・フィールド・引数などのうち、付けられる要素

3つの保持ポリシーが、コンパイルとロードのどこで失われるかを図で整理します。

保持ポリシーとアノテーションが保持される範囲

  • ソースファイルはコンパイルでクラスファイルになり、クラスファイルはロードでJavaランタイム(メモリー空間)に読み込まれる
  • SOURCEのアノテーションは、コンパイル時に破棄される
  • CLASSのアノテーションは、クラスファイルまで残り、ロード時に破棄される
  • RUNTIMEのアノテーションは、Javaランタイムまで届き、リフレクションで取得できる

8.3.2 Java SEのアノテーションとコンパイラの警告

Java SEのアノテーションの多くは、コンパイラに検査や警告を指示するものです。本レッスンでは、非推奨を示す@Deprecatedと、警告を抑制する@SuppressWarningsを取り上げ、警告を消してよいかどうかを判断できるようにします。

非推奨とコンパイラの警告

ライブラリの機能に不備が見つかり別の機能へ移行させたい場合や、いきなり削除せずに一定期間残す場合、その要素を非推奨にします。@Deprecatedを付けると、その要素を使う側のコンパイルで警告が出ます。属性sinceには非推奨になったバージョンを、forRemovalには将来削除する予定ならtrueを指定します。

【表】Java SEの主なアノテーション

アノテーション 保持ポリシー 働き
@Override SOURCE オーバーライドのルールへの違反をコンパイルエラーにする
@FunctionalInterface RUNTIME 関数型インタフェースのルールへの違反をコンパイルエラーにする
@Deprecated RUNTIME 非推奨の要素を使う側に警告を出させる
@SuppressWarnings SOURCE 指定した種類の警告を抑制する

非推奨を宣言する側と、使う側の関係を以下に示します。

  • 08_reflection_annotation: 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;
    }
}
  • 【1】add()を非推奨にします。使う側の警告の種類はdeprecationです。
  • 【2】subtract()を、削除予定の非推奨にします。使う側の警告の種類はremovalで、"deprecation"を指定しても抑制されません。

非推奨の宣言から、使う側に警告が届くまでを図で示します。

@Deprecatedによる警告の届き方

  • 左側のライブラリCalculatorでは、add()に@Deprecated、subtract()に@Deprecated(forRemoval = true)が付いている
  • 右側の利用側Main_CalcClientがCalculator.add()を呼ぶと、コンパイラがdeprecationの警告を出す
  • 同じくCalculator.subtract()を呼ぶと、コンパイラがremovalの警告を出す
  • removalの警告には「将来のバージョンでコンパイルできなくなる」という注記があり、移行先のAPIへの書き換えが求められる

警告の抑制とその判断

@SuppressWarningsは、指定した種類の警告を、付けた要素の範囲で抑制します。指定できる文字列はコンパイラによって異なり、知らない文字列は無視されます。Codinableのエディターの警告は言語サーバーEclipse JDT LSの診断で、Gradleによるビルドの警告はjavacが出します。

【表】@SuppressWarningsに指定する主な文字列

文字列 抑制する警告 備考
unused 使われていないprivateメンバーやローカル変数 エディターが認識する(javacはこの警告を出さない)
rawtypes、unchecked 型パラメータの無い総称型、型の安全性を検査できない変換 どちらも認識する
static-access スタティックなメンバーへのインスタンス経由のアクセス エディター用。javacではstatic
deprecation、removal 非推奨の要素、削除予定の要素へのアクセス どちらも認識する

コンパイラが警告を出すのは、コードに問題があると判断したからです。演習のMain_JavaSE_Annotationには、Integer val1 = null;に対してval1.parseInt("1234")とインスタンス経由でスタティックメソッドを呼び、static-accessで警告を抑制している箇所があります。本来の対処は、抑制ではなくInteger.parseInt("1234")と書き直すことです。

警告を抑制してよいかを判断する流れを図で示します。

警告を抑制するかどうかの判断の流れ

  • 起点は「コンパイラの警告」で、最初の分岐は「警告の原因をコードで直せるか」である
  • 「はい」の場合は「コードを直す(抑制しない)」に進む
  • 「いいえ」の場合は「抑制しても問題が無いと説明できるか」の分岐に進み、説明できなければ「設計を見直す」に進む
  • 説明できる場合は「最も狭い範囲(ローカル変数やメソッド)に@SuppressWarningsを付け、理由をコメントに残す」に至る
  • 「クラス全体に付ける」「"all"を指定する」には×印が付いている

レビューの観点

  • 警告を消すためだけに@SuppressWarningsを付けていないか。原因をコードで直せないかを確かめる
  • @SuppressWarningsをクラス全体など広い範囲に付けていないか。付けた理由がコメントで説明されているか
  • removalの警告が出るAPI(削除予定)を、新しいコードで使っていないか
  • 非推奨にする側で、sinceやforRemoval、移行先を示すJavadocの@deprecatedを書いているか

8.3.3 カスタムアノテーションとフレームワークの仕組み

本レッスンでは、独自のアノテーションを宣言し、リフレクションで読み取って処理する仕組みを作ります。これは、Bean ValidationやJUnitなどのフレームワークがアノテーションを処理する仕組みの縮図です。

カスタムアノテーションの宣言と取得

アノテーションは@interfaceで宣言します。属性は「型 名前()」の形で書き、defaultで既定値を定義できます。属性の型に使えるのは、プリミティブ型、String、Class、列挙型、アノテーションと、それらの配列です。宣言には、保持ポリシーを指定する@Retention、付与対象を指定する@Target、Javadocの対象にする@Documentedというメタアノテーションを付けます。

  • 08_reflection_annotation: pro.kensait.java.advanced.lsn_8_3_2.metainfo.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】
}
  • 【1】実行時にリフレクションで読み取るため、保持ポリシーをRUNTIMEにします。
  • 【2】クラス、メソッド、フィールド、引数に付けられるようにします。
  • 【3】既定値の無いvalueは、付けるときに必ず指定する属性です。
  • 【4】既定値のあるelemNameは、省略できる属性です。

付けたアノテーションは、Class・Method・Field・Parameterに共通のgetAnnotation(Class)で取得します(いずれもAnnotatedElementインタフェースを実装しています)。付いていなければnullが返ります。取得したアノテーションのインスタンスのvalue()などのメソッドで、属性値を読み出せます。

アノテーションの宣言から、要素への付与、実行時の取得までの流れを図で整理します。

カスタムアノテーションの宣言・付与・実行時の取得

  • @MyMetaInfoは、@Retention(RUNTIME)と@Targetを持つカスタムアノテーションであり、属性value()とelemName()を持つ
  • Greetingクラスでは、クラス、フィールドpersonName、メソッドsayHello()、引数ageの4か所に@MyMetaInfoを付与する
  • 実行時のプログラムは、Class・Field・Method・ParameterのgetAnnotation(MyMetaInfo.class)で、アノテーションのインスタンスを取得する
  • 取得したインスタンスのvalue()とelemName()で、属性値を読む

なお、ParameterのgetName()は、通常のコンパイルでは引数の名前がクラスファイルに残らないため、ageではなくarg0を返します(Main_Custom_3)。javacに-parametersオプションを付けると名前が記録されます。演習のGreetingでは、elemName = "age"のようにアノテーションで名前を与えています。

アノテーションによる汎用的なバリデーション

フィールドの文字列の最大長を宣言する@MaxLength(保持ポリシーRUNTIME、付与対象FIELD、属性int value())を作り、GreetingのpersonNameに@MaxLength(10)と付けます。次のAnnotationProcessorは、任意のインスタンスを受け取り、@MaxLengthが付いたフィールドの長さを検証します。

  • 08_reflection_annotation: pro.kensait.java.advanced.lsn_8_3_2.maxlength.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();
            if (max < str.length()) { // 【4】
                throw new RuntimeException(
                        "文字列長が指定された長さを超えています");
            }
        } catch (IllegalArgumentException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 【1】受け取ったインスタンスのクラスから、すべてのフィールドを取り出します。特定のクラスに依存しません。
  • 【2】@MaxLengthが付いていないフィールドは飛ばします。
  • 【3】フィールドの値を文字列として取り出します。String以外の型ならキャストで、nullなら【4】のlength()で例外になります。
  • 【4】属性値(最大長)を超えていれば、検証の違反として例外を送出します。

checkMaxLength()の処理の流れを図で整理します。

@MaxLengthによる汎用的な文字列長チェックの流れ

  • 任意のインスタンス(例はGreeting)から、getClass().getDeclaredFields()でフィールドの配列を取り出す
  • フィールドに@MaxLengthが付いていなければ、次のフィールドへ進む
  • 付いていれば、field.get()で文字列(例は"Foooooooooo")を、value()で最大長(例は10)を取得する
  • 文字列の長さが最大長を超えていればRuntimeExceptionを送出し、最大長以下なら次のフィールドへ進む

Mainで11文字のnew Greeting("Foooooooooo")を検証すると、例外「文字列長が指定された長さを超えています」が送出されます。Jakarta EEのBean Validation(@Size、@NotNullなど)も、同じ「アノテーションを付け、リフレクションで読み取り、値と照合する」構造で作られています。

保持ポリシーの指定漏れ

カスタムアノテーションで@Retentionを省略すると、保持ポリシーはCLASSになり、実行時には読み込まれません。コンパイルは通り、例外も出ず、getAnnotation()がnullを返すため、検証が黙って行われなくなります。

保持ポリシーの違いで、検証の結果がどう変わるかを図で比べます。

保持ポリシーの指定漏れで検証が黙って行われなくなる様子

  • 上段の@Retention(RUNTIME)の@MaxLengthでは、getAnnotation()がアノテーションを返し、11文字の値が違反として検出される
  • 下段の@Retentionを省略した@MaxLength(既定はCLASS)では、getAnnotation()がnullを返し、フィールドが検証の対象から外れる
  • 下段では、11文字の値がそのまま通り、例外もエラーも出ない
  • 右側に「付けたアノテーションが効いていることを、違反するデータのテストで確かめる」という対策がある

レビューの観点

  • 実行時に読み取るカスタムアノテーションに@Retention(RetentionPolicy.RUNTIME)が付いているか
  • @Targetで、意図した要素にだけ付けられるようにしているか
  • アノテーションを処理するコードが、想定外の型、null、継承したフィールドを扱えるか
  • 検証の違反を知らせる例外のメッセージに、フィールド名・上限値・実際の値など、原因を特定できる情報が含まれているか
  • アノテーションが効いていることを、違反するデータを使ったテストで確かめているか

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

本チャプターでは、以下を取り上げました。

  1. アノテーションはそれ自体では何も実行せず、コンパイラ・ツール・フレームワークが読み取ってはじめて働くこと。
  2. 保持ポリシー(SOURCE・CLASS・RUNTIME)によって読み取れる時点が決まり、実行時に読むにはRUNTIMEが必要で、省略するとCLASSになること。
  3. @Deprecatedは使う側に警告を出させ、@SuppressWarningsは警告を抑制するが、抑制の前に原因をコードで直せないかを判断すべきこと。
  4. カスタムアノテーションを宣言し、getAnnotation()で読み取って値と照合することで、Bean Validationのような汎用的な検証を実現できること。
  5. アノテーションの処理は、指定漏れや型の不一致があっても黙って効かなくなることがあるため、違反するデータで効いていることを確かめるべきこと。
10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?