アノテーションを体が欲している: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を実装しています。
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触ればいい気がしてきた。
みんなもっとアノテーション塗れになろうや!!!!!
備忘録としてここに書いておきます。
JavaFX入門した
やる気ない感じでJavaFXを軽く触ることにした。
ごらんの有様だよ!!
ドラッグ・アンド・ドロップしたファイル(.jar or .class)をデコンパイルします。
ドラッグ・アンド・ドロップしたファイルと同じディレクトリにdecompiledというフォルダを作成し
そこにデコンパイルされたソースを展開します。
一部デコンパイルできないケースがあると思いますが
ライブラリが対応してないので許してください。。。
というわけでJavaFXでデコンパイルするだけのアプリケーションを作ったよ、という話でした。
リポジトリはこちらになります。
GitHub - Wreulicke/decompiler-javafx-application: JavaFX Application for decompile tool using windup-procyon
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から呼び出される。
WebSocketAnnotationMethodMessageHandlerはSimpAnnotationMethodMessageHandlerの子クラスである。
SimpAnnotationMethodMessageHandlerにはinitReturnValueHandlersというメソッドが実装されており
これはこの親クラスのAbstractMethodMessageHandler
また、このクラスAbstractMethodMessageHandler
afterPropertySetメソッドはInitializingBeanで定義されているメソッドである。
話を元に戻して、先ほどのSimpAnnotationMethodMessageHandlerのinitReturnValueHandlersではSendToMethodReturnValueHandlerとSubscriptionMethodReturnValueHandlerと
その他の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が呼ばれる個所
AbstractMethodMessageHandler
このクラスはHandlerMethodReturnValueHandlerを実装しており
実際にはaddHandlersで追加されたHandlerに処理を移譲するようなクラスになっている。
HandlerMethodReturnValueHandlerCompositeのgetReturnValueHandlerにてaddHandlersで追加されたHandlerのsupportsReturnTypeの戻り値を見て
実際に使うMethodHandlerの決定が行われる。
このメソッドは、
AbstractMethodMessageHandler
→AbstractMethodMessageHandler
→AbstractMethodMessageHandler
→HandlerMethodReturnValueHandlerComposite#handleReturnValue
→HandlerMethodReturnValueHandlerComposite#getReturnValueHandler
の流れで呼ばれる。
Websocket Scopeの実装
Spring MessagingとSpring Websocketの連携では、websocket scopedなBeanの定義が可能になっている。 これらのScopeの実装に関わるクラスは以下のクラスである。
- org.springframework.messaging.simp.SimpSessionScope
- org.springframework.messaging.simp.SimpAttributesContextHolder
- org.springframework.messaging.simp.SimpAttributes
org.springframework.messaging.simp.SimpAttributesContextHolderの内部にて
ThreadLocalな値を保持しており、その値の型はSimpAttributes型になっている。
SimpAttributesはただのMap
解説はここでは特にしない。
また、SimpAnnotationMethodMessageHandlerのhandleMatchにてSimpAttributesContextHolderのsetAttributesFromMessageとresetAttributesが呼び出されており
ここでWebsocket Scopeの実装が行われているように伺える。
まとめ
とりあえずこれまでのSpringはここまで。