Category Archives: Java

そろそろ2年間Mayaa使ってわかったことを書く

僕は、2年ほど前に、所属している会社の主要プロダクトであるのリニューアルに係わりました。その際、Mayaaを採用し、今日までにそれを運用してノウハウが蓄積してきたので、ここに発表しようと思います。

経緯

私たちはコンシューマ向けのビューを多く扱うので、プログラミング工数を要せずデザイナーがダイレクトにデザインをカスタマイズできる仕組みの実現が急務でした。

以前はテンプレートにJSPを採用していたため、デザインを作る(HTMLを書く)デザイナーと、プログラムに打ち込むJSPコーダーが必要という状況でした。一方ブログサイトなどでは、CSSレベルでのデザインカスタマイズをサポートしていることは多いですが、それでは自由度に限りが出てしまいます。フルHTMLレベルでのテンプレートエンジンが必要でした。

結果的に、この状況がどのように変化したのかは下記のエントリにも書きましたので、こちらも合わせて読んでいただけると幸いです。

プログラマーとデザイナーの境界が縮まってきた気がする

今回は技術よりのこと、及び運用していく上で培ったノウハウを紹介したいと思います。

JavaServerTemplates "Mayaa" とは

前置きが長くなってしまいましたが、自由なテンプレートの実現のため、Mayaaというライブラリを採用しました。

http://mayaa.seasar.org/

MayaaはSeasar2系の日本発オープンソースライブラリですが、Seasar2に関係なく使用することができます。JSPと同じレイヤーで動作し、JSPのようにServletからforwardして使うことができます。以下のページの図が分かりやすいです。

http://mayaa.seasar.org/documentation/about.html

Mayaaを導入する以前のテンプレートエンジンはこんな感じでした。

<% if (a != null) { %>
hello, <%= a %>
<% } %>

同じようなテンプレートをMayaaで書くとこんな感じです。

<div m:id="IF_A_EXIST">
    hello, <span m:id="A_HERE">dummy</span>
</div>

という感じで、普通のHTMLにm:idという属性が付いていますね。このようにしておくと、テンプレートファイルをそのままブラウザでプレビューすることが可能ですし、HTMLエディタ、例えば Dreamweaver で編集することができます。

Mayaaのすばらしさ

「テンプレートファイルをそのままブラウザでプレビューすることが可能です」といっても、簡単なようで簡単なことではありません。例えば画像の相対パスです。

<img src="images/xxx.jpg" />

ローカルではこのようになんの変哲もなく動く感じですが、サーバにデプロイしてみるとうまく動かないかもしれません。例えば、テンプレートは"view/hello.html"だけど、URLマッピングは別に管理されていて、"action/hello.do"かもしれません。そうすると、上記部分は

<img src="../view/images/xxx.jpg" />

<img src="http://something.example.com/view/images/xxx.jpg" />

のように書き換えるか必要があります。しかし、このようにしてしまったら、今度はローカルで画像が見られなくなったりします。Mayaaにはこの問題を解決するために、PathAdjusterという機構があります。

http://mayaa.seasar.org/documentation/path_adjust.html

詳しくは上記リンクに任せて割愛しますが、大ざっぱに言うと、一番目の記述を三番目の記述に実行時に書き換えてくれる機構です。これを応用すると、例えば「画像パスにtimestampを付与してmod_expireと組み合わせたクライアントキャッシュ対応」なんてこともできてしまいます。

その他にも、Webのフロントとして使う上でかゆいところに手が届く機能が結構付いているので気に入っています。

はまったこと

とはいえ、ここまで順風満帆というわけではありませんでした。例えばこんなころにハマりました。

最初が取っ付きにくい

よく言われます。これが故に、敬遠する方も多いようです。しかし、慣れてみると何も苦はなく、もうJSPには戻れません。ドキュメントが非常によく整備されていますので、とっつきやすさで言えば取っ付き易いほうだと思います。

