Hatena::ブログ(Diary)

130単位

2009-04-07

SAStruts JSONを返すアクションメソッド 修正編

SAStruts アクションにJSONを返すメソッドを作成してみる - 130単位

↑で書いたコードに対し、気になった点があって直そうと思いました。が、あまりに修正点が多かったので新たに記事にしてみます。

以前のコード

Ajaxでリクエストされ、従業員のリストをJSON形式で返すメソッドです。

@Execute(validator = false)
public String ajaxEmployeeList() {
    List<Employee> employeeList = employeeService.findByDeptId(form.departmentId);
    
    HashMap<String, String> map = new HashMap<String, String>();
    for (Employee emp: employeeList) {
        map.put(emp.id, emp.name);
    }
    
    ResponseUtil.write(JSONSerializer.serialize(map), "text/javascript");
    return null;
}

修正4点

Mapの順序を固定する

HashMapだと、値の順序が保証されません。実際にJSON文字列ではバラバラになってしまっていて、受け側のJavaScriptで扱う際にも影響が出ていました。

HashMap<String, String> map = new HashMap<String, String>();

LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();

これで、putした順序通りにJSONに変換されます。ただし期待する結果を得るためには、employeeService#findByDeptId()での照会の際にOrderBy()を利用してソートしておくことが必要です。

また、Mapにはキーの値で自動的にソートされるTreeMapという実装もあるようです。こちらなら、OrderBy()しておく必要はなさそうです。パフォーマンスも含めてどちらが適しているかは未調査です…。

インタフェース型で宣言する

変数などの宣言には、インタフェースを用いるのが良いようです。

LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();

Map<String, String> map = new LinkedHashMap<String, String>();

大きな理由としては、コードに柔軟性を持たせるためだと認識しています。『Effective Java』が詳しいです*1

JSONICを使う

前の記事でも触れましたが、JSONSerializerは@deprecatedとなっているため、JSONICを利用します。

JSONSerializer.serialize(map)

//import net.arnx.jsonic.JSON; が必要
JSON.encode(map)
Content-Typeを「application/json」にする

RFCで定められているように、"application/json"を使うことにします。

修正したコード

以上4点を修正したコードです。

@Execute(validator = false)
public String ajaxEmployeeList() {
    List<Employee> employeeList = employeeService.findByDeptId(form.departmentId);
    
    Map<String, String> map = new LinkedHashMap<String, String>();
    for (Employee emp: employeeList) {
        map.put(emp.id, emp.name);
    }
    
    ResponseUtil.write(JSON.encode(map), "application/json");
    return null;
}

ついでに

これまでの記事で、「デパート」→「部署」に変えておきした。常識的な英語表現っぽくてなんとなく恥ずかしかったので。

*1:第8章 項目52

2009-04-03

Eclipse Subversiveプラグインを導入

Eclipse ビルド・パス設定で.svnフォルダを除外する - 130単位

以前こんな記事を書きましたが、これでは対処にはなっていませんでした。ビルド・パスを変更して再ビルドしたりすると、出力側の(本来は必要な).svnフォルダが消えてしまいます。

Eclipse Subversive - SVN Team Provider Project
http://www.eclipse.org/subversive/

で、ちょうどリポジトリを整理する機会があったので、Subversionと連携できるEclipseプラグインを使い始めてみることにしました。SubclipseでなくSubversiveにしたのは、Eclipseの公式プラグインらしいから、という単純な理由です。

以下は、実利用とは別でキャプチャ用に手順をなぞったものです。間違った記述がありましたらすみません。

Eclipse 3.3

インストール

Subversiveプラグイン - EclipseWiki
http://eclipsewiki.net/eclipse/index.php?Subversive%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3

上記を参考に、

これら3つのプラグインインストールします。

利用

プロジェクトの共用

プロジェクト右クリック→チーム→プロジェクトの共用へ進み、SVNを選択します。

f:id:deeeki:20090403221131j:image

リポジトリ情報の入力

f:id:deeeki:20090403221133j:image

  1. リポジトリURLを入力
  2. 必要に応じて認証情報を入力
  3. 「ロケーション情報が誤って指定されています」とダイアログが出たりするが、とりあえずOKしてみる

f:id:deeeki:20090403221132j:image

リポジトリレイアウト設定

ルートのプロジェクトをつくり、その中で複数のプロジェクトを管理できるようにします。

