javassistでCoC

Scala@東北でPlay!Frameworkを勉強してたところ、「メソッドパラメータに合わせてURLパラメータが自動的に補完される」という不思議な仕様があったので、仕組みを調べてみました。バイトコードを解析してCoC(設定より規約)を実現するのがPlay!流のようです。

昨日のScala@東北の内容

Play! Framework
昨日のScala@東北
@ymnkさんによるPlay! Framework for Scala on GAEサンプル

毎度のことながらスーパーハッカー@ymnkさんの華麗な技を見られるので、参加しないと損ですね。

Play!の不思議な仕様がありました

「メソッド引数名をURL引数名に自動的にマップしてくれる」というもの。

例:

コントローラのメソッド
object Lists extends Controller{
  def show(pekepeke: Long){...}
}

が存在する場合、テンプレートに

 @{Lists.show(list.id)}

と書けば、次のようなURLに自動的に変換される。

 /lists/show?pekepeke=xxxx

リフレクション(java.lang.reflect)では引数名(ローカル変数名)までは取得できないし、なんでこんなことができるのかと疑問だったのですが、Play!のソースを読んで納得。
javassistを使って、バイトコードから引数名を取得しているようでした。

試しにちょっとやってみました

public class Some {
 public void doNothing(){
 }
 public String hello(String fullName,int count){
  String tmpl = "%sさん %s回目のこんにちは";
  return String.format(tmpl, fullName, count);
 }
}

これをコンパイルして、javapしてみます。

> javap -verbose -classpath classes Some

(長いので省略)
public class Some extends java.lang.Object
  :
public Some();
  :
public void doNothing();
  :
public java.lang.String hello(java.lang.String, int);
 Code:
  :
 LocalVariableTable:
  0  23  1 fullName Ljava/lang/String;
  0  23  2 count   I
  :
}

たしかに「LocalVariableTable」というエリアに引数名が残っている。
これらの名前を取り出すには、javassist.jarを使って(CLASSPATHに入れて)バイトコード解析を行います。

import java.util.*;
import javassist.*;
import javassist.bytecode.*;

public class Main {
 public static void main(String[] args) {
  try {
   //上記のSomeクラスのメソッド名と引数名を取得。
   Map<String,List<String>> names = getMethodParamNames("Some");
   System.out.println(names);
  } catch (Exception e) {
  }
 }

 /**
 与えられたクラス名の
  Map<メソッド名,引数名のリスト>
 を返す。
 */
 private static Map<String,List<String>>
  getMethodParamNames(String className)
 throws Exception {
  Map<String,List<String>> methodNames = new HashMap<String,List<String>>();
  CtClass c = ClassPool.getDefault().get(className);
  for (CtMethod m : c.getDeclaredMethods()) {
   List<String> paramNames = new ArrayList<String>();
   CodeAttribute code = (CodeAttribute)m.getMethodInfo().getAttribute("Code");
   LocalVariableAttribute lval = (LocalVariableAttribute)code.getAttribute("LocalVariableTable");
   for (int i = 0; i < m.getParameterTypes().length+(Modifier.isStatic(m.getModifiers())? 0:1); i++) {
    if (lval==null) continue;
    String name = lval.getConstPool().getUtf8Info(lval.nameIndex(i));
    if (!name.equals("this")) {
     paramNames.add(name);
    }
   }
   methodNames.put(m.getName(),paramNames);
  }
  return methodNames;
 }
}

Main#mainの実行結果は以下のとおり。

  {hello=[fullName, count], doNothing=[]}

このしくみを使ってPlay!ではCoCを実現していて、ソースコード上での変数命名が実行時にあらゆるところで影響してくるようです。

Play!ではさらに、ソース修正を即反映(HotDeploy)するために動的にクラスをコンパイル&ロードしたり、ロードする際に必要な機能を付加(Enhance)したりしています。
enhancersのソースコードが参考になります。


コメント

コメントしてください

closed.