とはいえ教育コストがかかります(mayaaファイルを覚えなければならない)

mayaaファイルという聞きなれない独自のファイルの記述が必要です。しかも、これを書くには、幾つかあるmayaaプロセッサーをマスターしていないと心もとないところです。

http://mayaa.seasar.org/documentation/processor_reference.html

そこで、mayaaファイルを書ける技術者の数は限られてしまいます。そのためには、「なるべくテンプレート(HTML)側に制御を委ねる」アプローチが必要かなと思います。(詳しくは後ほど書きます)

JavaScriptが気持ち悪い

mayaaでは、Javaオブジェクトとテンプレートとの橋渡しにJavaScript(Rhino)を使用します。Javaとそのまま埋め込めるJSPとは異なるので、「Javaのように見えて少し違う」書き方をしなければなりません。Javaだと思って書いていると、うっかり思いもよらないバグを作ってしまってハマります。これに対するアプローチは以下の二つかなあと思っています。

  • 出来る限りJavaに委譲する
  • 出来る限りJavaScriptに統一する

従来ならば、前者の書き方が理想だと思われますが、そのような方法を2年ほどやってきた結果、少々冗長さを感じています。ちょっとした変更でもサーバの再起動が必要なのも億劫です。(本番環境だと簡単に再起動できませんしね?)今はサーバーサイドJavaScriptも広く利用されています。今後は後者のアプローチも有効ではないかと思っています。 実際にやってみて失敗でした。JavaScriptのビルドや実行はJavaより遅いので多用するとパフォーマンス上の問題があります。JavaScriptはなるべく使わず可能な限りJavaに寄せるのが正解です(2014/07/02 追記)

また、別件ですが、mayaaファイルはXMLなので、以下のような冗談みたいな記述もあります

<m:if test="${hoge &gt; 0}">

本当はif (hoge > 0)と書きたいところなのですが、mayaaがxmlファイルなので、">"が書けずこのような書き方になってしまいます(笑)どうしても">"と書きたい場合は、""の中をCDATAにすればいいのですが、

<m:if test="${<![CDATA[hoge > 0]]>}">

さらに冗長になっちゃいました(笑)

負け惜しみとしてはmayaaのおかげで鍛えられて、もうltとgtがどっちかなんて迷うことはなくなりました(笑)

私たちが採用したノウハウ集

実際に使っていて培ったノウハウを発表します。

m:idに命名規則を使用する

mayaaには数多くのプロセッサーが存在しますが、HTML側から見ると、下記の4種類に集約されることがわかりました。

  • 変数を出力する
  • 属性を書き換える
  • 条件を満たす場合のみ表示する(条件を満たさなければ消える)
  • 特定の条件のもと繰り返す

それぞれを、"〜HERE"、"〜_TAG"、"IF〜"、"LOOP_〜"と命名するように統一しました。このようにすると、こんなこともできてしまいます。

Mayaaファイルを命名規則から一括作成するEmEditorマクロ

デザイナーとプログラマーで使う名前空間を分ける

Mayaaでは、動的なタグの紐付けにHTMLの"id"属性を使用できますが、"m:id"のように独自の属性を使用することができます。まずは有無を言わずそちらを採用します。

http://mayaa.seasar.org/documentation/equals_id_resolver.html

それでもどうしても、id属性やclass属性を使用したくなることがあります。これはMayaaに限らず一般論ですが。そこで、「プログラマーは全て大文字を使う」「デザイナーは全て小文字を使う」などの棲み分けをすると良いと思います。

xpath指定は使用しない

Mayaaではidの代わりにxpath指定でタグをコントロールすることができます。

http://mayaa.seasar.org/documentation/xpath.html

が、この方法は緊急時を除いて使用するべきではありません。あとからどうにもならなくなって泣けてきます。。。

また、パフォーマンス上の問題もありますので、できれば、templateResolverリストから外してしまうことをおすすめします。(2014/07/02 追記)

