Category Archives: Mayaa

5年間Mayaaを使って思ったこと

関連記事はこちら:そろそろ2年間Mayaa使ってわかったことを書く

上記記事を書いたのが2011年ですので、あれから3年。まだMayaa使ってます(笑)

一つのプロダクトをずっとメンテし続けられるって、ビジネス的に成功した証拠ですから素晴らしいです!というわけで、10年の半分Mayaaを使い続けて思ったことを書いてみようと思います。

Mayaaは長期メンテに耐えるライブラリか

もちろんYesです。もう作られてから10年近い老舗プロダクトで、Javaは1.4準拠だったりしますが、それを加味しても十分現役で使えます。それは、私の勤務先でもずっと使い続けられていることが証拠です。

教育コスト・生産性は

当初とっつきにくい、教育コストがかかると思いましたが、大して問題になりませんでした。既に10人以上の開発者がMayaaを使った開発をしていますが教育コストが高いと思ったことはありません。

この辺りは、「2年間~」のときに書いたように開発ルールを定めたことが効いたのだと思います。はじめにビビってこれらのドキュメントを整えたことがかえって組織には良かったのかもしれません。

コードとデザインが分離するのでフロントのコードがスパゲッティにならないことも良かったです。最初のめんどくささは確実に後から利益となって返済されています。

デザイナーとの相性

あれから、想像した以上にデザイナーと開発者のすれ違いというものを多く目撃してきました。しかしそれはそもそも話せば分かる問題だったり、Javaプログラマーは業務システムを多く経験してきていてフロントに疎いことが問題だったりするためであり、むしろ、Mayaaを使うことで、一緒のプロダクトを同時開発することで発言した必然的なことであるような気がします。

多くの組織では、デザイナーと開発者の線を明確に切るようです。うちのように両者が入り乱れて開発できる現場というのはそんなに多くないでしょう。それができることは、お互いに、そしてプロダクトにとっても、ユーザーにとっても良いことだと思います。

パフォーマンス

今私が全力で取り組んでいる課題です。インフラがクラウド化してパフォーマンスとコストが直結したこと、Webの利用が広がりユーザトラフィックに対してサーバの余裕はもはやないこと、そしてWebブラウザの性能が向上したこともあり、低コストで、高パフォーマンスを実現することがWebアプリケーションに求められています。

そんな中で、Mayaaははっきり言ってパフォーマンスが良くありません。

Mayaaはかなり頑張っています。まず重要なのがキャッシュです。SpecificationCacheというオブジェクトに、ページのツリーデータがキャッシュされていきます。このため、普段のレンダリングは十分に高速です。ただし、キャッシュを行った結果は次の2つの問題点が発生します。

  • キャッシュ自体のメモリのオーバーヘッド
  • 更新直後などのキャッシュがない状態のパフォーマンス

キャッシュ自体のメモリオーバーヘッドは大きいです、たった数10KB程度のテンプレートファイルが1MBを超えたりします。特にJava6の古いマイナーバージョンや、Java5以前を使っているなら、即Java7以降に移行するべきです。Java6u14以降には圧縮ポインタという機能が搭載されています。詳しくはこちらに書きました。→64bit環境でなんかheapを多く消費するなあと思ったら

それでも、メモリを食うことは変わらないので、なるべく大きめにヒープを割り当てるようにしてください。トラフィックやサイトの種類にも依存しますが目安としては全テンプレートファイルの10から50倍程度用意するべきだと思います。

キャッシュがないときの問題も大きいです。そこで、Mayaaは可能な限り、キャッシュがない状態を防ごうとしています。ヒープがいっぱいになってキャッシュがGCによって消えてしまった場合や再起動した時も、ハードディスク上にserializeしておき、そこから復元することでXML解析の時間を短縮しようとする機能があります。この機能はデフォルトでOFFになっていますが、ONにしても問題が発生することはないので、プロジェクトの初期段階からONにすることをおすすめします。

