アノテーションを体が欲している:NotNull:NotNull:NotNull

みんなだいすきアノテーション みんな大嫌いアノテーション あの手この手でアノテーション

というわけで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入れた時に気づいたのですが、jmockitVM実行中に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が入っています。

が、現状のJavassistAPIではアノテーションの数しか取れません。 また、アノテーションの配置場所とアノテーションが一緒に取れないといけません。 なかなか骨が折れそうです。 (ローカル変数や型パラメータに対するアノテーションの情報もここに入るので)

asmなら少し調べたところ、触れそうですね。 MethodVisitor (ASM 5.1 Documentation)

visitTypeAnnotationとvisitLocalVariableAnnotationというAPIが存在します。

まとめ

JavapしてJVM Specificationとにらめっこした話、でした。

ASM触ればいい気がしてきた。

みんなもっとアノテーション塗れになろうや!!!!!

備忘録としてここに書いておきます。