Webサーバを作りながら学んでいる

初めに

少し素振りにScalaを書いている。

タイトル通りだが、以下の本を読みながら書いている。

Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門

まだまだ序盤だが なかなか丁寧に解説されているのと 実験ベースでゆっくり進んでいく。

最初に書いた2つをgitに上げておいた。

TCPサーバ

簡易的なファイルを送りあう、クライアント・サーバのプログラムをまずはじめに書いた。

ソケットを開くのは普通にJavaのライブラリを使った。 ScalaAPIが分かっていないが、たぶんないだろう。(わざわざラップするほどでもないが辛いと言えば辛い。)

というわけで書いたのが以下のソースだ

class TcpServer(port: Int) {

  def open() {
    withClosable(new ServerSocket(port)) { server =>
      withClosable(server.accept()) { socket =>
        val src = socket.getInputStream
        withClosable(Files.newOutputStream(Paths.get("server_recv.txt"))) {
          dest =>
            Source.fromInputStream(src).takeWhile(_ != 0).foreach(dest.write(_))
        }
        val dest = socket.getOutputStream
        withClosable(Files.newInputStream(Paths.get("server_send.txt"))) {
          src =>
            Source.fromInputStream(src).takeWhile(_ != -1).foreach(dest.write(_))
        }
      }
    }
  }
}

object TcpServer extends App {
  new TcpServer(8001).open()
}

めっちゃネスト深い!!めっちゃネスト深い!! まぁこんなもんだろうと思い、諦めた感ある。 元々のソースはJavaなので・・・。 そして溢れ出る、Java臭。

上記ソース中に出てくるwithClosable(Closeableではない)は以下のソースである。 コップ本に出てくるローンパターンをそれっぽく名前を付けて定義しているだけである。

object Resources {

  def withClosable[T <: AutoCloseable](resource: T)(r: T => Unit): Unit = {
    try {
      r(resource)
    } finally {
      resource.close()
    }
  }
}

いい方法が思い浮かばなかった。

簡易的なWebサーバ

とりあえず、適当なヘッダ付けてhtmlを返却してみよう。みたいな課題。

で、さっきのwithClosableをN個定義しようと思ったけど 助け船が。scala-arm、というライブラリがあるそう。

foreach生えた。すごい。 まだ辛い。

// ...省略
   def start() {
     for (
       server <- managed(new ServerSocket(port));
       socket <- managed(server.accept)) {
       val startLine = Source.fromInputStream(socket.getInputStream).getLines().take(1)
       startLine.next().split(" ") match {
         case Array("GET", path, "HTTP/1.1") =>
           val output = socket.getOutputStream
           writeHeader.foreach(output.write(_))
           for (src <- managed(Source.fromInputStream(Files.newInputStream(Paths.get(".", path))))) {
             src.foreach(output.write(_))
           }
        case _ => sys.error("invalid")
       }
     }
   }

scala-armを使う前がこう。 f:id:reteria:20170517050610p:plain

これはひどい。ちなみに画像のソースは動かない。 で、また別の人に助けていただいたのを取り込んで 以下の形に落ち着いた。

// ...省略
  def start() {
    for (
      server <- managed(new ServerSocket(port));
      socket <- managed(server.accept)) {
      val startLine = Source.fromInputStream(socket.getInputStream).getLines().take(1).toList.headOption.map(_.split(" "))
      startLine flatMap {
        case Array("GET", path, "HTTP/1.1") => Some(path)
        case _ => None
      } foreach { path =>
        val output = socket.getOutputStream
        writeHeader.foreach(output.write(_))
        for (src <- managed(Source.fromInputStream(Files.newInputStream(Paths.get(".", path))))) {
          src.foreach(output.write(_))
        }
      }

    }
  }

flatmapっょぃ

最後のforeach内部の処理、もう少し綺麗に書けそうな気もするけど そこまで気にしなくてもいいかなぁとおもいつつ。

まとめ

今回はここで終わり。 Scalaは奥が深い・・・

本を読み進めていきたい。

リポジトリgithub.com

Byte BuddyのAdvice APIを触ってみる

はじめに

前回から色々と触ってきた Byte Buddyというライブラリですが、自分の頭の整理のために ここに簡単にまとめておく。

Byte Buddyはバイトコード操作用のライブラリがあり その中にAdviceというAPIが存在する。