f:id:deeeki:20090403221134j:image

  1. 「Advanced Mode」を選択
  2. プロジェクト・リポジトリー・レイアウトで、「指定されたルート名で複数のプロジェクト・レイアウトを使用」を選択
  3. Subversion推奨レイアウトを使用」にチェック
コミットの実行

ログメッセージを入力して、コミットします。

f:id:deeeki:20090403221135j:image

完了

パッケージ・エクスプローラの表示はこんな感じになります。

f:id:deeeki:20090403221136j:image

リビジョン番号とともに、変更の有無が視覚的にわかりやすくなっています。

アイコンの装飾については、こちらの記事を参考に。

Subversionプラグイン SubclipseSubversive - 都元ダイスケ IT-PRESS
http://d.hatena.ne.jp/daisuke-m/20071118/1195370546

雑感

  • .svnフォルダがコピーされる煩わしさから解放された
  • プロジェクトまたはパッケージ/フォルダを選択して[Ctrl]+[Alt]+[C]でコミットできるのが便利
  • コミット時、「選択されているチェックのクリア」を実行しても全てクリアされるのが不満
  • 以下は調査中
    • すでにリポジトリに存在するプロジェクトを扱う場合
    • 他の開発メンバーとの共有方法

ほか参考リンク

バージョン管理に便利なSubversiveプラグイン (1/3) - @IT
http://www.atmarkit.co.jp/fjava/rensai3/eclipseplgn15/eclipseplgn15_1.html

2009-03-17

SAStruts 例外発生でエラーページに遷移

  • SAStrutsで、例外発生時にエラーページに遷移させたい。

これが今回やりたいこと。どうやらStrutsの機能でできるようです。以下はリファレンスの引用です。

struts-config.xml

<global-exceptions>
    <exception path="/error/norole.jsp" key="errors.norole"
        type="org.seasar.struts.exception.NoRoleRuntimeException"/>
</global>
Super Agile Struts - Feature Reference

これを参考に、struts-config.xmlに以下のように記述。実行時例外が対象です。

<global-exceptions>
    <exception path="/error.jsp" key="errors.system"
        type="java.lang.RuntimeException"/>
</global>

で、適当なアクションの実行メソッドにて、0で割る処理を入れて試してみました。すると、404となってしまいました。

HTTPステータス 404 - /app/error.jsp

なので、パスの書き方を「/WEB-INF/」からに変更してみました。

<global-exceptions>
    <exception path="/WEB-INF/view/error.jsp" key="errors.system"
        type="java.lang.RuntimeException"/>
</global>

これでうまくいきました。

2009-03-11

SAStruts アクションにJSONを返すメソッドを作成してみる

部署: 従業員:

このような2つのセレクトボックスがあって、部署の選択に応じて、従業員のセレクトボックスを動的に生成したいとします。

これを、Ajaxを使ってSAStrutsで実現しようとしてました。

StringBufferの例

アクションにAjaxリクエスト用のメソッドを作成します。

※ パラメータの部署idはアクションフォームで定義されているとします。

@Execute(validator = false)
public String ajaxEmployeeList() {
    List<Employee> employeeList = employeeService.findByDeptId(form.depertmentId);
    
    StringBuilder buf = new StringBuilder(100);
    buf.append("{");
    for (Employee emp: employeeList) {
        buf.append("\"");
        buf.append(emp.id);
        buf.append("\":\"");
        buf.append(emp.name);
        buf.append("\",");
    }
    buf.setLength(buf.length() - 1);
    buf.append("}");
    
    ResponseUtil.write(buf.toString(), "text/javascript");
    return null;
}

idとnameさえ得られれば良いので、単純にStringBufferで書いてみました。最後にユーティリティクラスのResponseUtilを使ってJSONを出力しています。ちなみに、最初はgetEmployeeList()という名前にしようとしたのですが、Getterと解釈されるっぽいので違う名前にしました。

ただ、やはりどうもコードにすっきり感がありません。

で、なんとなく「json」と書いてコード補完をしてみたら、「JSONSerializer」なるクラスを発見。serialize()というメソッドがあり、どうやらこれで出来そうです。

JSONSerializerの例

追記:修正した記事↓を書きました。なお、JSONSerializerは非奨励とされています。
SAStruts JSONを返すアクションメソッド 修正編 - 130単位

※ org.seasar.framework.util.JSONSerializerをインポートします。

