Home > Java

Java Archive

さりげなく安全ではないMavenへの対策


将来的には安全になる様子。誰が署名するのってもあるけどとりあえずは。

完成、開発環境紹介


最後のフェーズまで実装して完成。あとは考慮漏れがないことを祈るだけ。開発期間は認証周りなどは以前作ったものを流用してたので1週間ちょい。今回使ったものは次のとおり。

  • 開発環境
    • Jiemamy: テーブル設計 (今回はERダイアグラムエディタとテストデータ管理としてしか使っていない)
    • H2 Database: 初期開発環境でのデータベース、初回リリース後は本番環境のPostgreSQLを使用して開発
    • S2JDBC-Gen: DDL -> Entity, Names, Service
    • Maven: ライブラリ管理、S2JDBC-Gen実行、warファイル生成
  • 作成した Web アプリケーションの環境
    • S2Container: DI x AOPコンテナ
    • Cubby: Web アプリケーションフレームワーク
    • Mayaa: Web テンプレートエンジン
    • S2JDBC: O/R Mapper (初期開発時: H2 Database, 初回リリース後: PostgreSQL)
    • S2Directory: O/D Mapper (開発環境: OpenDS?, 本番環境: OpenLDAP)
  • 本番サーバ環境
    • Apache: 2.2.x系なのでmod_proxy_ajp使用
    • Tomcat: アプリケーションサーバ
    • PostgreSQL: 本番環境データベース

S2JDBC-Genは最初Mavenと別にライブラリ管理するのに戸惑って使うつもりなかったもののid:j5ik2oさんのとこでMavenと連携させる例があって取り入れました。結果これが良くて、さらに途中で新バージョンも出ちゃったりして、既に作ってたServiceを移植してすべてタイプセーフに流れるようなインターフェース使うようになりました。第1フェーズ終えた後に、既にデータ入ってたテーブルを直さないと後のフェーズで処理が面倒になる箇所があって直したけど、タイプセーフだったおかげですぐに直すべきコード中の箇所も判り、かなり助かりました。

今回ほとんどS2JDBCの話題しか日記には書いてなかったけど、Jiemamyはじめ毎度ですがS2ContainerのHOT deploy、Cubby、Mayaaも超役立ってて、これらどれが欠けてても期間的にやばかった。

それぞれについて多少何か書くネタが出来たので気が向いたら日記に書いていくかも(^^)。

第1フェーズ完了


突貫工事だった割にエラー出ることなく第1フェーズ無事完了したようです。残りはフェーズ的には3つ。最後のフェーズでの動作機能はまだ完成していない。

1つ判ったのは、データベースでデータの依存関係を表せてないテーブル設計は後々プログラムでとても面倒な処理をしなければならないことになる。単に期間を表すテーブルだけでなく、その前後の繋がりや階層表現もデータベースの中で表現した方が良さそう。

今回それぞれのフェーズに対応する期間を表すテーブルはあったものの、前後関係をデータベースに持たせていなかったので面倒な判定処理が増えてしまった。フェーズというのはワークフローなので、escafeFlow使うと楽できそうなものの、サンプル動かしてもイマイチ判らず時間もなかったのでまた次回に。

間一髪・・・


