フィールドやメソッドに付与されたRetentionPolicy.CLASSのアノテーションをJavassistで実行時に判定する

lombok.NonNull やKotlinの valコンパイルすると付与される org.jetbrains.annotations.NotNull など、 RetentionPolicy.CLASSアノテーションを実行時に判定したい場面があった。

RetentionPolicy.CLASS の場合は、 RetentionPolicy.RUNTIME のようにリフレクションでは実行時に情報を取得できないが、クラスファイルには書き出されている。

クラスファイルに情報があるならJavassistで読み込めないかと思って試してみたら、フィールドやメソッドに付与された場合は何とか判定できたのでメモ。

環境

OpenJDK 8.282, Javassist 3.27.0-GA。

実装

アノテーション

RetentionPolicy.CLASSRetentionPolicy.RUNTIMEアノテーションをそれぞれ宣言。

/**
 * RetentionPolicy.CLASS  のアノテーション
 */
@Target({
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.TYPE_PARAMETER,
        ElementType.TYPE_USE,
})
@Retention(RetentionPolicy.CLASS)
public @interface ClassAnnotation {
}

/**
 * RetentionPolicy.RUNTIME のアノテーション
 */
@Target({
        ElementType.FIELD,
        ElementType.METHOD,
        ElementType.TYPE_PARAMETER,
        ElementType.TYPE_USE,
})
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeAnnotation {
}

アノテーションを付与したクラス

先に作成した2つのアノテーションを、フィールド、メソッド、また ElementType.TYPE_PARAMETERElementType.TYPE_USE で総称型の型パラメーターやメソッドの戻り値などに付与しておく。

/**
 * アノテーションを付与したクラス
 */
@lombok.Getter
public class AnnotatedClass {

    @ClassAnnotation
    private String fieldClass;
    @RuntimeAnnotation
    private String fieldRuntime;

    private List<@ClassAnnotation String> fieldGenericsTypeParameterClass;
    private List<@RuntimeAnnotation String> fieldGenericsTypeParameterRuntime;

    private @ClassAnnotation String fieldTypeUseClass;
    private @RuntimeAnnotation String fieldTypeUseRuntime;

    @ClassAnnotation
    public String getMethodClass() {
        return "MethodRetentionPolicyClass";
    }
    @RuntimeAnnotation
    public String getMethodRuntime() {
        return "MethodRetentionPolicyRuntime";
    }

    public List<@ClassAnnotation String> getMethodGenericsTypeParameterClass() {
        return Collections.emptyList();
    }
    public List<@RuntimeAnnotation String> getMethodGenericsTypeParameterRuntime() {
        return Collections.emptyList();
    }

    public <@ClassAnnotation T> T getMethodTypeParameterClass() {
        return null;
    }
    public <@RuntimeAnnotation T> T getMethodTypeParameterRuntime() {
        return null;
    }

    public @ClassAnnotation String getMethodTypeUseClass() {
        return "getMethodTypeUseClass";
    }
    public @RuntimeAnnotation String getMethodTypeUseRuntime() {
        return "getMethodTypeUseRuntime";
    }

}

動作確認

JUnit5によるユニットテストで確認。対比のため通常のリフレクションによる取得も記述。

class AnnotatedClassTest {

    private final Class<AnnotatedClass> cls = AnnotatedClass.class;
    private final Class<ClassAnnotation> classAnnotation = ClassAnnotation.class;
    private final Class<RuntimeAnnotation> runtimeAnnotation = RuntimeAnnotation.class;

    private void assertAnnotatedType(
            AnnotatedElement annotatedElement,
            Class<? extends Annotation> typeParameterAnnotation,
            boolean validAnnotation) {
        assertEquals(annotatedElement.isAnnotationPresent(typeParameterAnnotation), validAnnotation);
    }

    private void assertGenericsFieldTypeParameter(
            Field field,
            Class<? extends Annotation> typeParameterAnnotation,
            boolean validAnnotation) {
        AnnotatedParameterizedType parameterizedType = (AnnotatedParameterizedType) field.getAnnotatedType();
        AnnotatedType annotatedType = parameterizedType.getAnnotatedActualTypeArguments()[0];
        assertAnnotatedType(annotatedType, typeParameterAnnotation, validAnnotation);
    }