このAPIでは、メソッドの先頭や最後に処理を追加することができ、 次の登場人物が出てくる。

  • Adviceのためのクラス
  • Adviceのためのstaticメソッド
  • staticメソッドに付与@OnMethodEnter, @OnMethodExit

コードを見てもらった方が早いと思うので 前後してしまうが、コードを見せる。 次のコードがAdviceのためのコードだ。

  public static class ForFieldAdvice {
    @OnMethodEnter
    public static void enter(@Advice.FieldValue("string") String field, @Advice.FieldValue("LOG") Logger logger) {
      logger.tracef("ForReturnAdvice:field 'string' is {0}", field);
    }
  }

上記のようなクラスを記述することで追加するバイトコードを記述することができる。 このAPIで追加されたバイトコードはデフォルトではインライン化される。 インライン化しない場合は、OnMethodEnter, OnMethodExitのinlineオプションをfalseにすることで可能である。

勘のいい読者には順番が前後してしまったかもしれないが ライブラリに入っているAdvice APIアノテーションについてのまとめを記述していく。

と、その前に

Byte BuddyでAdviceAPIを使うための簡単なコードを次に記述しておく。

  @Test
  public void testForField() throws IOException, InstantiationException, IllegalAccessException {
    byte[] bytes = new ByteBuddy().rebase(Example.class)
      .visit(Advice.to(ForFieldAdvice.class).on(ElementMatchers.isMethod()))
      .make()
      .getBytes();
  }

rebaseで変更したいクラスを指定し(これはクラスパスから読み込まれるはずである) Advice.toでAdviceのためのクラスを指定する。 その後、onメソッドを利用し、Adviceするメソッドを絞り込む。 メソッドのシグネチャによっては、利用できないアノテーションもあるので それらについてはJavadocの方を参照していただきたい。 (ただまぁ、想像は容易いのでそこまで混乱するわけではないかと思う)

クラスローダーに読み込みすることも可能だが 自分の用途上、byte列で吐き出したかったのでgetBytesメソッドを利用している。 この辺は、ドキュメントを見ていただきたい。(あんまりないが)

Advice.FieldValue

このアノテーションが付与された引数ではフィールド名を指定することで、フィールドの参照が可能である。 もちろん、staticメンバも参照できる。

先程も見た次のコードは、メソッドの先頭にstringというString型のフィールドをログに吐く処理を追加するためのAdviceクラスである。 (※ここで利用しているLoggerはorg.jboss.loggingのLoggerである)

ここ以降、出来る限り、どのようなコードに置き換えられるかはコメントで示していく。

  public static class ForFieldAdvice {
    @OnMethodEnter
    public static void enter(@Advice.FieldValue("string") String field, @Advice.FieldValue("LOG") Logger logger) {
      logger.tracef("ForReturnAdvice:field 'string' is {0}", field);
      // --> LOG.tracef("ForReturnAdvice:field 'string' is {0}", this.field);
    }
  }

対して面白いところはないので、せっせと次に行く。

Advice.This

このアノテーションではバイトコードの操作をしているクラスのインスタンスの取得が可能だ。 staticメソッドはもちろんだが、コンストラクタから呼び出されるようなメソッドでこのアノテーションを利用するのは避けられたし。

  public static class ForThisAdvice {
    @OnMethodEnter
    public static void enter(@This Object arg, @FieldValue("LOG") Logger logger) {
      logger.tracef("ForThisAdvice:this is {0}", arg);
      // --> LOG.tracef("ForThisAdvice:this is {0}", this);
    }
  }

Advice.Origin

このアノテーションでは元々のメソッドの参照が取得可能である。 このアノテーションで取得したメソッドの参照は、リフレクションに置き換えられるので注意されたし。

  public static class ForOriginAdvice {
    @OnMethodEnter
    public static void enter(@Origin Method original, @FieldValue("LOG") Logger logger) {
      logger.tracef("ForOriginAdvice:original method is {0}", original);
      // --> LOG.tracef("ForOriginAdvice:original method is {0}", Example.class.getDeclaredMethod("someMethod", /* 省略 */));
    }
  }

Advice.AllArguments, Advice.Argument