あやうく予告してあったURLにPDFファイルを置いといて、「これを印刷して手で書いて出してください」っとなるとこだった。。ここ3日間必死になって作ってたWEBアプリの第1フェーズが完成し、利用開始3時間前にリリースという危ない橋を渡った(汗。次また3日間で全部完成予定。

最後の最後、利用開始6時間前ぐらいにH2 Database相手にしかテストしてなかったアプリを本番DBであるPostgreSQLにして突撃したらエラーが出てもうダメかと思った。。

エラーはPostgreSQLで text 型を使っているフィールドに対応するEntityクラスのフィールドに @Lob があったのがいけなかったらしい。それで、今あらためて検索してみたらkoichikさんが関連してる回答をされていた

  • 例外
org.seasar.framework.exception.SQLRuntimeException: [ESSR0072]SQLで例外(SQL=[], Message=[Bad value for type int : user1], ErrorCode=0, SQLState=22003)が発生しました
    at org.seasar.extension.jdbc.query.AbstractQuery.handleResultSet(AbstractQuery.java:443)
    at org.seasar.extension.jdbc.query.AbstractSelect$6.handle(AbstractSelect.java:413)
    at org.seasar.extension.jdbc.query.AbstractSelect$6.handle(AbstractSelect.java:410)
    at org.seasar.extension.jdbc.query.AbstractSelect$1.handle(AbstractSelect.java:283)
    at org.seasar.extension.jdbc.query.AbstractSelect$1.handle(AbstractSelect.java:281)
    at org.seasar.extension.jdbc.manager.JdbcContextImpl.usingPreparedStatement(JdbcContextImpl.java:144)
    at org.seasar.extension.jdbc.query.AbstractSelect.processPreparedStatement(AbstractSelect.java:278)
    at org.seasar.extension.jdbc.query.AbstractSelect.processResultSet(AbstractSelect.java:407)
    at org.seasar.extension.jdbc.query.AbstractSelect.getSingleResultInternal(AbstractSelect.java:232)
    at org.seasar.extension.jdbc.query.AbstractSelect.getSingleResult(AbstractSelect.java:177)
    at org.example.hoge.entity.AdminUserTest.testFindById(AdminUserTest.java:32)
    ...
  • 対応するEntityのフィールド
/** uidプロパティ */
@Lob
@Column(length = 2147483647, nullable = false, unique = true)
public String uid;

このEntityはS2JDBC-Genで生成したものですが、ソース見てみたら、org.seasar.extension.jdbc.gen.internal.dialect.PostgreGenDialectの225行目でtext型の時はlobをtrueにしているようです。

private static PostgreColumnType TEXT = new PostgreColumnType("text",
               String.class, true);

これをfalseにすれば良いんだろうけど、そうすると、EntityからDBスキーマを生成する時に影響してしまうのかな?

また、そうしたらそうしたらで、H2 DatabaseのCLOB使ってる属性に対応するEntityには@Lobが付くことになるのに、それをそのままPostgreSQLで使えば結局エラーが出るので、そもそも異なるDBサーバをいったりきたりして開発するのが間違いなのかもしれない。

とりあえず仮眠してからまた考えよう。

  • [追記 11:46]
  • DBスキーマ
CREATE TABLE admin_user (
  id BIGSERIAL NOT NULL PRIMARY KEY,
  uid TEXT NOT NULL UNIQUE
);
  • レコード例
INSERT INTO admin_user (id, uid) VALUES (1, 'user1');
  • Entity全体
@Entity
public class AdminUser {

    /** idプロパティ */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(precision = 19, nullable = false, unique = true)
    public Long id;

    /** uidプロパティ */
    @Lob
    @Column(length = 2147483647, nullable = false, unique = true)
    public String uid;
}
  • 例外が発生するテストケース例
    • uidの@Lob外すと例外出ずに正常に実行されるようになります。

public void testFindById() throws Exception {
    jdbcManager.from(AdminUser.class).id(1L).getSingleResult();
}
  • 実行されたSQL
select T1_.ID as C1_, T1_.UID as C2_ from ADMIN_USER T1_ where T1_.ID = 1

[追記 10/14 11:45]

本件、id:taediumさんとこのPostgreSQLのTEXT型はCLOBか否か?に続きがあり、PostgreSQLのTEXT型では@Lobを付けてはいけないということになりました。修正ありがとうございました。

コメントが付けられなかった件、tDiaryの「ツッコミ中でURLを表す文字の占める割合が70%より高いものはspamとみなす」という設定に引っかかってしまったのではないかと思います、すいません。数値を30%に変えておきました。

[追記 10/29 0:39]

すこし時間空いてしまいましたが、id:taedumさんとこのPostgreSQLのTEXT型はCLOBか否か? その2より、@LobつきStringをOID型ではなくTEXT型にマッピングすることになったそうです。

S2JDBCで複雑なSQLもタイプセーフに流れるようなインターフェースで書いた方が良いと思う理由


昨日書いたS2JDBCを使った複雑な↓のクエリ

public ThesisProfUser findByUidAndYear(String userId, int year) {
    return select()
        .leftOuterJoin(year())
        .leftOuterJoin(profUser())
        .leftOuterJoin(thesisAssignmentList())
        .leftOuterJoin(thesisAssignmentList().thesisStudentUser())
        .leftOuterJoin(
            thesisAssignmentList().thesisStudentUser().studentUser())
        .leftOuterJoin(thesisAssignmentList().assignmentVoteList())
        .where(
            new ThesisProfUserCondition().profUser().uid.eq(userId).and(
                new ThesisProfUserCondition().year().year.eq(year)))
        .getSingleResult();
}

これだけ見たらSQL生で書いたり、外だしSQLファイルで書いた方が良いと思う方もいると思います。てなわけで、複雑なSQLも↑のようにタイプセーフに流れるようなインターフェースで書いた方が良いと思う理由を書いときます。とても簡単な理由です。

  • DBスキーマを変更してEntityやNamesをS2JDBC-Genで生成し直すと、DBスキーマの変更に応じてコード中のどこを変更すべきかをコンパイラ(Eclipseなど)が「エラーとして教えてくる」

これです。属性名が1つ変わっただけでもコード中から参照している箇所を全部自力で探し出すのはツライですよね。1からコードを書く時に流れるように次々とEclipseが補完してくれるという大きなメリットもありますが、もっと重要だと思うのが↑です。

このことに関しては、S2JDBC-Gen開発者のtaediumさんがプッシュされているS2JDBC-Genのコンセプト機能?であるEntityからDBスキーマを生成する機能ではさらに一歩先をいっていると思います。スキーマから生成して後からエラー箇所を直すより、先にEclipseのRefactorでEntityを修正させてしまえば、1発でコード中のほとんどの箇所を直せてしまいます(JSPやMayaaなどのビュー部分で参照している箇所も一緒にRefactorされればさらに良い。。)。ただ、単純な修正ならこっちの方が確実で早いですが、テーブル構造的に大きく変えるとなると、リレーション部分をグラフィカルなERダイアグラムで直した方が全体が見通せて考えやすいかなとも思います(てなわけで、EntityをグラフィカルなERダイアグラムとして表示して編集できるPluginがあれば、EntityからDBスキーマを生成する機能が爆発的に使われるかも?)。

ちょっと脱線しましたが以上のような理由で、手元にあるコードではすべてのクエリをS2JDBCを使ったタイプセーフに流れるようなインターフェースで書いています。もしSQLファイルを使う例外があるとしたら、ぱっと思いつく中では次の2ケースぐらいでしょうか。

  • DBに定義してある特殊なストアドプロシージャを使いたい
  • DBが持つ独自関数を使いたい

でも、S2JDBCでいくらDBを簡単に扱えるようになってもまだまだ開発上で解決しない問題があります。それは、S2JDBC関係なく、根本的にDBスキーマの設計がダメダメなら欲しいデータをDBから取得するのはコストが掛かるし、コンパイラがエラー箇所を教えてくれるとはいえ構造的に大きく変更になればロジック部分も考え直さないといけなく、変更コストはやっぱし甚大。

というわけで、今使っているDBスキーマも修行が足りないとひしひし感じているので、もっと知識を集めて良くしたい。

Namesにjoinクエリ用文字列 -> Seasar2.4.30とS2JDBC-Gen 0.9.2で解決


whereはConditionで辿れるけど、そのConditionが使うjoinが辿れないので、S2JDBC-Genで生成されるNamesにそのテーブルから繋がっているテーブルすべてを網羅するjoinクエリ用文字列があれば便利かも。

"table1List"
"table1List.table2List"
"table1List.table2List.table3List"
  • [追記: 19:53]

ひがさんがコメントくださったように、書いた直後に新しくリリースされたSeasar2.4.30とS2JDBC-Gen 0.9.2で解決しました。しかも、流れるように書けるというもっといい感じに解決。

  • 利用例
    • leftOuterJoinの中は、static importsされたものでS2JDBC-Gen 0.9.2が生成するNames(例ではThesisStudentUserNames)に定義されている
    • whereのConditionもS2JDBC-Gen 0.9.2が生成している
    • orderByの中のdesc()は、org.seasar.extension.jdbc.operation.Operationsが持っている

public ThesisStudentUser findByUidOrderByYear(String uid) {
    return select()
        .leftOuterJoin(year())
        .leftOuterJoin(studentUser())
        .leftOuterJoin(thesisAssignmentList())
        .where(new ThesisStudentUserCondition().studentUser().uid.eq(uid))
        .orderBy(desc(year().year()))
        .getSingleResult();
}

すごい綺麗になった気がします。ただ次のようにConditionをandでつなぐケースだけはちょっと複雑になってしまうかもしれない感じ(何か別の書き方があるのかもしれない)。別の書き方をkoichikさんに教えていただきました、↓の方に追記あります。

public ThesisProfUser findByUidAndYear(String userId, int year) {
    return select()
        .leftOuterJoin(year())
        .leftOuterJoin(profUser())
        .leftOuterJoin(thesisAssignmentList())
        .leftOuterJoin(thesisAssignmentList().thesisStudentUser())
        .leftOuterJoin(
            thesisAssignmentList().thesisStudentUser().studentUser())
        .leftOuterJoin(thesisAssignmentList().assignmentVoteList())
        .where(
            new ThesisProfUserCondition().profUser().uid.eq(userId).and(
                new ThesisProfUserCondition().year().year.eq(year)))
        .getSingleResult();
}

何にせよS2JDBC-Gen使うとS2JDBCが異次元的に強力になりヤバいです。たぶん羽生さんのらくらくERDレッスンで勉強して組んだテーブルとの相性の良さは僕が知っているO/Rマッパーの中で一番高いと思う(残念ながらERDレッスンを未だに読み終えてないので似非テーブル設計しかできてないけど・・・)。

  • [追記: 21:30]

さらにkoichikさんがコメントくださったように、Conditionの部分も流れるように書けました!

public ThesisProfUser findByUidAndYear(String userId, int year) {
    return select()
        .leftOuterJoin(year())
        .leftOuterJoin(profUser())
        .leftOuterJoin(thesisAssignmentList())
        .leftOuterJoin(thesisAssignmentList().thesisStudentUser())
        .leftOuterJoin(
            thesisAssignmentList().thesisStudentUser().studentUser())
        .leftOuterJoin(thesisAssignmentList().assignmentVoteList())
        .where(eq(profUser().uid(), userId), eq(year().year(), year))
        .getSingleResult();
}

これは素晴らしい。。。XxxConditionいらないですね。

S2JDBC-Gen便利


S2JDBC-Genを初めて触ったけど良いですね。エンティティに@OneToMayや@ManyToOneも生成してくれたり、エンティティ以外にも対応するCondition、Names、Serviceも生成してくれていたせりつくせり。ConditionとNamesは使ったことなかったけど、今回生成されたのを使ってみたら、今までS2JDBC使った時に曖昧だったwhere内の "id = ?" などの部分がきっちりし、スキーマ変えてもエラー箇所が追随できるようになり全部ConditionとNames使うように変えました。

異なるDBサーバをいったりきたりして開発する時の型に迷う


H2 DatabaseのIDENTITYは8バイトだけど、PostgreSQLのSERIALは4バイト。

なので、JiemamyでDialectをH2からPostgreSQLに変えると、8バイト整数であるPostgreSQLのBIGSERIALになる。

ところで、主キーであるIDENTITY or BIGSERIALに対応する別テーブルにある外部キーは精度落ちるけど4バイトのINTでも書けるのでそう書いておいて、S2JDBC-GenでEntityを生成します。そうすると、主キーの部分はjava.lang.Longで生成され、外部キーの部分はjava.lang.Integerで生成されます。

そして、ActionやServiceでtable2.table1_id = table1.id なんてことをしようとするとIntegerにLongを代入することになるのでエラーとなってしまう。

そんなわけで、H2 Databaseで開発して本番がPostgreSQLで同じJiemamyのER ダイアグラムを使うには、外部キーも全部BIGINTにしとかないと面倒なことが起きてしまう。

H2 DatabaseのIDENTITYの持つ精度のことを考えれば当然なんだけど、テーブル定義にBIGINTがたくさんあると見慣れてないせいか変な感じする。でも、H2 DatabaseとMySQLで開発しようとするとどうなるかというとH2 DatabaseのIDENTITYは4バイトのSERIALではなくBIGINTになってしまいそもそもの自動採番がなくなってしまい結局そのままでは対応できない。

とりあえずPostgreSQLとMySQLのSERIALに対応する4バイト版のIDENTITYがH2 Databaseにあれば良いのかもしれない。

というわけで、O/RマッパーがDBの方言解釈してくれても、複数DBをいったりきたりして開発するのは意外と面倒なのかもしれない。。

OpenLDAPのスキーマをOpenDSに追加する時の7つのポイント


OpenDSはSunが中心にオープンソースで開発しているJavaで書かれたディレクトリサーバです。詳細についてはOpenDSのサイトを見てもらえば判りますが、個人的に興味を引かれた特徴は次の点です。

  • Javaで書かれているので組み込みで動かせる
    • テストも兼ねて実際にコード書いてみましたが良い感じ
    • Java 5以上必要
  • マルチマスタ構成が可能
    • もちろんschmeaやconfig系も込みで
  • 将来的にSun Java System Directory Server Enterprise Editionの中身はOpenDSになるらしい

さて、では実際に使おうとするといろいろスキーマを追加したくなります。早速、広く使われているOpenLDAP用のスキーマを追加しようとしたところいくつか変更する必要がありました。

先に良く使うスキーマの変更済みスキーマを提示しておきます。動作確認はちょっとバージョンが古いですが、Mavenで手に入るOpenDS 1.0.0 build 005でしました。

これらを作るにあたって判ったスキーマファイルの変更ポイントは次の7つです。

  • 1. スキーマファイルは $OPENDS_HOME/config/schema 以下に XX-hoge.ldif というファイル名で配置

XXの部分には任意の数字を入れます。数字が若い順に読み込まれるので標準で入っているスキーマより大きい数字を指定します。上で挙げたスキーマでは、適当に10としています。

  • 2. 定義を開始する部分に次を追加
dn: cn=schema
objectClass: top
objectClass: ldapSubentry
objectClass: subschema

ファイルの先頭のコメント部分はそのままで構いません。

  • 3. attributetype ( … ) を attributeTypes: ( … ) に置換

変更前: openssh-lpk.schemaより

attributetype ( 1.3.6.1.4.1.22054.500.1.1.1.13 NAME 'sshPublicKey'
        DESC 'MANDATORY: OpenSSH Public key'
        EQUALITY octetStringMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

変更後: openssh-lpk.schemaより

attributeTypes: ( 1.3.6.1.4.1.22054.500.1.1.1.13 NAME 'sshPublicKey'
        DESC 'MANDATORY: OpenSSH Public key'
        EQUALITY octetStringMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
  • 4. objectclass ( … ) を objectClasses: ( … ) に置換

変更前: openssh-lpk.schemaより

objectclass ( 1.3.6.1.4.1.22054.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
        DESC 'MANDATORY: OpenSSH LPK objectclass'
        MUST ( sshPublicKey $ uid )
        )

変更後: openssh-lpk.schemaより

objectClasses: ( 1.3.6.1.4.1.22054.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
        DESC 'MANDATORY: OpenSSH LPK objectclass'
        MUST ( sshPublicKey $ uid )
        )
  • 5. attributeTypes ( … ) の ) 前には必ずスペースが1つ必要

NG: qmail-ldap.schemaより

attributeTypes: ( 1.3.6.1.4.1.7914.1.2.1.6 NAME 'mailHost'
        ... 省略 ...
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE)

OK: qmail-ldap.schemaより

attributeTypes: ( 1.3.6.1.4.1.7914.1.2.1.6 NAME 'mailHost'
        ... 省略 ...
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )

objectClassesはスペースが無くても大丈夫です。

  • 6. 定義途中で改行した場合、先頭にスペースを2つ以上入れてから続きを記述する

NG: openssh-lpk.schemaより

attributetype ( 1.3.6.1.4.1.22054.500.1.1.1.13 NAME 'sshPublicKey'
 DESC 'MANDATORY: OpenSSH Public key'
 EQUALITY octetStringMatch
 SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

OK: openssh-lpk.schemaより

attributetype ( 1.3.6.1.4.1.22054.500.1.1.1.13 NAME 'sshPublicKey'
  DESC 'MANDATORY: OpenSSH Public key'
  EQUALITY octetStringMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

TabもNGです。

  • 7. 定義と定義の間に空行を作ってはいけない、コメントアウト行を入れるのはOK

NG: openssh-lpk.schemaより

dn: cn=schema
objectClass: top
objectClass: ldapSubentry
objectClass: subschema

# octetString SYNTAX
attributeTypes: ( 1.3.6.1.4.1.22054.500.1.1.1.13 NAME 'sshPublicKey'
  DESC 'MANDATORY: OpenSSH Public key'
  EQUALITY octetStringMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

# printableString SYNTAX yes|no
objectClasses: ( 1.3.6.1.4.1.22054.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
  DESC 'MANDATORY: OpenSSH LPK objectclass'
  MUST ( sshPublicKey $ uid )
  )

OK: openssh-lpk.schemaより

dn: cn=schema
objectClass: top
objectClass: ldapSubentry
objectClass: subschema
# octetString SYNTAX
attributeTypes: ( 1.3.6.1.4.1.22054.500.1.1.1.13 NAME 'sshPublicKey'
  DESC 'MANDATORY: OpenSSH Public key'
  EQUALITY octetStringMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
# printableString SYNTAX yes|no
objectClasses: ( 1.3.6.1.4.1.22054.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
  DESC 'MANDATORY: OpenSSH LPK objectclass'
  MUST ( sshPublicKey $ uid )
  )

以上の7つに注意すればOpenLDAPのスキーマをOpenDSでも使うことができます。OpenLDAPとOpenDSの標準で提供されているスキーマのいくつかは、オブジェクトクラスの構造クラスと補助クラス定義が異なるのがありますが、基本的にはほぼ同じようなのでそれほど戸惑うこともないと思います。

そして最後に、S2Directoryで問題無くOpenDSのエントリを扱えることも確認済みです。

[18:50追記] S2DirectoryでSMD5, SSHAを使用してパスワードを保存した時に保存したパスワードでOpenDSのSASL認証が上手くいきませんでした。原因はsaltの長さがS2Directoryは4バイトを、OpenDSは8バイトを想定しているためでした。とりあえずDIRECTORY-25で対応して、S2Directory 0.7-SNAPSHOTをデプロイしときました。

Cubbyの良いところ


先日Cubbyで作った2個目の簡単なアプリを予定通り動かし始めました。今のとこ大きなトラブルもないようです。というわけで、Cubbyの良いところを書きたいと思います。ちなみにほとんど同じことがSAStrutsにも言えます。

  • Examplesが実用的で簡単

Cubbyに興味を持ったらとりあえずサンプルwarを動かし、Todoサンプルアプリケーションを見てみてください。一覧表示・詳細・追加・編集・確認・保存に加えて、ログインとログイン状態確認(AuthActionInterceptor)もついています。そして、それぞれの機能に対応するTodoListAction、TodoAction、LoginActionを見ると、とてもコードが少なく、これならすぐに似たようなアプリが作れる気がします(実際すぐに作れました)。

さらにコードを良くみると、フォーム=アクション自身になっています。これは僕の中でかなり革新的でした。今までフォーム用にPOJO(Entityを継承したりして)を用意してたけど、結局僕のように小さなアプリしか作らない場合、POJOを使いまわすなんてことは無く、それだったらアクション自身にあった方が見通しが全然良いんです。なお、別に分けたい場合は@Formで別途指定できるので、フォーム=アクション自身に絶対する必要があるというわけでもありません。

他にもCubbyのExampleは小さなテクニックがいろいろ散りばめられています。例えば、エラー時の画面装飾。effects.jsでちょっと目に付いて親切な装飾がされています。もちろんこれはCubbyでしかできないことではないですがこういったちょっと「良いな」っと思えるサンプルがいろいろあります。そんなわけでますますCubbyを使ってみたいと思ってしまうわけです。

また、今のExampleの例では、S2DxoとS2Daoを使っていますがここをBeansとS2JDBCにしたって問題ありません。僕はそうしています。

  • URIが綺麗

やはりURIは、名は体を表す如く美しくあるべきだと思います。以前、無設定S2Struts用にSplitUpperActionPathNamingRuleというのを取り入れていただきましたが、それでも自由自在というところまではいきませんでした。Cubbyでは@Pathにより、URIの一部分をリクエストパラメータにするなど思い通りにURIを表現できます。この仕組みは、RequestRoutingFilterというフィルタにより、実体のアクションクラスへフォワードすることで実現されています。この仕組みも最初見たときそんな手があったかーっと驚きました。

また、通常認証確認処理はURI単位で設定したいので今まではオリジナルのフィルタで書いていました。StrutsなどでInterceptorを使ってやろうとすると、アクションに引っ掛けることになるので、.jspなどのページに直接アクセスされると認証処理が掛けられないので困ってしまいます。でも、Cubbyを使ったアプリでユーザがアクセスするのはすべて綺麗なURIにすることができ、実体のページにアクセスさせないようにしやすいです。なのでExampleのAuthActionInterceptorのようにInterceptorでも抜けのない認証確認が出来ます。ちなみにこのInterceptorはcustomizer.diconでLoginAction以外のアクションに適用するように設定されています。サンプルのAuthActionInterceptorをちょっと拡張すれば、URIごとにアクセス権限を分けるといったことも簡単に実現できます。

public Object invoke(MethodInvocation invocation) throws Throwable {
    // 実行しようとしているアクションを取得
    final Action action = (Action)invocation.getThis();
    // リクエストされたURLを取得
    final String url =
        (String)request.getAttribute("javax.servlet.forward.servlet_path");

    // 非ログインページ
    if (url.startsWith("/index")) {
        return invocation.proceed();
    }
    ...
    // ログインページ
    // ログイン状態の確認
    final PosixAccount user = (PosixAccount)sessionScope.get("user");
    if (user == null) {
        action.getFlash().put("notice", "ログインしていません。");
        return new Redirect("/login/");
    }
    ...
    // 各ページへのアクセス権限の確認
    Object group;
    // admin
    group = sessionScope.get("group_admin");
    if (url.startsWith("/admin/") && group == null) {
        return new Redirect("/");
    }
    ...
    return invocation.proceed();
}
  • @Pathによる制約

「URIが綺麗」に関係しますが、@Pathで指定する値には正規表現で制約を付けることができます。簡単な例だと次のとおりです。

public class TodoAction extends Action {
    public Integer id;
    @Path("{id,[0-9]+}")
    public ActionResult show() {
        Todo todo = todoDao.selectById(this.id);
        todoDxo.convert(todo, this);
        return new Forward("show.jsp");
    }
}

これだと、/todo/1 にアクセスすれば、idに1が自動的に入った状態でshow()が実行されます。/todo/aa の時はshow()は実行されません。この例はアクションメソッドに@Pathを付けていますが、クラス自身に@Pathを付けることもできます。

僕が使った複雑な例としては、こんな感じです。

@Path("admin/{thesisTypeName,abstract|full}/{userId}")
public class AdminThesisAction extends ThesisAction {
    ...
    @Validation(rules = "submitValidation", errorPage = "/admin/index.html")
    public ActionResult submit() {
        super.submit();
        return new Forward("/admin/submit.html");
    }
    ...
    public ActionResult download() {
        File file = new File(saveDir, thesis.getFileName());
        if (!file.exists()) {
            flash.put("notice", "ファイルが見つかりません: " + file.getName());
            return new Redirect("/admin/");
        }
        return super.download(file);
    }
}
public class ThesisAction extends DownloadAction {
    public String thesisTypeName;
    public String userId;
    ...
    public ActionResult submit() {
        if (thesis != null) {
            Beans.copy(thesis, this).excludesNull().execute();
        }
        return new Forward("submit.html");
    }
    ...
}
public class DownloadAction extends Action {
    public HttpServletResponse response;
    protected ActionResult download(File file) {
        ...
    }
}

この例だと、/admin/abstract/user001/submitや/admin/abstract/user099/downloadなどがAdminThesisActionですべて処理されます。extends ThesisActionとしているのは同じようなことをユーザごとや特定の権限を持った人も同じ機能を使いたかったからです。実際に@Path("student/{thesisTypeName,abstract|full}")を持つStudentThesisActionと@Path("faculty/{thesisTypeName,abstract|full}/{userId}")を持つFacultyThesisActionを作りました。権限ごとに項目が多少違うので、validationはそれぞれの権限に応じたものをそれぞれの実装クラスで定義しました。

  • Validationが柔軟

CubbyではValidationはValidationRuleをValidationRulesでひとまとめにして定義し、それぞれのアクションメソッドにそれぞれ適用できます。つまり、アクションメソッドごとに違うValidationを適用できます。アクションのコードが少ないのはここでも役に立ちます。ExampleのLoginActionを見れば判りますが、アクション内にオリジナルのValidationRuleがあります。ここでも僕のように小さなアプリで使いまわすことがないなら同じアクション内の方が見通しが良いです。しかも同じアクション内にあることでアクションにあるパラメータもそのまま使えます。

@Binding(bindingType = BindingType.NONE)
public ValidationRules confirmValidation = new DefaultValidationRules() {
    @Override
    public void initialize() {
        add("token", new TokenValidator());
        add("enTitle", new RequiredValidator());
        add("enTitle", new RegexpValidator("\\p{ASCII}+"));
        add("jaTitle", new RequiredValidator());
        add("pageSize", new RequiredValidator());
        add("file", new RequiredValidator());
        add("file", new FileRegexpValidator(".+\\.pdf"));
    }
};

Cubbyが提供するカスタムタグは親切な設計になっています。特にお気に入りなのが、tokenです。ページのform内に <t:token /> と書いて、ValidatgionRulesに add("token", new TokenValidator()); するだけで、2重サブミット・CSRF対策が出来てしまいます。また、カスタムファンクションのoddもテーブルなどで色分けが簡単に出来るようになっていて親切です。

  • プレゼンテーションがJSPである = Mayaaが使える

個人的にフレームワークを選ぶ基準の一つにMayaaが使えるかどうかというのがあります。Mayaaは「プログラマとデザイナの作業分担を強く意識したWEBフロントサービスエンジン」という説明がありますが、一人で作る場合でも有効です。主な理由は2つです。1つ目はそのページの機能が一目瞭然になること。2つ目はコンポーネントによる機能分割が可能なことです。

1つ目は、僕の記憶力が良くないことに関係します。たいてい1日経たずしてそのページが何の機能を持ってたか綺麗に忘れます。そんな時、まず.mayaaを見ます。.mayaaにはそのページで実現される「機能」しか書かれていません。それもたいていたいした量はありません。そんなわけで、一目瞭然にそのページの機能が判ります。そして機能を把握したら、頭をデザイナモードにして.htmlを編集するか、プログラマモードにしてアクションを編集します。記憶力が良い方でもおそらく1年も経てばすっかり忘れてしまうと思います。そんな時、.mayaaはきっと役に立つはずです。

2つ目は、1つ目にも関係しますが、まとまった機能・使いまわす機能はコンポーネントにして、<m:insert>を使えば、複雑なページでも.mayaaが簡潔になります。簡潔になれば、1つ目のように機能を確認する時にすぐに把握できます。先程の@Pathのとこで出てきた複雑な例は、ThesisActionを継承したクラスが3つありました。フォームの項目に1つか2つの違いはありますが基本的に同じです。そんなわけでそれらに対応したフォーム部分は共通のコンポーネントに切り出してしまいます。

他にも、m:extendsでページデザインを楽に出来たり、やろうと思えば.mayaa単体でアクション相当のことが出来てしまうなどMayaaは超強力です。そんなわけで、フレームワークを選択する時、僕はMayaaが使えるかどうかは重要な判断基準になります。

  • 何はともかくS2ContainerのHOT deploy, publicフィールド, Beans, S2JDBCなどなど

CubbyとMayaaの良いところを書きましたが、忘れちゃいけないのがS2Containerです。アクションのコードを小さく書けるのは、Cubbyだけでなく、S2Containerの多くの機能があるおかげです。S2ContainerというとDI・AOPと想像される方もいるかもしれませんが、S2Containerのすごいとこは、DI・AOPと同じかそれ以上にその周辺の様々な強力な機能群があることだと思います。Cubbyの内部でもリクエストパラメータを処理するのにS2Dxoが使われていたり、CubbyやSAStruts利用者がアクションメソッドを小さく書くのに、publicフィールド, Beans, S2JDBCは必須機能です。これらはきちんと型を理解して処理してくれるのも大きな魅力です。また、サクサク開発するのにHOT deployは欠かせません。

というわけで、長々書きましたが、今現在僕には、Cubby + S2ContainerのHot deploy, publicフィールド, Beans, S2JDBC + Mayaaがちょうど良いのです。

他にも書いてない良いとこもあると思いますがとりあえずこんな感じです。Cubbyの代わりにほぼ同様のことのできるSAStrutsを選択するのも良いと思います。CubbyとSAStruts、まだ触ってない方は是非触ってみてください。

ホーム > Java

検索
フィード
メタ情報

Return to page top