キャッシュがない状態でのレンダリングで最も時間を食っている原因は、Rhinoスクリプトのコンパイル・最適化です。Rhinoスクリプトをそのまま実行すると遅いので、一旦Javaクラスとしてコンパイルするとう最適化を行っています。そして、Javaクラスは繰り返し実行するにつれてJITコンパイルされ次第にネイティブコードに近くなっていきます。なので、数回同じページを表示すると、JSPやServletで画面を構築した場合に劣らないくらい高速になります。VMを再起動したり、テンプレートを更新するとこれらキャッシュは消えてしまいます。コンパイルされたRhinoスクリプトはtransientなので、シリアライズ・デシリアライズされると最適化がはじめからやり直しです。この件については、今後Mayaa自体が効率化することを望みます。

究極のパフォーマンス改善は、そもそもRhinoScriptを使用しないことです。RhinoScriptを使用しないためには、独自のProcessorを作成する必要があります。Mayaaのソースを読んでいくことで、TextCompiledScriptが使用されている箇所を軒並み潰して、Reflectionなどを使用して直接Javaコードを呼び出すようにすることで、ページの処理速度が最大5倍程度速く出来ました。

拡張性

かなり良いです。5年間で本体をforkし独自のコードを挿入する必要があったことはありません。

Processor, InjectionResolver, Builderなどを独自化することで全く別のテンプレートエンジンに仕上げてしまうこともできます。

ただし、一方で、拡張するには非常に多くのコードを書かなければいけなかったり、キャッシュやMayaaツリーの解析など、あらゆる機構が独自で実装されているため、ソースコードのかなりの部分を精読しなければなりません。なんでもできるけど、その難易度は高いといったところです。

XPathMatchesInjectionResolverは無効にするべき

Mayaa User Mailing listに私が報告したとおり、こいつがあるとパフォーマンスが低下します。またメンテ性が悪いのではじめから使うべきではありません。

org.seasar.mayaa.provider.ServiceProvider
を編集し、下記のようにコメントアウトしてしまいましょう。

<!-- <resolver class="org.seasar.mayaa.impl.builder.injection.XPathMatchesInjectionResolver"/> -->

動的なページの構築、MayaaはSPAよりPJAXと相性が良い

最近はSPAといって静的なHTMLページ一枚でWebアプリを構成し、JavaScriptによる非同期リクエストにサーバはJSONを返すことで画面を構築していくというアプローチが普及しつつあります。AnqularJSなどがそのページの作り方をサポートするライブラリとして有力です。

この構成でサイトを作る場合は、そもそもMayaaの出る番がありません。しかし、もし作っているものが、Webサイトの延長で、全ての画面の状態にはURLがあるという状況においてはSPAにせず、Mayaaを使ってPJAXを実装するとスムーズです。

つまり、Ajaxに対してレスポンスするのはJSONではなく、普通のHTMLページであり、jQueryを使うなどしてここから必要な断片を画面に上書きするというアプローチです。Push Stateを使用してURLも書き換えてしまえばSEOもバッチリです!このURLが静的コンテンツとしてMayaaにリクエストした時のページのURLでもあり、静的ページと動的ページの両方をMayaaによって実装することができます。

2014年以降もMayaaは最強か?

これについては、なんとも言えないでしょう。
拡張しやすさを求めるならMixer2、生産性を求めるならThymeleafもあります。パフォーマンスを求めるならJSPで割り切るのも手です。

デフォルト状態でのデザイナーとの相性が非常に良いので、CMSのような物を作りたい場合は、Mayaaは未だにかなり良い選択肢です。この領域にはWordPressというそもそものデファクトスタンダードがあるので、わざわざJavaで作るという機会はあまりないかもしれません。でも、もしそのタイミングが訪れたなら、WordPressのテーマやプラグインよりもずっと良い基盤です。

パフォーマンスの問題がシビアな状況ではあまり適切とはいえません。あらゆる汎用エンジンは何かしらパフォーマンスとトレードオフの関係にあります。いっそ、独自でテンプレートエンジンを作るのも手だと思います。