    private void assertFieldTypeUse(
            Field field,
            Class<? extends Annotation> typeParameterAnnotation,
            boolean validAnnotation) {
        AnnotatedType annotatedType = field.getAnnotatedType();
        assertAnnotatedType(annotatedType, typeParameterAnnotation, validAnnotation);
    }

    private void assertMethodGenericsTypeParameter(
            Method method,
            Class<? extends Annotation> typeParameterAnnotation,
            boolean validAnnotation) {
        AnnotatedParameterizedType parameterizedType = (AnnotatedParameterizedType) method.getAnnotatedReturnType();
        AnnotatedType annotatedType = parameterizedType.getAnnotatedActualTypeArguments()[0];
        assertAnnotatedType(annotatedType, typeParameterAnnotation, validAnnotation);
    }

    private void assertMethodTypeParameter(
            Method method,
            Class<? extends Annotation> typeParameterAnnotation,
            boolean validAnnotation) {
        TypeVariable<Method> typeVariable = method.getTypeParameters()[0];
        assertAnnotatedType(typeVariable, typeParameterAnnotation, validAnnotation);
    }

    private void assertMethodTypeUse(
            Method method,
            Class<? extends Annotation> typeParameterAnnotation,
            boolean validAnnotation) {
        AnnotatedType annotatedType = method.getAnnotatedReturnType();
        assertAnnotatedType(annotatedType, typeParameterAnnotation, validAnnotation);
    }

    @Test
    void reflectionTest() throws Exception {
        // field
        assertNull(cls.getDeclaredField("fieldClass").getAnnotation(classAnnotation));
        assertNotNull(cls.getDeclaredField("fieldRuntime").getAnnotation(runtimeAnnotation));

        // field generics type parameter
        assertGenericsFieldTypeParameter(cls.getDeclaredField("fieldGenericsTypeParameterClass"), classAnnotation, false);
        assertGenericsFieldTypeParameter(cls.getDeclaredField("fieldGenericsTypeParameterRuntime"), runtimeAnnotation, true);

        // field type use
        assertFieldTypeUse(cls.getDeclaredField("fieldTypeUseClass"), classAnnotation, false);
        assertFieldTypeUse(cls.getDeclaredField("fieldTypeUseRuntime"), runtimeAnnotation, true);

        // method
        assertNull(cls.getMethod("getMethodClass").getAnnotation(classAnnotation));
        assertNotNull(cls.getMethod("getMethodRuntime").getAnnotation(runtimeAnnotation));

        // method generics type parameter
        assertMethodGenericsTypeParameter(cls.getMethod("getMethodGenericsTypeParameterClass"), classAnnotation, false);
        assertMethodGenericsTypeParameter(cls.getMethod("getMethodGenericsTypeParameterRuntime"), runtimeAnnotation, true);

        // method type parameter
        assertMethodTypeParameter(cls.getMethod("getMethodTypeParameterClass"), classAnnotation, false);
        assertMethodTypeParameter(cls.getMethod("getMethodTypeParameterRuntime"), runtimeAnnotation, true);

        // method type use
        assertMethodTypeUse(cls.getMethod("getMethodTypeUseClass"), classAnnotation, false);
        assertMethodTypeUse(cls.getMethod("getMethodTypeUseRuntime"), runtimeAnnotation, true);
    }

