これはなんですか
Java屋さん向けのjava.lang.Classやjava.lang.Methodやアノテーションを使ったあれやこれやする浅瀬くらいの深みな話。
ソース中でstream構文等を使用しているためjava9未満のバージョンをお使いの方はよくわからないかもしれませんが、心の目で見ればわかる、かも。
やりたいこと
- インターフェースに書いてあるメソッドをimplementsしたメソッドに @Override を書いてない輩を見つけたらあんじょうよろしゅうしたい
- そうだ、単体テストで見つけよう!
- 結論:だめでした
検証その1 指定したクラスのメソッド一覧を取得する
単体テストのコード
import java.util.Arrays;
import org.junit.jupiter.api.Test;
public class FindOverrideTest {
@Test
public void test() {
Arrays.stream(Class1.class.getMethods())
.forEach(method -> System.out.println(method.getName()));
}
interface Interface1 {
void methodHasOverrideAnno();
void methodHasNoOverrideAnno();
}
class Class1 implements Interface1 {
@Override
@Annotation1
public void methodHasOverrideAnno() {
}
// @Override
@Annotation1
public void methodHasNoOverrideAnno() {
}
}
}
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Annotation1 {
}
実行すると得られる出力
methodHasOverrideAnno
methodHasNoOverrideAnno
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll
すべてのクラスは暗黙で java.lang.Object
を継承しているため何も extends してないクラスでも wait, equals, toString等の Object のメソッドが見える
検証その2 Class1
に記述されたメソッドを検出したい場合どうしたらいいか?
@Test
public void test() {
Arrays.stream(Class1.class.getMethods())
.filter(method -> Class1.class.equals(method.getDeclaringClass())) // 後述のフィルタを追加 ※1
.forEach(method -> System.out.println(method.getName()));
}
method.getDeclaringClass() がそのメソッドが宣言されたクラスを返してくれるため、対象のクラスと同一であるかをチェックするフィルタを足します。
実行すると得られる出力
methodHasOverrideAnno
methodHasNoOverrideAnno
検証その3 あのアノテーションをアレせよ
メソッドにくっついてるアノテーションを列挙してみます。
@Test
public void test() {
Arrays.stream(Class1.class.getMethods())
.filter(method -> Class1.class.equals(method.getDeclaringClass()))
.forEach(method -> {
System.out.println(method.getName());
System.out.println(" Annotations = " + Arrays.stream(method.getDeclaringAnnotations()) // 追加
.map(annotation -> annotation.annotationType().getName()).collect( // 追加
Collectors.joining(","))); // 追加
});
}
補足: 追加2行目の annotation.annotationType() で正しいクラスが取得できた
実行すると得られる出力
methodHasOverrideAnno
Annotations = Annotation1
methodHasNoOverrideAnno
Annotations = Annotation1
あれ、おかしいな java.lang.Override
がいない・・なぜだ。
・・・・・・
どうやら java.lang.Override
は@Retention(RetentionPolicy.SOURCE)
による宣言で、ソースファイルのみで保持されるものでした。※2
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
これを単体テストなどの実行時に検出するためには、RetentionPolicy は Runtime である必要がある。※2
本来ならこの処理はソースコードをチェックするSonarCubeとか、CodeGuruあたりでチェックすべきところだということがわかりました。
めでたしめでたし。
あとがき なぜOverrideを書いてない輩を誅したいか、について
openapi-code-generator(※3) でコントローラーのインターフェースを生成すると、defaultなメソッドが作成されるため、コンパイルが通ってしまい、インターフェースと実装されたクラスの乖離に気づかないことがあります。
openapi-code-generator側にはdefault宣言を抑止するフラグがなかったように思いますが、本当にそうだったかは要再確認。
このアプローチとは違うやり方ですが、実装されていないインターフェースのメソッドを発見したらあんじょうよろしゅうするという単体テストは記述できたので、結果としてよしとしてます。
参照
※1 https://stackoverflow.com/questions/2315445/how-to-quickly-determine-if-a-method-is-overridden-in-java 宣言されたクラス名と拾ったクラス名が一致しないと対象のクラスで実装されたことにならないということを教えてくれた記事 他に方法があれば教えて下さい
※2 https://itsakura.com/java-annotation-make アノテーションの説明です めったに使わないので RetentionPolicy.SOURCE
の存在を失念していました。