Mayaaの柔軟性を取り入れつつ、パフォーマンスも良くしたい場合、私が業務で取り組んでいるRhinoを使わないしくみがもし汎用化できて、公開することができれば最高ですね。それは私の業務的にはプライオリティが高いことなので、案外実現してしまう可能性もあります。が、まだ約束できることではないことので、現時点ではまだ考慮にいれられないことが残念です。

JJUG ナイト・セミナー 「ビール片手にLT&納涼会」でLTしてきました。

動画:

こちらが自分で

こちらが共同発表をしてくれた@smilelx_xl さんです。

去年の11月にCCCで発表させて頂いて以来、公の場で発表するのは二度目でした。共同発表をしてくれた、 @smilelx_xl さんは初めての発表だったようですが、手書き風スライドが好評でした。
一方僕の発表はビデオで見ると早口過ぎて聞き取れない、スライドは文字が小さすぎて読めないという相当ダメな内容で、改善の余地があることが分かりました。自分の発表をビデオで見るということは非常に勉強になり、撮影・youtubeアップいただいた、@yusuke さんには大変感謝致します。

今回の発表で伝えたかったことは、デザイナーさん視点の声でした。僕の発表は、@smilelx_xl さんがスムーズに発表できるための前置き的位置づけだと考えれば十分目的を達成できたのではないかと思います。
スライドで書いたように、2009年からMayaaを使い始め、この発表で扱われていることは、2010年から2012年頃の話です。その頃は自分の所属する会社も小さく従業員は一桁でした。プログラマーといえば、社員は僕ともう一人、Webデザイナーさんは@smilelx_xl さん一人でした。サービスを成長させる過程は面白いものですが、このテンプレートエンジンのこと一つとっても、世の中にシェアしたいと常々思っていました。ただ、多分僕一人出て行ってもインパクトがないので、@smilelx_xl さんに発表させたいと周囲に話したりしていました。もし、Seasar Conferenceが2011年以降も行われていたら、もっと早く応募して発表が実現していたかもしれません。JJUGはちょっと固いイメージがあったので発表するには敷居が高く感じていました。そんなJJUGも最近世代交代が行われたため、急に近寄りやすくなったと感じています。お陰で今回の発表を応募することができたと思います。

Twitterで「デザイナーは名前空間なんて言わないだろ」という的確なつっこみを頂きましたが、ごもっともです。実は今回の発表は、僕が事前に原稿案を作成し、@smilelx_xlさんに渡していました。@smilelx_xlさんはそのまま書かれたのですね。まったくもって「一般人に分かりやすい」用語を使っていない一例です。
今回はデザイナーとプログラマーの話でしたが、今回の話の概要は、実は、別の組み合わせでも成り立つと思います。違う職種同士共同作業をする上で大切なことは同じだと思います。一人で全部できてしまうスーパーエンジニアなら、もっと効率がよいかもしれませんし、実際にそういうスキルがある人も知っていますが、普通はなかなかいないでしょう。それに、やはり、違うバックグラウンドをもった人同士が共同作業をすることは楽しい。

そういうわけで「Aを利用してプログラマーとBが共同作業をする上で大切なこと」シリーズをやったらどうかと思いましたが、毎回僕が社内の別部署の女子を連れて発表したらさすがに殺されると思うので、うちのチームの誰かやってくれないかなーと思っていない(笑)

これでこの件の発表はおしまいと思ったのですが、尊敬する@skrbさんに、JJUG CCC 2013 Fall の Call for Paperに応募しないかとはっぱをかけられてしまったので、今回の発表をもっと詳細化したバージョンをまた@smilelx_xlさんと共同で計画しています。もし、採用されたら今度は早口にならずに丁寧に話したいと思います。

Mayaaでm:idの解決の仕方を自分好みにカスタマイズする方法

このブログで何度か触れましたが、僕の勤務先の会社ではMayaaを使っています。まだMayaaを使っていますし、これからも使っていくと思います。しかしさすがにMayaa長いこと使用していると、次の悩みが発生しました。

  • default.mayaaファイルが巨大化しすぎた
  • 利用当初のノウハウが無かった頃に作ったm:id体系を改めて新しく作り直したい!

