play1.2をJavassistで改造してみた

先日の第3回PlayFramework勉強会でこんなことを考えた。

play1系はメンテナンスモードか、今後の更新はあまり期待できないな、じゃあJavassistで改造すればいいか。

普通に考えると、自分でPlayFrameworkをビルドしろってことでしょうが、当日は道に迷ったせいかおかしくなってました。
今回は実際Javassistで改造できるものなのか試してみました。

ここをこうしたい

play1系はGsonでJSONエンコードしますが、GsonでなくJSONICとかJackson使いたいんだよ、ってケースだとします。ControllerをOverrideするとか、JSONエンコード後のString渡せばいいじゃんとかありますが、これはまあ例ってことで。
JSONエンコードはRenderJsonのコンストラクタでやってます。これを

package play.mvc.results;

public class RenderJson extends Result {
    public RenderJson(Object o) {
        json = new Gson().toJson(o);
    }
}

こうしたい。

    public RenderJson(Object o) {
        json = net.arnx.jsonic.JSON.encode(o);
    }

RenderJsonを書き換える

前回作ったMyPluginを利用してonApplicationStartで書き換えてみます。

public class MyPlugin extends PlayPlugin
{
    public void onApplicationStart() 
    {
        play.Logger.info("@@@ MyPlugin.onApplicationStart");

        try {
            enhanceRenderJson();
        } catch (Exception e) {
            play.Logger.error(e, "");
        }
    }

    protected void enhanceRenderJson() 
            throws NotFoundException, CannotCompileException, IOException
    {
        // クラスプールから RenderJson を得る
        ClassPool classPool = ClassPool.getDefault();
        CtClass cc = classPool.get("play.mvc.results.RenderJson");

        // コンストラクタのSignatureを得る
        // CtConstructor[] cnsts = cc.getConstructors();
        // for (CtConstructor cnst : cnsts) {
        //     play.Logger.info("@@@ CtConstructor:%s", cnst.getSignature());
        // }

        // コンストラクタ書き換え
        cc.defrost();
        CtConstructor m = cc.getConstructor("(Ljava/lang/Object;)V");
        m.setBody("{"
                + "  play.Logger.info(\"@@@ jsonic encode!\", null);"
                + "  json = net.arnx.jsonic.JSON.encode($1);"    // $1:1つ目のメソッド引数のこと
                + "}"
                );
        cc.writeFile();

        // クラスローダーに登録
        Class thisClass = cc.getClass();
        ClassLoader loader = thisClass.getClassLoader();
        ProtectionDomain domain = thisClass.getProtectionDomain();
        cc.toClass(loader, domain);
    }
}

onApplicationStart ならプラグインでなくても、JobをExtendsしてOnApplicationStartアノテーション付けてもいいですよね。

動作確認

モデル

public class Hoge
{
    public Long id;
    public String name;
    @net.arnx.jsonic.JSONHint(format="yyyy-MM-dd'T'HH:mm:ssZZ")
    public Date createdAt;
}

コントローラー

public class MyController extends Application
{
    public static void index()
    {
        Hoge h = new Hoge();
        h.id =  1000L;
        h.name = "hogehoge";
        h.createdAt = new Date();
                
        renderJSON(h);
    }
}

改造前:Gsonでレンダリング

{
  id: 1000,
  name: "hogehoge",
  createdAt: "Jul 26, 2012 6:47:41 AM"
}


改造後:JSONICレンダリング
JSONHintアノテーションによってcreatedAtがフォーマットされている

{
  createdAt: "2012-07-26T06:48:33+09:00",
  id: 1000,
  name: "hogehoge"
}

まとめ

play1.2をJavassistで改造してみました。
自前ビルドしろよとか改造後のテストはどうすんのとか思いますが、とりあえず置いておきます...

参考

Javassist - ひしだま's ホームページ
Javassistチュートリアル - Acroquest Technology

書き換えたクラスのクラスローダー登録を忘れてはまりました。いやー感謝感謝。