lombok.NonNull
やKotlinの val
をコンパイルすると付与される org.jetbrains.annotations.NotNull
など、 RetentionPolicy.CLASS
のアノテーションを実行時に判定したい場面があった。
RetentionPolicy.CLASS
の場合は、 RetentionPolicy.RUNTIME
のようにリフレクションでは実行時に情報を取得できないが、クラスファイルには書き出されている。
クラスファイルに情報があるならJavassistで読み込めないかと思って試してみたら、フィールドやメソッドに付与された場合は何とか判定できたのでメモ。
環境
OpenJDK 8.282, Javassist 3.27.0-GA。
実装
アノテーション
RetentionPolicy.CLASS
と RetentionPolicy.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_PARAMETER
や ElementType.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
のアノテーションも、 CtField
や CtMethod
の getAnnotation
で判定できる。
戻り値は Object
になるが、キャスト可能。パラメーターを持つアノテーションの場合に、パラメーターの値を取得できるかは未確認。
コード内では CtMethod#getMethod
を使っているが、戻り値や引数の情報をmethod descriptorとして渡さないといけないので、通常は CtMethod#getMethods
で配列で取得し、メソッド名で判定することになりそう。
一方、型パラメーターやメソッドの戻り値に付与されたアノテーションの場合、 CtField#getFieldInfo
や CtMethod#getMethodInfo
で取得したクラスから、 getAttribute(TypeAnnotationsAttribute.invisibleTag)
で RetentionPolicy.CLASS
のアノテーションが、また getAttribute(TypeAnnotationsAttribute.visibleTag)
で RetentionPolicy.RUNTIME
のアノテーションが付与されていることは判定できるが、アノテーションの型は取れなかった。
ただ、Javassistの挙動をきちんと把握していないので、うまくやれば別の方法で取れるかもかもしれない。 CtField#getType
や CtMethod#getParameterTypes
あたりが怪しいが。
getAttribute
の戻り値は javassist.bytecode.TypeAnnotationsAttribute
だが、メソッドの戻り値に付与した場合、IDEのデバッグ機能でインスタンス内を見てみると、 constPool/items/objects[0]
の配列内にクラス名が文字列で保存されているため、どうにかできそうではある。
当初の目的であった、 lombok.NonNull
や org.jetbrains.annotations.NotNull
の判定はできそう。
振り返り
コード書いた後に、別件を調べていてふと Javaアノテーションメモ(Hishidama's Java annotation Memo) を見たら、 Javassistメモ(Hishidama's Javassist Memo) で RetentionPolicy.CLASS
をJavassistで実行時に読み込めることが書いてあった。さすがです。
なぜ RetentionPolicy.CLASS
がアノテーションのデフォルトなんだろう。 RetentionPolicy.SOURCE
と RetentionPolicy.RUNTIME
の2択だったらわかりやすかったのになぁ。