しかしながら、

  • 既存の資産を捨てる訳にはいかない

というビジネス上の事情もあり、このような手段を取ることにしました。

  • 今までのID体系をm:id属性として提供し、新しいID体系をe:idとして提供する(eは弊社の製品のイニシャルがeであるためです)
  • e:idは全てdefault.mayaaのように全ページで使えるようにし、ファイルが肥大化しないように分割できるようにする
  • Mayaaファイル内の記述が冗長にならないよう、よく使うプロセッサーをショートカット出来るような新しいプロセッサーを作る

これらについての実現方法を今日は紹介したいと思います。ボリュームが多いので複数回に分けようと思います。

Not only m:id, but also e:id

テンプレートのid、またはm:id属性と、mayaaファイルのプロセッサーとのマッピングは、EqualsIDInjectionResolverによって行われています。他に、XPathMatchesInjectionResolverなど、複数のInjectionResolverが存在しそれらを登録することで、柔軟にプロセッサーの解決ルールを定義することができます。なんて柔軟な作りなのでしょう!このおかげで独自のInjectionResolverを実装して登録することによって、独自のルールでプロセッサーを解決することが出来るのです!この時点で勝利が決定したようなものです。

では、InjectionResolverの実装はどのようにすればいいでしょうか?実際はEqualsIDInjectionResolverのコードを熟読したわけですが(美しいコードで読みやすかったです!)、今回はEqualsIDInjectionResolverを継承することにしました。修正部分はこの部分です。

まず、IDをm:idではなく独自のnamespaceの属性で取れるようにします。

// ここにURIを定義、実際は所属組織のURIなどを記述
public static final URI URI_EXAMPLE_COM = URIImpl.getInstance("http://hogehoge.example.com");

@Override
protected NodeAttribute getAttribute(SpecificationNode node) {
    NodeAttribute attr = node.getAttribute(QNameImpl.getInstance(URI_EXAMPLE_COM, "id"));
    if (attr != null) {
    return attr;
    }
    return null;
}

これで、テンプレートに

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:m="http://mayaa.seasar.org" xmlns:e="http://hogehoge.example.com" xml:lang="ja" lang="ja">

と記述するだけで、m:idではなくe:idでマッピングすることができます。さらに、デフォルトのページ名.mayaaやdefault.mayaaを見に行かず別のルーティングでmayaaファイルを見つけに行くようにしましょう。これにはどうしたらよいでしょ?ヒントはページのmayaaファイルの次にdefault.mayaaを読みに行く機構です。親のMayaaファイルを探しに行く機構として、ParentSpecificationResolverというインターフェースが提供されています。

public interface ParentSpecificationResolver extends ParameterAware {
    /**
     * 指定した{@link Specification}の親を取得する。
     * <p>
     * 標準の実装では、テンプレートファイルの場合は対応するMayaaファイル、
     * Mayaaファイルの場合はdefault.mayaaファイルの{@link Specification}を返す。
     * default.mayaaの親はないので{@code null}を返す。
     * </p>
     * @param spec 親を探す起点となる{@link Specification}。見つからない場合は{@code null}。
     */
    Specification getParentSpecification(Specification spec);
}

ふむふむ。テンプレートもSpecificationであって、Mayaaファイルに当たるものはPageらしいです。そして、Pageの親はEngine(これはdefault.mayaaに相当)なのですね!

今回は、一つのInjectionResolverにだけ別の親解決ロジックを組み込みたかったので、残念ながらこの機構は使用出来ませんでした。それではどうするかというと、少々強引ですが、メソッド一個をコピペして書き換えました。

