Play!の黒魔術を読み解こうとしてみる(未完

この記事は、Play! framework Advent Calendar 2011 jp #play_ja : ATNDの11日目の記事です。

さて、軽めに行きましょう!(ということにさせてください…

僕とPlay!

とあるWEBサービスの受託開発案件があり、自分を含めてメンバー的にJavaプログラマが多かったので、Javaが必然。で、環境面も含めてモロモロがフルスタックでサポートされてるフレームワークはないかなーということで、Play! Frameworkを使うことになりました。

2ヶ月ばかり携わったあとで私はその仕事を離れることになったのですが、無事サービスは開始されたようです。めでたしめでたし。

初めのPlayの印象は「黒魔術」の印象でしたが、それはControllerとviewの結合部分が黒いくらいだけで、それ以外の部分は案外素直な作りではあるので、学習にかかるコストは恐ろしく低く済みました。

で、せっかくなので、その黒魔術の部分について少し読んでみようとして、結局読みきれなかったんですが、書いていきます。

バージョンは1.2.3。ソースコードはすべて https://github.com/playframework/play から引用しています。

Controller.render(Object…args)

viewを描画するときに呼ぶのがControllerのrenderメソッドなわけで、引数として渡す変数名をview側の識別子として使えるわけで、なんとまぁ書きやすいけど不思議な部分なわけです。さて、行きましょう。

    protected static void render(Object... args) {
        String templateName = null;
        if (args.length > 0 && args[0] instanceof String && LVEnhancerRuntime.getParamNames().mergeParamsAndVarargs()[0] == null) {
            templateName = args[0].toString();
        } else {
            templateName = template();
        }
        renderTemplate(templateName, args);
    }

引数の確認をして、配列の先頭がテンプレート名かどうか判定しつつ、renderTemplate(templateName, args); に渡しています。次。

    protected static void renderTemplate(String templateName, Object... args) {
        // Template datas
        Map<String, Object> templateBinding = new HashMap<String, Object>(16);
        String[] names = LVEnhancerRuntime.getParamNames().varargs;
        if(args != null && args.length > 0 && names == null)
            throw new UnexpectedException("no varargs names while args.length > 0 !");
        for(int i = 0; i < args.length; i++) {
            templateBinding.put(names[i], args[i]);
        }
        renderTemplate(templateName, templateBinding);
    }

引数argsからMap templateBindingを作って、renderTemplate(temlateName, templateBinding); に渡しています。雰囲気的に、String[] names = LVEnhancerRuntime.getParamNames().varargs; が怪しいようです。

        public static ParamsNames getParamNames() {
            Stack<MethodExecution> stack = getCurrentMethodParams();
            if(stack.size() > 0) {
                MethodExecution me = getCurrentMethodExecution();
                return new ParamsNames(me.subject, me.paramsNames, me.varargsNames);
            }
            throw new UnexpectedException("empty methodParams!");
        }

スタックの内容があれば、MethodExecution meの内容から、ParamsNamesインスタンスを作って返しているようです。ParamsNamesはデータを扱う程度のクラスで、そのなかのvarargsが、viewに渡る識別子になる雰囲気です。

ということで、meに代入しているgetCurrentMethodExecution()。

        protected static LVEnhancer.MethodExecution getCurrentMethodExecution() {
            Stack<MethodExecution> stack = getCurrentMethodParams();
            if(stack.size() > 0)
                return stack.get(stack.size() - 1).currentNestedMethodCall;
            throw new UnexpectedException("empty methodParams!");
        }

さっきのgetParamNames()と同じように、getCurrentMethodParamsスタックを扱っているようです。で、そのスタックの最後のもの(Stackクラスならpeekと同義???)の、currentNestedMethodCallメンバを返しています。うむー。

で、そのメンバをセットしているのは1箇所で、LVEnhancer.LVEnhancerRuntime.initMethodCall。

        public static void initMethodCall(String method, int nbParams, String subject, String[] paramNames) {
            getCurrentMethodParams().peek().currentNestedMethodCall = new MethodExecution(subject, paramNames, nbParams);
            Logger.trace("initMethodCall for '" + method + "' with " + Arrays.toString(paramNames));
        }

先ほどから見るgetCurrentMethodParamsスタックをpeekして、currentNestedMethodCallを設定しているようですが、これ以上はEclipse上では動作を追うことができません。

ということで「initMethodCall」でgrepしてみると、LVEnhancerクラスのenhanceThisClassメソッドに、以下の記述が見つかりました。

                        stmt.append("play.classloading.enhancers.LVEnhancer.LVEnhancerRuntime.initMethodCall(\"" + dmio.getName() + "\", " + dmio.getNbParameters() + ", " + (methodParams.subject != null ? ("\"" + methodParams.subject + "\"") : "null") + ", $$paramNames);");
                        stmt.append("}");

                        insert(stmt.toString(), ctClass, behavior, codeAttribute, iterator, frame, false);

メソッド呼び出しのようなものが、文字列で書かれています。メソッドの先頭ではjavassist.CtClassを使っているので、Javassist*1を使ってバイトコードの変換をしているようです。

雰囲気的に(こればっかだな)、initMethodCallメソッドの最終引数paramNamesに渡すために埋め込んでいる、"$$paramNames"が怪しそうです。

                        DecodedMethodInvocationOp dmio = (DecodedMethodInvocationOp) frame.decodedOp;
                        StringBuffer stmt = new StringBuffer("{");
                        MethodParams methodParams = DecodedMethodInvocationOp.resolveParameters(frame);
                        stmt.append("String[] $$paramNames = new String[").append(methodParams.params.length + (methodParams.varargs != null ? methodParams.varargs.length : 0)).append("];");

DecodedMethodInvocationOp.resolveParameters(frame)から取ったものをゴリゴリして、String配列を定義するコードにしています。

まとめのようなもの

…とまぁ、追えたのはここまででした。この先は、JavassistのFrameからTrackableArrayを取り出してLocalVariableクラスをごにょごにょしてるらしいです。雰囲気的に。

LVEnhancer#enhanceThisClassはEnhancerクラスの抽象メソッドの実装で、抽象メソッド定義部分のコメントを見てみると、

    /**
     * The magic happen here...
     */
    public abstract void enhanceThisClass(ApplicationClass applicationClass) throws Exception;

とあるので、やはりここが臭いますよね〜という程度で、やんわりと〆ることにします。

次は [twitter:@tan_go238] です。よろしく!