デバッグ用関数は値を返す

こんな関数を用意してdefault.mayaaから呼び出すようにするとデバッグで便利です。Javaのような豪華なデバッガがあれば本当はいいんですけどね

function debug(value, tag) {
    if (tag == null) tag = 'debug';
    java.lang.System.out.println(tag + "t" + tag);
    return value;
}

プログラムは文言を吐き出さない

以下のような書き方は、文言の変更にプログラムの修正が必要となってしまいます。

性別:<span m:id="USER_SE×_HERE">男性</span>

※上記コードは、中学生並みの恥ずかしがりではなく、変なサーチキーワードに引っかからないように一部改変しています。

例えば、「男性」という文言ではなく、画像にしたいかもしれません。英語化するかもしれません。以下の書き方が良いです。

性別:<span m:id="IF_USER_MAN">男性</span><span m:id="IF_USER_WOMAN">女性</span>

id一覧表を自動生成する

mayaaファイルはxmlファイルなので、比較的簡単にパースできます。XMLコメントもパースの対象にできるので、それを元にid一覧表を作るのは容易です。JavaDocを自作するイメージです。(ソース載せようとしたらちょっと長いので割愛します。SAXを使うとスムーズに実装できました。

http://www.atmarkit.co.jp/fxml/rensai/xmljava04/xmljava02.html

また、ドキュメントの出力部分では、以下を参考に、ここでもMayaaを使用しました(笑)

http://d.hatena.ne.jp/terazzo/20071116/1195250832

PathAdjusterをうまく使う!

Webサイトですから、ハイパーリンクを多用します。この時、いちいちリンク一つ一つにm:idを発行していたら大変です。そういう時は、PathAdjusterでなんとかしましょう。僕は、adjustRelativePathのオーバーライドで、拡張子が".html"の場合はsuper.adjustRelativePath(base, path)に引き継がず、独自で処理するようにしました。結果的に、リンクは以下のように書くことになっています。

<a href="member_input.html">会員登録</a><!-- 実行すると、servlet用のURLに変換される -->

このように、PathAdjusterをうまく使うことで、発行するidの数を減らすことは非常に強力です!

一つのmayaaファイルで複数のテンプレートを使い回す。

Mayaaは設定ファイル

  • org.seasar.mayaa.provider.ServiceProvider
  • org.seasar.mayaa.source.PageSourceFactory

などの記述で非常に柔軟にエンジンをカスタマイズできます。

前述したとおり、mayaaファイルはできるだけ少なくしたほうが得策です。そこで、一つのページをベースに複数の派生ページがデザインレベルで作れると幸せです。以下のエントリでその試みを成功させました。

Mayaa ファイルの共通化

そうこうしていると、「派生したページでちょっとだけm:idを追加したい」なんていう要望も発生してしまいました。これについても、EqualsIDInjectionResolverのカスタマイズで対応することができてしまいました。

http://ml.seasar.org/archives/mayaa-user/2010-January/000893.html

これは、当時での実現方法ですので今は、https://www.seasar.org/issues/browse/MAYAA-76
が実装されたのでさらに簡単にできると思います。

m:idの数を減らす

例えば、「名前の頭30文字を出力して残りは...にする」「名前の頭60文字を出力して残りは...にする」「名前の頭60文字を出力して残りは...にする」のように、パラメータが存在する制御をしたい時があります。そんなとき、

  • NAME_30_HERE
  • NAME_60_HERE

のようにいちいちつくっていたらヤボです。はじめのうちは本当にそんな感じで対応していたためミスもありました。

今は以下の方法でパラメータ化を実現しています。

function $p(name, defaultValue) {
    var attr = originalNode.getAttribute(
        Packages.org.seasar.mayaa.impl.engine.specification.SpecificationUtil.createQName(name)
    );
    if (attr != null) return attr.value;
    attr = originalNode.getAttribute(
        Packages.org.seasar.mayaa.impl.engine.specification.SpecificationUtil.createQName(originalNode.getDefaultNamespaceURI(), name)
    );
    return defaultValue;
}

テンプレート側では、以下のように書けばOKです。

<span m:id="FREE_HERE" m:free_no="20">自由項目20番</span>

m:を抜かすと実行後のマークアップにfree_noが残ってしまいますのでつけることをおすすめします(2014/07/03 追記)

コメントはこう書け!

JSPだとこんな風なコメントが使用できました。

<%-- ほげほげ --%>

開発が切羽詰ってくるとコメントも荒れてきます。うっかり外に出してはまずい言葉を書いてしまうかもしれません。JSPコメントならば出力されないのでセーフですが、mayaa+HTMLだと、この書き方ができないので、うっかりテンプレートに開発者しかしらない内部情報を書いては大問題です。そんな時はこう書きましょう。

<!-- ${/* FIXME:ここ意味がわからないけどなぜかそれっぽく動く>< */} -->

これでブラウザには出力されなくなります(笑)

最後に

今どきUIに強いプログラマー=モテです。さあ、ここまで読めば、君も立派なMayaa使い!

ちなみに、弊社の求人はこちらです↓

http://www.interfactory.co.jp/recruit/index.html

(2011/07/16)書き忘れたことを別エントリで追記しました

2年間Mayaaを使ってわかったこと その2

この記事は以前多くのブックマーク・コメントを頂きました。

これからブックマークされる方はこちら↓

TreeSetのComparatorではまったのでメモ(初心者向け)

今更感のある話題ですが、初心者=僕が、ドハマリして、いろいろ面白い現象に遭遇しました。
内容はいたって基本中の基本のため、仕事でプログラミングしている者としては恥ずかしい限りですが、後に同じようにハマる人がいたときのために、メモを残します。

やろうとしたこと:

もともとこんなコードがあって

set = new TreeSet();
for (DBの取得結果) {
set.add(取得した値);
}

この後、setをつかって、UIを生成していたのだけど、順番が違うよって指摘を受けた。DBには、よくある、「表示順」のカラムがある。SQLならorder byで取れる。TreeSetの場合はCompatatorを渡せば良い。ということで、こんなHashMapをつくって

Map<String, Integer> idToSortOrderMap = new HashMap<String, Integer>();
for (DBの取得結果) {
idToSortOrderMap.put(id, order);
}

みたいにして。こんなstatic内部クラスを作る

private static class MyComparator implements Comparator, Serializable {
private HashMap<String, Integer> idToOrderMap;
public MyComparator(HashMap<String, Integer> idToOrderMap) {
this.idToOrderMap= idToOrderMap;
}
public int compare(String o1, String o2) {
Integer i1 = idToOrderMap.get(o1);
Integer i2 = idToOrderMap.get(o2);
if (i1 == null) { return 1; }
if (i2 == null) { return -1; }
return i1 - i2;
}
}

これで一見すると、表示順は正常で、あたいもちゃんと追加されているので、完了…そう思っていました。

問題発生

ところが、例えば

idToOrderMap.put("hoge", 1);
idToOrderMap.put("hoge2", 2);
idToOrderMap.put("fuga", 1);

のとき、

set.add("hoge");
set.add("hoge2");
System.out.println(set.contains("fuga")); // falseのはず

ところが、結果はtrueになってしまいます。

問題の原因

初歩的なことですが、Java APIリファレンスに、
http://java.sun.com/javase/ja/6/docs/ja/api/java/util/TreeSet.html

あるセットが Set インタフェースを正しく実装するには、明示的なコンパレータが提供されているかどうかにかかわらず、そのセットによって維持される順序付けが「equals との一貫性」のあるものでなければいけないことに注意してください。

http://java.sun.com/javase/ja/6/docs/ja/api/java/util/Comparator.html

たとえば、(a.equals(b) && c.compare(a, b) != 0) である 2 つの要素 a および b をコンパレータ c で空の TreeSet に追加すると仮定します。a と b はツリーセットの点から見て等価ではないため、2 番目の add オペレーションは、Set.add メソッドの仕様とは異なる場合でも、true を返し、ツリーセットのサイズは大きくなります。

と記述されています。

噛み砕いていうと「compareの結果が0と、equalsが同値でないと、TreeSetは異常な動きをするよ」というところです。

Effective Java

を持っている人は、Effective Javaの第2版 「項目12 Comparableの実装を検討する」に関連する記述があります。

はまりどころは、compareとequalsの一貫性は、必須ではないというところです。しかし、TreeSetやTreeMapのようにこの一貫性を要求する実装クラスがあるということです。

完成コード

上記クラスの修正版は以下のとおりです。

private static class MyComparator implements Comparator, Serializable {
private HashMap<String, Integer> idToOrderMap;
public MyComparator(HashMap<String, Integer> idToOrderMap) {
this.idToOrderMap= idToOrderMap;
}
public int compare(String o1, String o2) {
if (o1 == null) {
return o2 == null ? 0 : -1;
}
Integer i1 = idToOrderMap.get(o1);
Integer i2 = idToOrderMap.get(o2);
if (i1 == null) {
if (i2 != null) {
return -1;
}
return o1.compareTo(o2);
}
if (i2 == null) {
return 1;
}
if (i1.equals(i2)) {
return o1.compareTo(o2);
}
return i1.compareTo(i2);
}
}

2011/06/24 修正

ソースを修正しました。修正した箇所は、

if (i1 == i2) {

の箇所で、i1,i2がintではなく、Integerだったので、値の比較ではなく、オブジェクトの比較がされていました。equalsを使うべきでしたが、そうすると、nullの場合にヌルポになるので、上記の対応になります。やりたいことは最終行の

return i1.compareTo(i2);

なんですよね。Javaが冗長とか言われるのも、こういったNull対策があったりとかするところなんでしょうね……。

まあ、SQLでもこう言うのありますけどね。

JavaでPDF生成する方法(LibreOffice, jodConverterによる方法)

JavaでPDFを生成する場合、こういった方法や、http://allabout.co.jp/gm/gc/80691/ こういった方法http://www.atmarkit.co.jp/fjava/javatips/121jspservlet41.html がある。しかし、どちらも低レベル過ぎて、美しいビジネス文書や帳票を出力するにはワープロを作るくらいの気合が必要となる。

手っ取り早いのは、ExcelやWordの文書をApache POI経由で編集し、そいつをPDFにして出力出来れば、美しいPDFが任意のテンプレートで作れるというプランである。

最近のOfficeならPDF出力をできるので、もしMSマンセーな組織なら、Windowsサーバ上でExcelを常駐させて、COMなんちゃらを利用して実現するのが良いと思う。しかし僕はJava屋だ。サーバはLinuxが好きだ。というわけで、今回は、以下の組み合わせで実現したので、ポイントを抜粋して紹介します。

なお、POIのことについては割愛します。
まずはPOIなので、HSSFWorkbookを生成します。

final HSSFWorkbook book = new HSSFWorkbook(templateStream);

そして、bookを使って色々操作します(w
最後にoutputStreamをbook#writeに渡せばExcelファイルが出力されます。(ここまでは普通のPOIです)

book.write(out);

次はPDF化です。LibreOfficeをインストールして、インストールディレクトリの
program/soffice (Windowsの場合はsoffice.exe)に、次のオプションを付けて起動します。

 -accept="socket,port=8100;urp;"

これでソケット通信を受け付けるようになります。

次に、jodConverterを用意しましょう。http://sourceforge.net/projects/jodconverter/files/JODConverter/2.2.2/ から jodconverter-webapp-2.2.2.zip をダウンロードして解凍します。zip中身にwarが入っているので、そいつをtomcatのwebappの下に配置したらすぐに使用できるようになります。

次に、アプリとjodConterverの連携です。幸い、jodConverterにはWebサービス機能があるのでそれを使うのが良さそうです。
ぐぐっても見つからないと思ったら本家に書いてありました(笑)
http://www.artofsolving.com/node/15

ということで、ざっくりこんな感じでユーティリティメソッドを作ります。

public int convertExcepToPdf(final InputStream in, OutputStream out) throws IOException {
    HttpClient httpClient = new HttpClient();
    PostMethod post = new PostMethod("http://localhost:8080/jodconverter-webapp-2.2.2/service");
    post.setRequestHeader("Accept", "application/pdf");
    post.setRequestHeader("Content-Type", "application/vnd.ms-excel");
    post.setRequestBody(in);
    try {
        int status = httpClient.executeMethod(post);
        if (status != HttpStatus.SC_OK) {
            return status;
        }
        InputStream response = post.getResponseBodyAsStream();
        byte[] buf = new byte[256];
        for (int len; (len = response.read(buf)) != -1;) {
            out.write(buf, 0, len);
        }
        return status;
    } finally {
        post.releaseConnection();
    }
}

これで接続ができますが、POIのwriteはOutputStream、今回作った関数のパラメータはInputStreamという問題が残ります。
ByteArrayInputやOutputStreamを使ってメモリに全部記憶すればできますが、Excelファイルも大きめのファイルなので、メモリは節約したいです。かと言って、一旦ファイルに保存するのはなんともダサい感じです。HDDガリガリとかナンセンスです!

悩んだ結果、僕はPipedOutputStream, PipedInputStreamを使用しました。

final PipedOutputStream pipeout = new PipedOutputStream();
PipedInputStream pipeIn = new PipedInputStream(pipeout);
Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            try {
                book.write(pipeout);
            } finally {
                pipeout.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
t.start();
PDFConverterConnector pdfC = new PDFConverterConnector();
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
try {
    pdfC.convertDocumentFileToPdf(pipeIn, out);
} catch (Exception e) {
    e.printStackTrace();
}

無事PDFが出力されたのを見ると感激しますね!

2011/05/26 追記

本番環境にインストールするに当たって、CentOS5に入れたのですが、バージョンが古くてLibreOfficeではなく、OpenOffice.orgになってしまったのと、コマンドラインから実行すると、

Set DISPLAY environment variable, use -display option or check permissions of your X-Server (See "man X" resp. "man xhost" for details)

というようなエラーが出てしまいました。これは、

yum install openoffice.org-headless

とすればOKでした。
参考:http://stackoverflow.com/questions/4004456/centos-server-openoffice-headless

追記(2014/04/26)

ここではJodConverter2を使っていますが、OpenOffice, LibreOffice側にメモリリーク問題があり、デーモンとして起動したOOo(LibreOffice)がクラッシュしてしまう問題があります。実際に私の現場でも、容量の大きいExcelファイルを連続して扱うとクラッシュしてしまう現象を観測し、現在は、JodConverter3に移行しています。

JodConverter3については、下記の記事を参照してください
https://code.google.com/p/jodconverter/wiki/WhatsNewInVersion3

JodConverter3はずっとbetaで開発が止まっていて作者によると

I started this project back in 2003, but I am no longer maintaining it. I moved the code here at GitHub in the hope that a well-maintained fork will emerge.

という立場のようです。すでに多数のforkされてpull requestもあるようです。
https://github.com/mirkonasato/jodconverter

私の現場では最新betaを使用していますが、とりたてて問題は発生していません。ただ、jodConverter3には便利なWebサービス機能がありませんので、自前でJodConnver2のものを元にWevServiceを作成しました。
その辺りを後日紹介したいと思います。

この記事は以前多くのブックマーク・コメントを頂きました。

これからブックマークされる方はこちら↓