アノテーションを体が欲している: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触ればいい気がしてきた。

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

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

JavaFX入門した

やる気ない感じでJavaFXを軽く触ることにした。

f:id:reteria:20170216011414p:plain

ごらんの有様だよ!!

ドラッグ・アンド・ドロップしたファイル(.jar or .class)をデコンパイルします。
ドラッグ・アンド・ドロップしたファイルと同じディレクトリにdecompiledというフォルダを作成し
そこにデコンパイルされたソースを展開します。

一部デコンパイルできないケースがあると思いますが
ライブラリが対応してないので許してください。。。

というわけでJavaFXデコンパイルするだけのアプリケーションを作ったよ、という話でした。

リポジトリはこちらになります。
GitHub - Wreulicke/decompiler-javafx-application: JavaFX Application for decompile tool using windup-procyon

余談

本番環境にしかソースがなくて困る!!デコンパイルしたい!!
コード書くのだるい!!JavaFXで適当にpackageして放り投げたい!!
そんな熱い気持ちで書きました。

Spring Messaging でのWebSocketメモ

Spring MessagingでのSTOMPは以下の形で設定を行う。 これがなぜ動くのかを軽く探ってみたのでメモとして残しておく。

普通にSpringでWebsocketを使いたいならリファレンスのここ見ればよいかと。

なぜこのリファレンスにあるソースが動くのかを大まかに調べるためにこの記事にメモしておく。

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic", "/queue");
    }

}

アノテーション@EnableWebSocketMessageBrokeには以下のアノテーションが付けられている

...
@Import({
  DelegatingWebSocketMessageBrokerConfiguration.class
})
public @interface EnableWebSocketMessageBroker {
}

DelegatingWebSocketMessageBrokerConfigurationが渡されている。

DelegatingWebSocketMessageBrokerConfigurationの中身

InjectionされたWebScoketMessageBrokerConfigurerのリストに対して 処理を移譲する形になっている。

親クラスWebSocketMessageBrokerConfigurationSupportにて
SimpAnnotationMethodMessageHandlerを返却するcreateAnnotationMethodMessageHandlerが実装されている。
このメソッド内部ではWebSocketAnnotationMethodMessageHandlerインスタンス化されて返却されている。
このメソッドはAbstractMessageBrokerConfigurationのsimpAnnotationMethodMessageHandlerから呼び出される。

WebSocketAnnotationMethodMessageHandlerSimpAnnotationMethodMessageHandlerの子クラスである。
SimpAnnotationMethodMessageHandlerにはinitReturnValueHandlersというメソッドが実装されており
これはこの親クラスのAbstractMethodMessageHandlerにて抽象メソッドとして定義されている。

また、このクラスAbstractMethodMessageHandlerから afterPropertiesSetメソッドにてinitReturnValueHandlersが呼び出されている。
afterPropertySetメソッドはInitializingBeanで定義されているメソッドである。

話を元に戻して、先ほどのSimpAnnotationMethodMessageHandlerのinitReturnValueHandlersではSendToMethodReturnValueHandlerSubscriptionMethodReturnValueHandler
その他のHandlerMethodReturnValueHandlerを実装しているクラスのリストを返却している。

HandlerMethodReturnValueHandlerは以下のようなインターフェースになっている。

package org.springframework.messaging.handler.invocation;

import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;

public interface HandlerMethodReturnValueHandler {
  boolean supportsReturnType(MethodParameter arg0);

  void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message) throws Exception;
}

この実装の参考はSendToMethodReturnValueHandlerを見るとよいかと思われる。

HandlerMethodReturnValueHandlerのhandleReturnValueが呼ばれる個所

AbstractMethodMessageHandlerafterPropertiesSetにてinitReturnValueHandlersが呼ばれたのちHandlerMethodReturnValueHandlerCompositeのaddHandlersメソッドにそのまま渡されている
このクラスはHandlerMethodReturnValueHandlerを実装しており
実際にはaddHandlersで追加されたHandlerに処理を移譲するようなクラスになっている。

HandlerMethodReturnValueHandlerCompositegetReturnValueHandlerにてaddHandlersで追加されたHandlerのsupportsReturnTypeの戻り値を見て
実際に使うMethodHandlerの決定が行われる。

このメソッドは、 AbstractMethodMessageHandler#handleMessage (MessageHandler#handleMessage)
AbstractMethodMessageHandler#handleMessageInternal
AbstractMethodMessageHandler#handleMatch
HandlerMethodReturnValueHandlerComposite#handleReturnValue
HandlerMethodReturnValueHandlerComposite#getReturnValueHandler
の流れで呼ばれる。

Websocket Scopeの実装

Spring MessagingとSpring Websocketの連携では、websocket scopedなBeanの定義が可能になっている。 これらのScopeの実装に関わるクラスは以下のクラスである。

  1. org.springframework.messaging.simp.SimpSessionScope
  2. org.springframework.messaging.simp.SimpAttributesContextHolder
  3. org.springframework.messaging.simp.SimpAttributes

org.springframework.messaging.simp.SimpAttributesContextHolderの内部にて
ThreadLocalな値を保持しており、その値の型はSimpAttributes型になっている。

SimpAttributesはただのMapである。
解説はここでは特にしない。

また、SimpAnnotationMethodMessageHandlerのhandleMatchにてSimpAttributesContextHolderのsetAttributesFromMessageとresetAttributesが呼び出されており
ここでWebsocket Scopeの実装が行われているように伺える。

まとめ

とりあえずこれまでのSpringはここまで。