@Override
    public SpecificationNode getNode(SpecificationNode original,
        InjectionChain chain) {
    if (original == null || chain == null) {
        throw new IllegalArgumentException();
    }
    String id = getID(original);
    if (StringUtil.hasValue(id)) {
        Specification spec = SpecificationUtil.findSpecification(original);
        SpecificationNode injected = null;
        while (spec != null) {
        SpecificationNode mayaa = SpecificationUtil.getMayaaNode(spec);
        if (mayaa != null) {
            List injectNodes = new ArrayList();
            getEqualsIDNodes(mayaa, id, injectNodes);
            if (injectNodes.size() > 0) {
            injected = (SpecificationNode) injectNodes.get(0);
            if (isReportDuplicatedID() && injectNodes.size() > 1) {
                logWarnning(id, original, 2);
            }
            break;
            }
        }
// ここを標準と差し替える。 by ishigami
//             spec = EngineUtil.getParentSpecification(spec);
        spec = MyMayaaEngineUtil.getNextEIDSpecification(spec);
// ここを標準と差し替える。 by ishigami end
        }
        if (injected != null) {
        if (QM_IGNORE.equals(injected.getQName())) {
            return chain.getNode(original);
        }
        return injected.copyTo(getCopyToFilter());
        }
        if (isReportResolvedID()) {
        logWarnning(id, original, 1);
        }
    }
    return chain.getNode(original);
    }

願うことならこの部分がprotected以上のメソッドとして提供されていたらOverrideできたので、是非ともそのようになって欲しいですね。

さて、MyMayaaEngineUtil.getNextEIDSpecification(spec);についてですが、
これは、ディレクトリのリストを取得して、アルファベット順に次のmayaaファイルを返し、次のファイルが無くなったらEngineを返すようにしています。普通のコードなのでここでは割愛します。

さあ、これで、任意のディレクトリに置いたmayaaファイルを順に読みこんでくれるようになったので、ディレクトリにmayaaファイルを分割して配置することが出来るようになりました!全部ロードするのが不都合になったらのちのちimport機構を作ればいいでしょう。

次にMayaaにこのInjectionResolverを登録しましょう。これは他の設定と同様で、src/META-INFなどの直下にorg.seasar.mayaa.provider.ServiceProviderというファイルを作成し、標準の設定ファイルから、次の部分を抜粋して書き換えます。

<provider>
    <templateBuilder
                class="org.seasar.mayaa.impl.builder.TemplateBuilderImpl">
        <resolver class="org.seasar.mayaa.impl.builder.injection.MetaValuesSetter"/>
        <resolver class="org.seasar.mayaa.impl.builder.injection.ReplaceSetter"/>
        <resolver class="org.seasar.mayaa.impl.builder.injection.RenderedSetter"/>
        <resolver class="org.seasar.mayaa.impl.builder.injection.InsertSetter"/>
        <resolver class="org.seasar.mayaa.impl.builder.injection.InjectAttributeInjectionResolver"/>
        <resolver class="org.seasar.mayaa.impl.builder.injection.EqualsIDInjectionResolver">
            <parameter name="reportUnresolvedID" value="true"/>
            <parameter name="reportDuplicatedID" value="true"/>
            <parameter name="addAttribute"
                                value="{http://www.w3.org/TR/html4}id"/>
            <parameter name="addAttribute"
                            value="{http://www.w3.org/1999/xhtml}id"/>
        </resolver>
        <!-- EID対応のため独自のものを加えている -->
        <resolver class="com.example.hogehoge.MyEqualsEIDInjectionResolver">
            <parameter name="reportUnresolvedID" value="true"/>
            <parameter name="reportDuplicatedID" value="true"/>
        </resolver>
        <resolver class="org.seasar.mayaa.impl.builder.injection.XPathMatchesInjectionResolver"/>
        <parameter name="outputTemplateWhitespace" value="true"/>
        <parameter name="outputMayaaWhitespace" value="false"/>
        <parameter name="optimize" value="true"/>
    </templateBuilder>
</provider>

この状態でWebアプリを立ち上げると、思った通りにe:idと記述した時、標準とは違う任意のファイル解決ルールでプロセッサーをひもづけることができました。しかし、まだ問題があります。今のままではmayaaファイルを変更・追加しても、Webアプリケーションを再起動するまで反映してくれません。これを対応するためには、SourceDescriptorの実装が必要ですが、長くなるので次回以降にしたいと思います。概要だけ説明すると、getTimestampをOverrideするだけです。