@Execute(validator = false)
public String ajaxEmployeeList() {
    List<Employee> employeeList = employeeService.findByDeptId(form.depertmentId);
    
    HashMap<String, String> map = new HashMap<String, String>();
    for (Employee emp: employeeList) {
        map.put(emp.id, emp.name);
    }
    
    ResponseUtil.write(JSONSerializer.serialize(map), "text/javascript");
    return null;
}

コードを短くするなら、エンティティのListをそのままJSONに変換してもいいと思います。その際エンティティに導出プロパティがあった場合、正しく定義していないと例外が発生してしまうので注意が必要です。

Content-Type

ResponseUtil#write()の第二引数にContent-Typeを指定していますが、JSONは"application/json"とするのが本来は正しいようです。

JSONのContent-Typeは application/json(管理人日記) - むぅもぉ.jp
http://muumoo.jp/news/2006/09/26/0applicationjson.html

ちなみに第二引数なしだと"text/plain"になります。"application/json"と"text/javascript"と"text/plain"、それぞれのContent-Typeを各ブラウザで試してみましたが、IE6/Firefox3/Safari/Chromeどれでも問題ありませんでした。

ところが

記事を書くにあたってリンクを張ろうとしてみたら、以下のような記述が。

推奨されていません。 このクラスの代わりにJSONIC等を使用してください

JSONSerializer (Seasar2 Framework 2.4.48 API)

JSONからの変換の際に、うまくいかないケースがあるようです。

[#CONTAINER-339] [S2Container] JSONSerializer を deprecated にしました.代わりに JSONIC 等を使用してください. - The Seasar Foundation Issues
https://www.seasar.org/issues/browse/CONTAINER-339

参考リンク

というわけで、JSONICを使うように修正したいと思います。以下参考記事。

suz-lab - blog: S2JDBC - SAStruts - JSON - jQuery
http://suz-lab.blogspot.com/2008/09/s2jdbc-sastruts-json-jquery.html

SAStruts + JSONIC でバリデーションエラーをJSONで返す : json, sastruts, seasar | メモリークラフト
http://www.memorycraft.jp/2008/11/sastruts-jsonic-json.html

2009-03-10

S2JDBC テーブルを利用した独自仕様のid採番メソッド

AS400のDB2では、通常ではトランザクションが扱えないようです。ただし、ジャーナル処理を設定すれば扱えるようになるみたいです。*1

で、今回の案件。

  • ジャーナルは作成しない
  • IDはテーブルを使って採番

という方針があり、それと関連してFOR UPDATEについてあれこれ調べたりしてました。

DB2で「SELECT ... FOR UPDATE」のロックを検証 - 130単位
S2JDBC DB2DialectでのforUpdate()を調べてみた - 130単位

さて、Javaには「synchronized」という修飾子があるそうです。これをメソッドの宣言に記述すれば、排他処理が行えるとのこと。

Java スレッドの排他制御 synchronizedメソッド‐ニコニコ動画(ββ)
http://www.nicovideo.jp/watch/sm2346912

というわけで、S2JDBCのサービスにid採番用のメソッドを書いてみます。

  • 採番用テーブルのカラムはkeyとvalueのみ
  • エンティティを作成してSQL自動生成で記述
  • 各サービスから扱えるようにAbstractServiceに定義
  • 最初の採番時、対象キーのレコードが無くても動作可
  • valueカラムの現在値=次に採番される値

こんな感じの仕様です。

public abstract class AbstractService<ENTITY> extends S2AbstractService<ENTITY> {

    public synchronized int generateId(String key) {
        int id;

        IdGenerator idGen = jdbcManager
            .from(IdGenerator.class)
            .where("key = ?", key)
            .getSingleResult();

        if (idGen == null) {
            id = 1;

            idGen = new IdGenerator();
            idGen.key = key;
            idGen.value = 2;
            jdbcManager.insert(idGen).execute();
        }
        else {
            id = idGen.value;

            idGen.value = id + 1;
            jdbcManager.update(idGen).execute();
        }
        return id;
    }
}

で、synchronizedが実際に効くのかどうか試してみましたが、いまいちわかりませんでした。

  1. トランザクションは無しに設定
  2. メソッドの採番中にユーザーAのみ分岐されるThread#sleep()を仕込む
  3. ユーザーAにて採番実行
  4. ユーザーBにて採番実行

こんな手順でやってみたのですが、synchronizedがない状態でも3の時点で止まってくれました。おそらく検証方法がまずいのか、理解が足りないかのどちらかかと思われますが…。考えてもわからないので、ひとまず一区切りとしたいと思います。

*1:間接的に触れているだけなので、曖昧な書き方ですみません