これらのアノテーションではメソッドの引数の取得が可能である。 AllArgumentsアノテーションで取得したメソッドの参照はメソッドの引数の配列に置き換えられることに注意。

  public static class ForAllArgumentsAdvice {
    @OnMethodEnter
    public static void enter(@AllArguments Object[] args, @FieldValue("LOG") Logger logger) {
      logger.tracef("ForAllArgumentsAdvice:arguments is {0}", args);
      // --> LOG.tracef("ForAllArgumentsAdvice:arguments is {0}", new Object[]{arg0, arg1, arg2, /*... 省略 .. */});
    }
  }

  public static class ForArgumentAdvice {
    @OnMethodEnter
    public static void enter(@Argument(0) Object arg, @FieldValue("LOG") Logger logger) {
      logger.tracef("ForArgumentAdvice:1st argument is {0}", arg);
      // --> LOG.tracef("ForArgumentAdvice:1st argument is {0}", arg0);
    }
  }

Advice.Return

このアノテーションではメソッドの戻り値の取得が可能だ。 このアノテーションはOnMethodExitのアノテーションがつけられたadviceのメソッドでのみ利用可能である。

  public static class ForReturnAdvice {
    @OnMethodExit
    public static void exit(@Return Object returns, @FieldValue("LOG") Logger logger) {
      logger.tracef("ForReturnAdvice:returninig value is {0}", returns);
      // -->
      //     Object result = "any";
      //     LOG.tracef("ForReturnAdvice:returninig value is {0}", result);
      //     return result;
    }
  }

Advice.Thrown

このアノテーションではメソッド内で発生した特定の例外に対して、任意の処理を追加することが可能だ。 またreadOnlyメンバをfalseにして、Thrownアノテーションが付けられたメソッドの引数をnullにすることで exceptionの握りつぶしをすることが可能である。

  public static class ForThrownAdvice {
    @OnMethodExit(onThrowable = IOException.class)
    public static void exit(@Thrown(readOnly = false) Throwable e, @FieldValue("LOG") Logger logger) {
      if (e != null)
        logger.tracef("ForThrownAdvice:1st this is {0}", e);
      if (e instanceof UncheckedIOException) {
        e = null;
      }
    }
  }

これは元のコードと変更後のコードが長くなるので 分けて示す。

  // 元々のメソッド
  public void forThrown() {
    throw new RuntimeException("test io exception");
  }

  // Adviceが追加されたメソッド
  public void forThrown() {
    try {
      throw new RuntimeException("test io exception");
    } catch (IOException ex) {
      if (ex != null) {
        LOG.tracef("ForThrownAdvice:1st this is {0}", (Object) ex);
      }
      if (ex instanceof UncheckedIOException) {
        ex = null;
      }
      if (ex != null) {
        throw ex;
      }
    }
  }

OnMethodExitで指定したクラスのcatch文が追加された後に ForThrownAdviceで追加したバイトコードが追加されている。

このThrownアノテーションとOnMethodExitのアノテーションの関係が非常にややこしいので 気が向いたら調べる。(たぶん調べない) (OnMethodExitにはsuppressedとかいうパラメータもある。。。ややこしい。。。)

Advice.Enter

これはOnMethodEnter, OnMethodExitで連携する機能である。 OnMethodEnterの戻り値がOnMethodExitで取得することが可能だ。

 public static class ForEnterAdvice {
    @OnMethodEnter
    public static String enter(@FieldValue("string") String string, @FieldValue("LOG") Logger logger, @FieldValue("any") String any) {
      if (string.equals("test")) {
        return string;
      }
      else if (string.equals(any)) {
        return any;
      }
      return "return value";
    }

    @OnMethodExit
    public static void exit(@Enter String enter, @FieldValue("LOG") Logger logger) {
      logger.tracef("OnMethodEnter returnd '{0}'", enter);
    }
  }

これもまた、差分が長くなるので使った例を次に示す。

  // 元々のメソッド
  public void forArgs(String string, int test) {}

  // Adviceで追加されたメソッド(整形している)
  public void forArgs(final String string, final int test) {
    LOG.tracef("OnMethodEnter returnd '{0}'", (Object) 
       (this.string.equals("test") ? this.string:
         (this.string.equals(this.any) ? this.any : "return value")
       )
    );
  }

Advice.Unused, StubValue

すまん、利用シーンがいまいち分からない。 デフォルトの値を渡してくれる。(numericなら0, referenceならnull) ので、解説しない。

まとめ

ここまででByte BuddyのAPIを見てきたが 他にもたくさんのAPIがあるので、興味があれば見てみてほしい。(ドキュメントはあんまりないが)

今回利用したアノテーションだけでなく、CustomMapping APIを利用して 独自処理でアノテーションを付与したパラメータの解決も可能である。 それは前回の記事で少し書いたので興味があれば見てほしい。

