みんなだいすきアノテーション
みんな大嫌いアノテーション
あの手この手でアノテーション
というわけでNotNullアノテーションが付けられたメソッドを
バイトコード操作する話です。
NotNull制約をアノテーションだけで実現します。
似たような仕組みはChecker Frameworkだったり
Bean Validationだったり、Apache PolygeneのConstraintだったり
あるわけでして。
まぁそんな仕組みを書いてみたかった、という話になります。
今回の話は紆余曲折します。
現在、Java8では色んな箇所にAnnotationを記述することができます。
JVM Specificationには以下のサンプルが途中都合上出てきます。
「Outer.Middle.@Foo Inner」なんて、普段使わない気もするのですが
面白いですね。
一番上のStringの二次元の配列の例はどこにAnnotation書いてるのか分かりにくい・・・
@Foo String[][]
String @Foo [][]
String[] @Foo []
@Foo Outer.Middle.Inner
Outer.@Foo Middle.Inner
Outer.Middle.@Foo Inner
@Foo Map<String,Object>
Map<@Foo String,Object>
Map<String,@Foo Object>
List<@Foo ? extends String>
List<? extends @Foo String>
さて、そんなこんなでJava8ではメソッド(コンストラクタ)の引数に自分の型を書くことができます。
レシーバパラメータと呼ばれる機能のようです。
JavaSE8リリース記念!マイナーな言語仕様を紹介してみる(交差型キャスト,レシーバパラメータ(仮引数にthis)) - きつねとJava!
使い方については、以下のようにアノテーションをつけることを想定されたような機能です。
Checker Frameworkのようにアノテーションで型検査をするためにつけるとかでしょうね。
class Test{
void m(Testt his, int n1, int n2){}
}
class Test{
void m(@Sample Test this, int n1, int n2){}
}
TypeScriptも似たような機能があって、
こちらはJavaScriptらしさがあります。
Playground · TypeScript
ECMAScriptのほうにも似たような話が上がってるみたいです。(このリポジトリbind-operatorなんですけどね)
Explicit naming of `this` · Issue #30 · tc39/proposal-bind-operator · GitHub
こっちはthisに別名をつけられるようにしません?みたいな話で
JavaScriptではthisがコロコロ変わることがあるので、下のコードみたいに一時変数への代入をしたりします。
function join(){
const array = this;
return array.join(", ");
}
これに合わせてflow typeのtype annotationで型検査できるとか楽しそうですね。
(現状、似たような機能ってあるんですかね?なんかある気もするけどぱっと見、見つからなかった)
型アノテーションにthisを使える、みたいなのはあるようですが。
Flow | Classes
NotNullアノテーションがあるメソッドに対してnullのチェックを追加する
これはお題目通りのコードです。
今回はバイトコード操作で書いてみます。
方法としてはメソッドのインターセプトとかでも書けそうですね。
InjectionPointからMethodが取れるのでそこからgetParameterAnnotations()でアノテーションが取れるので・・・
ってそっちからの方が良かったのでは・・・
下のようなサンプルのコードを用意しました。
public class Example {
public static void staticMethod(@NotNull String notNullString) {}
public void method(@NotNull String notNullString) {}
public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {}
}
では、これをこんな感じにしましょう。
public class Example {
public static void staticMethod(@NotNull String notNullString) {
Objects.requireNonNull(notNullString, "notNullString is required");
}
public void method(@NotNull String notNullString) {
Objects.requireNonNull(notNullString, "notNullString is required");
}
public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {
Objects.requireNonNull(this, "this is required");
Objects.requireNonNull(notNullString, "notNullString is required");
}
}
できませんでした。
こうなります。
public class Example {
public static void staticMethod(@NotNull String notNullString) {
Objects.requireNonNull(notNullString, "notNullString is required");
}
public void method(@NotNull String notNullString) {
Objects.requireNonNull(notNullString, "notNullString is required");
}
public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {
Objects.requireNonNull(notNullString, "notNullString is required");
}
}
気を取り直して実際書いたコードを下に示します。
javaagentで利用する想定で書いたのでClassFileTransformerを実装しています。
バイトコードの操作にはjavassistを使いました。
public class NotNullInstrumentation implements ClassFileTransformer {
public ClassPool pool;
public NotNullInstrumentation(ClassPool pool) {
this.pool = pool;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try (ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer)) {
CtClass clazz = pool.makeClass(stream);
for (CtMethod method : clazz.getDeclaredMethods()) {
MethodInfo info = method.getMethodInfo();
CodeAttribute codeAttribute = info.getCodeAttribute();
LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
Object[][] annotationArray = method.getParameterAnnotations();
for (int i = 0; i < annotationArray.length; i++) {
Object[] annotations = annotationArray[i];
String name = attribute.variableName(i + (Modifier.isStatic(method.getModifiers()) ? 0 : 1));
for (Object annotation : annotations) {
if (annotation instanceof NotNull) {
method.insertBefore("java.util.Objects.requireNonNull($" + (i + 1) + ",\"" + name + " is required\");");
}
}
}
}
return clazz.toBytecode();
} catch (IOException | ClassNotFoundException | CannotCompileException e) {
throw new RuntimeException(e);
}
}
}
宣言されたメソッドとそのメソッドの引数のアノテーションを見て
NotNullアノテーションを見てメソッドの先頭に処理を追加しています。
今回実装した、nullを検査するコードは実は不完全でもう少し注意深くコードを書かないといけません。
クラスパスにNotNullアノテーションが入っていないとエラーになってしまいます。
やるとしたら、完全修飾クラス名を取得して比較とかするんでしょうね。
横道:ローカル変数名の取得
LovalVariableAttributeはローカル変数テーブルを表しています。
なぜここからデータを取っているかというと
Javacはオプションを渡さない限り、メソッドの引数名を残しません。
このクラスのgetNameでは、コンパイルオプションを渡してない限り、変数名は帰ってきません。(argNになります)
Parameter (Java Platform SE 8 )
テストコード
このコードをテストします。といってもエラーがでないことを確認するだけです。
今回のお供はjMockitさんです。
@Test
public void test() throws Exception {
new MockUp(Class.forName("javassist.CtClassType")) {
@Mock
public boolean isFrozen() {
return false;
}
};
NotNullInstrumentation inst = new NotNullInstrumentation(ClassPool.getDefault());
inst.transform(getClass().getClassLoader(), forInstruments(Example.class), null, null, getByteCode(Example.class));
}
なぜisFrozenにmockupをしているかというと
実行中にClassの書き換えを行うのでjavassistから例外がスローされるからです。
(そういえば、jacocoとjmockit入れた時に気づいたのですが、jmockitはVM実行中にjavaagentを与えているようですね。何やらイケない香りがしますね。)
で、先ほどのコードでなぜthisのrequireNonNullが出力できないのかという話をします。
その前にjavapします。
Annotationさんや~どこぞ~
ちょっとテストのためにメソッドを追加した以下のクラスをjavapしてみます。
public class Example {
public static void staticMethod(@NotNull String notNullString) {}
public void method(@NotNull String notNullString) {}
public void thisAnnotated(@NotNull Example this,@NotNull String notNullString) {}
@NotNull
public void methodAnnotated(@NotNull Example this,@NotNull String notNullString) {}
public List<@NotNull String> typeAnnotated() {
return null;
}
}
下のような情報が出てきます。
~~~~
public static void staticMethod(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
RuntimeVisibleParameterAnnotations:
parameter 0:
0: #17()
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 notNullString Ljava/lang/String;
RuntimeVisibleTypeAnnotations:
0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
public void method(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
RuntimeVisibleParameterAnnotations:
parameter 0:
0: #17()
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/github/wreulicke/bean/validation/Example;
0 1 1 notNullString Ljava/lang/String;
RuntimeVisibleTypeAnnotations:
0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
public void thisAnnotated(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
RuntimeVisibleParameterAnnotations:
parameter 0:
0: #17()
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/github/wreulicke/bean/validation/Example;
0 1 1 notNullString Ljava/lang/String;
RuntimeVisibleTypeAnnotations:
0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
1: #17(): METHOD_RECEIVER
public void methodAnnotated(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
RuntimeVisibleAnnotations:
0: #17()
RuntimeVisibleParameterAnnotations:
parameter 0:
0: #17()
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/github/wreulicke/bean/validation/Example;
0 1 1 notNullString Ljava/lang/String;
RuntimeVisibleTypeAnnotations:
0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
1: #17(): METHOD_RECEIVER
public java.util.List<java.lang.String> typeAnnotated();
descriptor: ()Ljava/util/List;
flags: ACC_PUBLIC
Signature: #28 // ()Ljava/util/List<Ljava/lang/String;>;
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/github/wreulicke/bean/validation/Example;
RuntimeVisibleTypeAnnotations:
0: #17(): METHOD_RETURN, location=[TYPE_ARGUMENT(0)]
}
おやおや色々情報が出てきました。
今回見たいのはレシーバパラメータのメソッドなので少し絞ってみます。
public void thisAnnotated(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
RuntimeVisibleParameterAnnotations:
parameter 0:
0: #17()
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/github/wreulicke/bean/validation/Example;
0 1 1 notNullString Ljava/lang/String;
RuntimeVisibleTypeAnnotations:
0: #17(): METHOD_FORMAL_PARAMETER, param_index=0
1: #17(): METHOD_RECEIVER
RuntimeVisibleParameterAnnotationsには情報が一つついていますね。(あれ?レシーバパラメータのあのテーションは?)
また、先ほどお話したLocalVariableTableには変数名が残っています。
先ほどのコードではここからデータを取り出した形になっています。
17の情報はConstantPoolに保存されています。
おや、#17にはLjavax/validation/constraints/NotNull;ですね。
情報をこんな感じでテーブルにして保持しているのですねー。
Constant pool:
#1 = Class #2 // com/github/wreulicke/bean/validation/Example
#2 = Utf8 com/github/wreulicke/bean/validation/Example
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/github/wreulicke/bean/validation/Example;
#14 = Utf8 staticMethod
#15 = Utf8 (Ljava/lang/String;)V
#16 = Utf8 RuntimeVisibleParameterAnnotations
#17 = Utf8 Ljavax/validation/constraints/NotNull;
#18 = Utf8 notNullString
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 RuntimeVisibleTypeAnnotations
#21 = Utf8 method
#22 = Utf8 thisAnnotated
#23 = Utf8 methodAnnotated
#24 = Utf8 RuntimeVisibleAnnotations
#25 = Utf8 typeAnnotated
#26 = Utf8 ()Ljava/util/List;
#27 = Utf8 Signature
#28 = Utf8 ()Ljava/util/List<Ljava/lang/String;>;
#29 = Utf8 SourceFile
#30 = Utf8 Example.java
{
で、ですね結局、レシーバパラメータの情報が引数のアノテーションと一緒に取れないのは
入ってるところが別だから、ですね。
引数のアノテーションはRuntime(In)VisibleParameterAnnotations
アノテーション全般はRuntime(In)VisibleTypeAnnotationsに入っています。
引数のアノテーションはRuntime(In)VisibleParameterAnnotationsのアノテーションを取得しているからですね~
ローカル変数のアノテーションもこちらに入っているようです。
で、ですね。一応、JavassistにもRuntimeVisibleTypeAnnotationsに対応する
TypeAnnotationsAttributeが入っています。
が、現状のJavassistのAPIではアノテーションの数しか取れません。
また、アノテーションの配置場所とアノテーションが一緒に取れないといけません。
なかなか骨が折れそうです。
(ローカル変数や型パラメータに対するアノテーションの情報もここに入るので)
asmなら少し調べたところ、触れそうですね。
MethodVisitor (ASM 5.1 Documentation)
visitTypeAnnotationとvisitLocalVariableAnnotationというAPIが存在します。
まとめ
JavapしてJVM Specificationとにらめっこした話、でした。
ASM触ればいい気がしてきた。
みんなもっとアノテーション塗れになろうや!!!!!
備忘録としてここに書いておきます。