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 {
assertNull(cls.getDeclaredField("fieldClass").getAnnotation(classAnnotation));
assertNotNull(cls.getDeclaredField("fieldRuntime").getAnnotation(runtimeAnnotation));
assertGenericsFieldTypeParameter(cls.getDeclaredField("fieldGenericsTypeParameterClass"), classAnnotation, false);
assertGenericsFieldTypeParameter(cls.getDeclaredField("fieldGenericsTypeParameterRuntime"), runtimeAnnotation, true);
assertFieldTypeUse(cls.getDeclaredField("fieldTypeUseClass"), classAnnotation, false);
assertFieldTypeUse(cls.getDeclaredField("fieldTypeUseRuntime"), runtimeAnnotation, true);
assertNull(cls.getMethod("getMethodClass").getAnnotation(classAnnotation));
assertNotNull(cls.getMethod("getMethodRuntime").getAnnotation(runtimeAnnotation));
assertMethodGenericsTypeParameter(cls.getMethod("getMethodGenericsTypeParameterClass"), classAnnotation, false);
assertMethodGenericsTypeParameter(cls.getMethod("getMethodGenericsTypeParameterRuntime"), runtimeAnnotation, true);
assertMethodTypeParameter(cls.getMethod("getMethodTypeParameterClass"), classAnnotation, false);
assertMethodTypeParameter(cls.getMethod("getMethodTypeParameterRuntime"), runtimeAnnotation, true);
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());
assertNotNull(cc.getDeclaredField("fieldClass").getAnnotation(classAnnotation));
assertNotNull(cc.getDeclaredField("fieldRuntime").getAnnotation(runtimeAnnotation));
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));
assertNotNull(cc.getDeclaredField("fieldTypeUseClass").getAnnotation(classAnnotation));
assertNotNull(cc.getDeclaredField("fieldTypeUseRuntime").getAnnotation(runtimeAnnotation));
final String stringDescriptor = "()Ljava/lang/String;";
assertNotNull(cc.getMethod("getMethodClass", stringDescriptor).getAnnotation(classAnnotation));
assertNotNull(cc.getMethod("getMethodRuntime", stringDescriptor).getAnnotation(runtimeAnnotation));
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));
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));
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択だったらわかりやすかったのになぁ。