さて、色々あったが Byte BuddyはASMのラッパーとして書かれていて Byte BuddyのGithugにあるWikiによると 既にいくつかのプロジェクトで使われているようだ。 Spock、HibernateやMockitoなどが使っている模様。

Byte Buddyには他にもAgentBuilderや何やらLambda, HotSwapに関するAPIも存在しているので もう少し遊んでみることにする。

これは前回の記事でも書いたが Byte Buddyは何が魅力かというと、コンパイラで型検証したバイトコードを基にバイトコード操作ができるだろう。 これまでバイトコード操作のコードはあまり書いてこなかったが ASMやJavassistを使っていて辛かったのが、実行するまで分からない点が存在することである。

また、それはそれとしてByte Buddyで辛い点としてはstatic importを多用する。これにはいくつか思い出す作業が発生する。 これは辛い。人間はそこまで覚えていられない。

一方で、JavassistやASMで強い点というとバイトコードの情報を利用した柔軟なバイトコード操作が記述できることであるが これはしばしば、低レベルなAPIになりがちである。 筆者はJava8で追加されたTypeAnnotation属性に関するバイトコード操作を記述した際に体感した。 ASMはバイトコードと友達になった気分になれる。(JavassistにはTypeAnnotationのAPIは存在するがアノテーションの数しか取れない。)

というわけで、オチもなくこの記事を締めたいと思う。

次の記事は何を書こうかなぁ・・・。

Enjoy Java.

バイトコード操作でロギング処理を追加する

はじめに

前回はただフィールドを追加するだけのコードを書きました。 なんのひねりもありません。 progret.hatenadiary.com

そして今回はメソッドの最初と最後にロギング処理を追加します。 今回は簡単化のため、標準出力に出力します。

今回の記事ではJavassistとByte Buddyを使ってロギング処理を追加してみたいと思います。 (cglibは心が折れた内部APIがすごい見えるので今回は取り扱いません)

今回は以下のクラスに対してメソッドの最初と最後にロギング処理を追加します。

  public class Some {
    public void someMethod() {
      System.out.println("some implementation");
    }
  }

まずはJavassistから

まずはJavassistで追加してみたいと思います。

  @Test
  public void testAddLogMethodWithJavassist() throws CannotCompileException, IOException, NotFoundException {
    ClassPool pool = ClassPool.getDefault();
    CtClass clazz = pool.get(Some.class.getName());
    for (CtMethod method : clazz.getMethods()) {
      if (method.getDeclaringClass()
        .equals(clazz) && !Modifier.isNative(method.getModifiers()) && !Modifier.isAbstract(method.getModifiers())) {
        method.insertBefore("System.out.println(\"" + method.getName() + ":start\");");
        method.insertAfter("System.out.println(\"" + method.getName() + ":end\");");
      }
    }
    byte[] bytes = clazz.toBytecode();
  }

JavassitのAPIでは、Javaのソース文字列ベースで操作が可能です。 今回はCtMethodのinsertBeforeとinsertAfterというメソッドを使って メソッドの前後に処理を追加しています。

ダブルクォートのエスケープが必要で辛い感じが少しありますが 大した点ではないので置いておきます。

Javassistにはいくつかの制約がありますが $から始まる特殊な変数名により、あらかじめ定められた値の参照をすることができます。(参考URL) ローカル変数は参照できません。

ところで、javassistや標準のリフレクションAPIにあるModifierクラスにisSyntheticがないのはなんででしょう・・・ 欲しいですね。

Byte Buddyで書いてみる

今度はByte Buddyで書いてみます。

  @Test
  public void testAddLogMethodWithByteBuddy() throws IOException {
    byte[] bytes = new ByteBuddy().rebase(Some.class)
      .visit(Advice.to(ExampleAdvice.class)
        .on(ElementMatchers.isMethod()
          .and(isDeclaredBy(Some.class))
          .and(not(isAbstract()))
          .and(not(isNative()))))
      .make()
      .getBytes();
  }

  public static class ExampleAdvice {
    @OnMethodEnter
    public static void enter(@Origin Method method) {
      System.out.println(method.getName() + ":start");
    }

    @OnMethodExit
    public static void exit(@Origin Method method) {
      System.out.println(method.getName() + ":end");
    }
  }

Byte BuddyはJavassistと違い、アノテーションを使った独自のAPIによって バイトコードの操作が可能です。