    @Test
    void javassistTest() throws Exception {
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass cc = classPool.get(cls.getName());

        // field
        assertNotNull(cc.getDeclaredField("fieldClass").getAnnotation(classAnnotation));
        assertNotNull(cc.getDeclaredField("fieldRuntime").getAnnotation(runtimeAnnotation));

        // field generics type parameter
        // https://www.javassist.org/html/javassist/bytecode/TypeAnnotationsAttribute.html
        FieldInfo fieldInfo;

        fieldInfo = cc.getDeclaredField("fieldGenericsTypeParameterClass").getFieldInfo();
        assertNotNull(fieldInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNull(fieldInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));

        fieldInfo = cc.getDeclaredField("fieldGenericsTypeParameterRuntime").getFieldInfo();
        assertNull(fieldInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNotNull(fieldInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));


        // field type use
        assertNotNull(cc.getDeclaredField("fieldTypeUseClass").getAnnotation(classAnnotation));
        assertNotNull(cc.getDeclaredField("fieldTypeUseRuntime").getAnnotation(runtimeAnnotation));

        // method
        // getMethod の第2引数は JVM の method descriptor
        final String stringDescriptor = "()Ljava/lang/String;";
        assertNotNull(cc.getMethod("getMethodClass", stringDescriptor).getAnnotation(classAnnotation));
        assertNotNull(cc.getMethod("getMethodRuntime", stringDescriptor).getAnnotation(runtimeAnnotation));

        // method generics type parameter
        final String listDescriptor = "()Ljava/util/List;";
        MethodInfo methodInfo;

        methodInfo = cc.getMethod("getMethodGenericsTypeParameterClass", listDescriptor).getMethodInfo();
        assertNotNull(methodInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNull(methodInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));

        methodInfo = cc.getMethod("getMethodGenericsTypeParameterRuntime", listDescriptor).getMethodInfo();
        assertNull(methodInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNotNull(methodInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));

        // method type parameter
        // 総称型の情報はコンパイルされると消えるため、method descriptorはObjectを指定
        final String objectDescriptor = "()Ljava/lang/Object;";

        methodInfo = cc.getMethod("getMethodTypeParameterClass", objectDescriptor).getMethodInfo();
        assertNotNull(methodInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNull(methodInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));

        methodInfo = cc.getMethod("getMethodTypeParameterRuntime", objectDescriptor).getMethodInfo();
        assertNull(methodInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNotNull(methodInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));

        // method type use
        methodInfo = cc.getMethod("getMethodTypeUseClass", stringDescriptor).getMethodInfo();
        assertNotNull(methodInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNull(methodInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));

        methodInfo = cc.getMethod("getMethodTypeUseRuntime", stringDescriptor).getMethodInfo();
        assertNull(methodInfo.getAttribute(TypeAnnotationsAttribute.invisibleTag));
        assertNotNull(methodInfo.getAttribute(TypeAnnotationsAttribute.visibleTag));
    }

}

結果

フィールドやメソッドに付与されたアノテーションについては、通常のリフレクションでは判定できない RetentionPolicy.CLASSアノテーションも、 CtFieldCtMethodgetAnnotation で判定できる。

戻り値は Object になるが、キャスト可能。パラメーターを持つアノテーションの場合に、パラメーターの値を取得できるかは未確認。

コード内では CtMethod#getMethod を使っているが、戻り値や引数の情報をmethod descriptorとして渡さないといけないので、通常は CtMethod#getMethods で配列で取得し、メソッド名で判定することになりそう。

一方、型パラメーターやメソッドの戻り値に付与されたアノテーションの場合、 CtField#getFieldInfoCtMethod#getMethodInfo で取得したクラスから、 getAttribute(TypeAnnotationsAttribute.invisibleTag)RetentionPolicy.CLASSアノテーションが、また getAttribute(TypeAnnotationsAttribute.visibleTag)RetentionPolicy.RUNTIMEアノテーションが付与されていることは判定できるが、アノテーションの型は取れなかった。

ただ、Javassistの挙動をきちんと把握していないので、うまくやれば別の方法で取れるかもかもしれない。 CtField#getTypeCtMethod#getParameterTypes あたりが怪しいが。

getAttribute の戻り値は javassist.bytecode.TypeAnnotationsAttribute だが、メソッドの戻り値に付与した場合、IDEデバッグ機能でインスタンス内を見てみると、 constPool/items/objects[0] の配列内にクラス名が文字列で保存されているため、どうにかできそうではある。

当初の目的であった、 lombok.NonNullorg.jetbrains.annotations.NotNull の判定はできそう。

振り返り

コード書いた後に、別件を調べていてふと Javaアノテーションメモ(Hishidama's Java annotation Memo) を見たら、 Javassistメモ(Hishidama's Javassist Memo)RetentionPolicy.CLASSJavassistで実行時に読み込めることが書いてあった。さすがです。

なぜ RetentionPolicy.CLASSアノテーションのデフォルトなんだろう。 RetentionPolicy.SOURCERetentionPolicy.RUNTIME の2択だったらわかりやすかったのになぁ。