Eclipseのプロジェクト・エクスプローラを使ってWebアプリを快適に開発
Webアプリケーションの開発をEclipseで行っていると、Javaファイルを格納するフォルダと他のWebリソース(.jsp、.css、.js、など)を格納するフォルダがパッケージ/エクスプローラ上で離れているということがよくあります。
よほど大きな画面を使っていない限り、それぞれのリソースを行き来するのは面倒です。もちろん、Ctrl+Shift+T(キーワードで型を開く)やCtrl+Shift+R(キーワードでリソースを開く)という方法もありますが、ディレクトリ構成を見ながら複数のファイルを編集していきたいというケースもあるでしょう。
そんなときに使えるのが、プロジェクト・エクスプローラというビューです。これはパッケージ・エクスプローラにそっくりなビューで、同じようにフォルダ構成をツリーで表示してくれます。察しの良い方は、もう気づかれたと思いますが、要するにパッケージ・エクスプローラをJavaファイル編集用、プロジェクト・エクスプローラをWebリソース編集用のビューとして使うわけです。
このようにして使います。この図ではプロジェクト・エクスプローラでwebappフォルダ配下だけを表示しています。以外と便利そうに思いませんか?
使い方
欠点
欠点というほどでもありませんが、Eclipseを再起動するとプロジェクト・エクスプローラの表示は元に戻ってしまいます(特定のフォルダ配下を表示させていてもリセット)。なので、再起動の度に選択し直さなければなりません。そういう時は、エディタとのリンク機能を利用してさっと再選択するのが楽です(エディタにWebリソースを表示→リンク→次へジャンプ)。
まとめ
プロジェクト・エクスプローラを活用すると、少しだけ開発の効率が上がるかもしれない。
ジェネリクスを使用してスマートにリストを生成する
以前、どこかのブログで読んで「便利だなー」と思ったきり、すっかり忘れていたのでメモ。
一般的なリスト生成方法
Java5以降で、リストを生成するときは通常以下のようにする。
List<String> list = new ArrayList<String>();
コンパイル時にミスが発見できるジェネリクスを使わない手はないだろう。しかし、このコードには1つ不満な点がある。それは、リストの宣言と生成の2箇所において、扱う型としてStringを指定している点だ。これは、DRY原則(Don't Repeat Yourself=同じことを繰り返すな)に反する。
スマートなリスト生成方法
List<String> list = newArrayList();
public static <E> ArrayList<E> newArrayList() { return new ArrayList<E>(); }
newするコードを書くのではなく、ArrayList生成用のメソッドを用意して、それを呼び出すようにしている。型はコンパイラが推測してくれるので、メソッド呼び出し時に型を指定する必要がない。
この例では、そんなに恩恵を受けていないようにも見えるけど、Mapを格納する場合などには、コードがかなりすっきりする。
List<Map<String, EmployeeDto>> list = newArrayList();
ちなみにHashMapなどについても同様の実装が可能。
Map<Integer, String> map = newHashMap();
こういったAPIが用意されているライブラリ
今回は必要な分だけ自前で実装していたのだが、実装後にS2Containerに付属している(?)S2Tigerのorg.seasar.framework.util.tiger.CollectionsUtilに、同様のAPIが一通り用意されていることが分かった。今度からはこれを利用することにしよう。
まとめ
コレクション系のクラスを生成する際は、
List<Foo> list = newArrayList();
と書いてコード量を削減してしまおう!
S2Unitを使ってログイン検証インターセプタを単体テスト
Seasar2には、S2Unitという単体テスト支援ツールが含まれている。ツールと言っても、いわゆるJUnitみたいなもので、実際、S2UnitはJUnitを拡張して作られている。今回は、昨日の記事に書いたログイン検証インターセプタのテストをS2Unitで行う方法を書こうと思う。
テスト観点
- クラスに@Authenticationが定義されていて、@Executeがついているメソッドのみで検証処理が行われること
- 検証対象のメソッドで、ログイン情報が存在しない場合は、ログイン画面に遷移するパスが返却されること
- 検証対象のメソッドで、ログイン情報が存在する場合は、通常のメソッドがそのまま呼び出されること
テスト用のActionクラス
ログイン検証アノテーションが定義されているActionクラス。
@Authentication public class TestActionAuth { @Execute public String execute() { // このメソッドが検証対象になる return null; } public String noExecute() { return null; } }
ログイン検証アノテーションが定義されていないActionクラス。
// どのメソッドも検証処理は行われないはず public class TestActionNotAuth { @Execute public String execute() { return null; } public String noExecute() { return null; } }
単体テストクラス
S2TestCaseを継承した単体テストクラス。
public class AuthenticationInterceptorTest extends S2TestCase { private static final String PAHT = "authenticationInterceptorTest.dicon"; private LoginDto loginDto; private TestActionAuth testActionAuth; private TestActionNotAuth testActionNotAuth; public void setUp() { include(PAHT); } public void test処理対象で未認証() { loginDto.id = null; // 未認証 assertNotNull(testActionAuth.execute()); // 実際にはログイン画面に遷移するパスが返されるはず } public void test処理対象で認証済み() { loginDto.id = 1; // 認証 assertNull(testActionAuth.execute()); // 本来のアクションが実行されnullが返されるはず } public void test処理対象外_Authenticationがある_Executeがない() { loginDto.id = null; // 未認証 assertNull(testActionAuth.noExecute()); } public void test処理対象外_Authenticationがない_Executeがある() { loginDto.id = null; // 未認証 assertNull(testActionNotAuth.execute()); } public void test処理対象外_Authenticationがない_Executeがない() { loginDto.id = null; // 未認証 assertNull(testActionNotAuth.noExecute()); } }
S2TestCaseでは、include()を呼び出すことでdiconファイルが読み込まれ、S2Containerが初期化される。diconファイルに定義されたコンポーネント名と同じフィールドが定義されていれば、自動的にセットしてくれる。
- ログイン情報を変更できるようにLoginDtoをフィールドに用意
- setUp()内でdiconファイルを読み込み
diconファイル
単体テストクラスで使用するコンポーネントを定義したdiconファイル。
<components> <component name="loginDto" class="levelup.dto.LoginDto" /> <component name="authenticationInterceptor" class="levelup.aop.AuthenticationInterceptor" /> <component class="levelup.aop.TestActionAuth"> <aspect>authenticationInterceptor</aspect> </component> <component class="levelup.aop.TestActionNotAuth"> <aspect>authenticationInterceptor</aspect> </component> </components>
- ログイン情報としてLoginDtoを定義
- ログイン検証用インターセプタを定義
- テスト用の2つのActionクラスを定義し、ログイン検証用インターセプタをアスペクトとして登録
SAStrtusで初回リクエスト時にクラスからアノテーション情報が取得できない?
SAStrutsでアノテーションを利用したログイン検証
SAStrutsは内部でSeasar2を利用しているので、独自のインターセプタを定義してクラスに織り込むことが出来る(AOP)。Webアプリケーションといえば、ログイン検証はおなじみの処理だが、これはインターセプタを使って実現することができる。
以下は、Actionクラスにアノテーションを指定してログイン検証処理を行うサンプルである。ちなみに検討課題が無いわけではない。
ログイン情報格納用クラス
@Component(instance = InstanceType.SESSION) public class LoginDto implements Serializable { private static final long serialVersionUID = 1L; public Integer id; }
セッションで管理するのでInstanceType.SESSIONで。
ログイン検証指定アノテーション
Actionクラスに指定し、その中の全ての@Executeメソッドで検証処理が行われる仕様にする。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Authentication {}
実行メソッド単位で指定できるようにすることもできるが、それは少々面倒(=開発でミスが起きやすい)なので、1ユースケースにあたるAction単位で指定できるようにした。1ユースケース中で、検証したい場合としたくない場合があるというのはあまり考えられないが、その特殊なケースが発生したら、メソッドに@NotAuthenticationみたいなアノテーションつけて対応するという逃げ道はある。
ログイン検証インターセプタ
実際に検証処理を行うインターセプタ。
package levelup.aop; public class AuthenticationInterceptor extends AbstractInterceptor { private static final long serialVersionUID = 1L; @Resource protected LoginDto loginDto; public Object invoke(MethodInvocation invocation) throws Throwable { if (isTargetMethod(invocation) && !isLogin()) { return "/login?redirect=true"; } return invocation.proceed(); } private boolean isTargetMethod(MethodInvocation invocation) { Method method = invocation.getMethod(); Class<?> clazz = invocation.getThis().getClass(); Execute execute = method.getAnnotation(Execute.class); Authentication auth = clazz.getAnnotation(Authentication.class); return execute != null && auth != null; } private boolean isLogin() { return loginDto.id != null; } }
MethodInterceptorインターフェースを実装してもよかったけど、今回はAbstractInterceptorを継承して実装。LoginDtoがログイン情報で、これはDIコンテナにより自動的にセットされる(型がインターフェースではないので@Resouceは必須)。検証対象のメソッドで、未ログインの場合はログインActionにリダイレクトし、実行メソッドの呼出は行わない。飛ばしたい先が変わるということはあまり考えられないけど、心配ならreturnPathみたいなプロパティを用意しておいて、diconファイルで指定するのも手。
app.dicon
インターセプタの登録。
<components> ... <component name="authenticationInterceptor" instance="request" class="sample.aop.AuthenticationInterceptor" /> </components>
S2Containerのデフォルトのインスタンスは"singleton"だが、リクエスト単位で処理するので"request"を指定。これを忘れると、LoginDtoがインジェクションされない(自分より短いライフサイクルのコンポーネントはインジェクション出来ない)。
customizer.dicon
Actionにインターセプタを適用。
<components> <component name="actionCustomizer" class="org.seasar.framework.container.customizer.CustomizerChain"> ... <initMethod name="addAspectCustomizer"> <arg>"authenticationInterceptor"</arg> <arg>true</arg> </initMethod> ...
addAspectCutomizerの第2引数は、インスタンスがSingletonじゃない場合はtrueにしなきゃならないらしい。
コンポーネントに適用するインターセプタのインスタンス属性がsingleton以外の場合は、 useLookupAdapterプロパティをtrueに設定します。 これにより、 コンポーネントのメソッドが呼び出される度に、 コンテナからインターセプタのインスタンスをルックアップするようになります。
とのことだが、requestなインスタンスならいらないような気もする・・・どうなんだろ?
Actionに指定
こうやって使う。
@Authentication public class IndexAction { ... @Execute(validator = false) public String index() { ... } ... }
実際の処理ではログイン処理なんか一切気にしていないのが、いかにもAOP。
課題
ログイン検証処理よりもActionFormのバリデーションの方が先に動作してしまう。つまり、入力内容にエラーがあり、ログイン情報が切れていても、ログイン画面には飛んでくれない。
- おいジョージ、まだ登録が終わらないのか?
- ああ、ちょっ入力に間違いがあってな・・・よし終わっ・・・what!?
- ん?どうしたジョージ?
- ・・・ログイン画面に飛ばされちまったんだ。
なんて事態も考えられなくはない。
シーケンスを使う場合の注意
S2JDBCを使っていてはまったので日記に書こうと思います。
S2JDBCで識別子(ID)を自動生成させるためには、
エンティティのプロパティに@GeneratedValueというアノテーションを付けます。
自動生成のタイプとして指定できるのは、
- GenerationType.TABLE ⇒ テーブルを使用
- GenerationType.SEQUENCE ⇒ シーケンスを使用
- GenerationType.IDENTITY ⇒ データベース固有の自動生成を使用
の3種類ですが、今回はまったのはシーケンスです。
エンティティで識別子は以下のように定義します。
@Id
@GenerationType.SEQUENCE
public Integer id;
挿入処理(INSERT)は以下のように書きます。
jdbcManager.insert(entity).execute();
これでS2JDBC側で"call next value for ENTITY"といったSQLが投げられ、シーケンスの次の値が取得されます。
挿入する件数が1件の場合は特に問題ないのですが、複数件の場合は注意しなければなりません。たとえばActionクラスで、先ほどのjdbcManager.insert(entity).execute()を3回呼び出したとします。さて、このときシーケンスの次の値を取得するSQLは何回なげられるでしょうか?
答えは1回だけなのです。
つまり、S2JDBCではパフォーマンスを考慮し、最初の1回だけシーケンスの次の値を取得し、2回目以降はフレームワーク側で内部インクリメントしているのです。
さて、これで何が問題になるかというと、シーケンスと識別子の不一致です。たとえば、"call next value for ENTITY"で1が取得されたとします。
その際に3件挿入処理を行うと、1,2,3という識別子がレコードに設定されることになります。しかし、"call next value for ENTITY"は1回しか呼ばれていないので、値は3ではなく1のままなのです。
勘の良い方はもうお気づきですね。そう、次に挿入処理を行おうとした際にシーケンスから取得される値は2となり、
レコード挿入時に識別子の重複エラーが発生してしまうのです。
さて、それではどうすれば良いかということですが、実は簡単でシーケンスの一度のインクリメント値を大きくしてやればよいのです。具体的にはシーケンスを作る際に"CREATE SEQUENCE INCREMENT BY 50"といった感じで。
実際にはアプリケーション側で一度に挿入する可能性のあるレコード数が適切でしょうか。