今回の例ではOnMethodEnterとOnMethodExitアノテーションを使ってメソッドの前後に処理を追加しています。 ちなみに、この例ではExampleAdviceクラスのstaticメソッドが実際に呼ばれるわけではなく インライン化されて呼ばれます。

また、アノテーションのOnMethodEnter/Exitのinlineプロパティをfalseにすることでインライン化しないことも可能です。

では、これで生成したバイトコードはどうなるでしょうか?

public class Some {
  public void someMethod() {
    System.out.println(Some.class.getDeclaredMethod("someMethod", new Class[0]).getName() + ":start");
    System.out.println("some implementation");
    System.out.println(Some.class.getDeclaredMethod("someMethod", new Class[0]).getName() + ":end");
  }
}

こんな感じになります。(一部簡略化しています。) methodを毎回取得する形になってしまいます。 これではいけません。余計な処理が走ってしまいます。 インライン化しなければ、解決することも可能かもしれませんが置いておきます。

では、これをRewriteしてみます。

Byte BuddyにはCustom Mapping APIというものがあり 自分で定義したアノテーションが付けられたadviceの引数に対して、独自のマッピング処理を追加することが可能です。 今回はメソッド名の取得がしたいだけなので 動的ではなく静的に実行が可能なはずです。

というわけで、MethodNameアノテーションを用意してみます。

  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.PARAMETER)
  public static @interface MethodName {
  }

ここで、@Retention(RetentionPolicy.RUNTIME)を付与していることに注意してください。 筆者はここでハマりました。

デフォルトでは、メソッドの引数をマッピングしようとするので 例外が発生してしまいます。

では、MethodNameアノテーションを解決する、DynamicValueというインターフェースを実装したクラスを用意します。

  class MethodNameBinder implements DynamicValue<MethodName> {
    @Override
    public StackManipulation resolve(TypeDescription instrumentedType, MethodDescription instrumentedMethod, InDefinedShape target,
      Loadable<MethodName> annotation, Assigner assigner, boolean initialized) {
      if (target.getType()
        .asErasure()
        .isAssignableFrom(String.class)) {
        String name = instrumentedMethod.getName();
        return new TextConstant(name);
      }
      throw new IllegalStateException("not assignable type");
    }
  }

アノテーションが付与された引数の型がStringをAssignできるなら バイトコード上の定数としてメソッド名を返却します。

では、今回のCustom Mappingに合わせてExampleAdviceのクラスとテストコードに変更を加えます。

  @Test
  public void testAddLogMethodWithByteBuddy() throws IOException {
    byte[] bytes = new ByteBuddy().rebase(Some.class)
      .visit(Advice.withCustomMapping()
        .bind(MethodName.class, new MethodNameBinder())
        .to(ExampleAdvice.class)
        .on(ElementMatchers.isMethod()
          .and(isDeclaredBy(Some.class))
          .and(not(isAbstract()))
          .and(not(isNative()))))
      .make()
      .getBytes();
  }

  public static class ExampleAdvice {
    @OnMethodEnter
    public static void enter(@MethodName String methodName) {
      System.out.println(methodName + ":start");
    }

    @OnMethodExit
    public static void exit(@MethodName String methodName) {
      System.out.println(methodName + ":end");
    }
  }

こんな感じになりました。

実行してみると、Javassistと似たような形でバイトコードが出力されるでしょう。

まとめ

今回はバイトコード操作でロギング処理を追加してみました。

JavassitではJavaのソース文字列ベースでバイトコード操作が可能なのが特徴です。 これはASMで書く時よりもバイトコードに関する知識をあまりなくても非常に楽に取り扱うことができます。 (時と場合によりますが。)

一方、Byte Buddyでは、実際にソースからコンパイルしたバイトコードをベースにバイトコード操作が可能なのが特徴です。 これはJavassistに比べて優秀かというと、コンパイルの検証がコンパイル時になる、という点ではないでしょうか? 実行時に例外が減るのはいいことです。 (Advice次第ではありますが。)

しかし、Byte Buddyでは動的に解決した値に関してインライン化の制限が出てしまいます。 これに対しては、本来静的に解決可能な値であるなら、自分でカスタムした解決処理を追加することで回避できます。 非常に面白いライブラリですね。

どちらも面白いライブラリではありますが Javassistではバイトコードに近い内容の解析も可能なので 使う場面によってライブラリを選ぶべきかとは思います。

次は何書こうかなぁ。