Seasarプロジェクト見学(その2)

昨日の続き

サイトのSAStrutsの情報だけだと環境が作れなかったので、「Seasar2入門」(2009/2/23初版)を買ってきました。
P176-177に、こんな内容が書かれています。
Teedaを作ったが、JSFが思ったより受け入れられなかった。
・KuinaDaoを作ったが、JPAが思ったより受け入れられなかった。
SAStrutsS2JDBCは超大規模案件でも耐えられるように設計した。
SAStrutsS2JDBCS2StrutsS2Daoの後継。
(勝手に要約しています。正確なニュアンスがずれているかもしれないので、各自書籍でご確認ください。)

今からSeasarを試してみるなら、プレゼン層はSAStrutsがよさそうです。

二日だけ試してみた個人的なまとめ

SAStrutsを書籍にそって試してみた結果、StrutsとEntity,Serviceの概念が分かるJava開発者なら、直ぐに導入できる感触を得た。
SAStrutsは専用のEclipseプラグインまで含めて使うことを想定しており、フルスタックフレームワークと開発環境を提供する事を目的としている。
・よって、当初はDIコンテナとしてのSeasarを評価するつもりだったが、SAStrutsはその目的には不適当。

Seasarプロジェクト見学(その1 SAStruts)

概要

未だ未体験のSeasarプロジェクトを見学してみます。
まずは、DIがどのような作りか体験したっかのですが、
プレゼンテーションだけで12プロジェクトもあってどれを試せばいいのかいきなり迷います。
ググッてヒット件数を比較。

  • Cubby の検索結果 約 1,840,000 件
  • Mayaa の検索結果 約 153,000 件
  • mobylet の検索結果 約 15,500 件
  • S2BlazeDS の検索結果 約 6,230 件
  • S2Flex の検索結果 約 54,600 件
  • S2JSF の検索結果 約 13,300 件
  • S2OpenAMF の検索結果 約 686 件
  • S2Portlet の検索結果 約 1,850 件
  • S2Struts の検索結果 約 22,500 件
  • SAStruts の検索結果 約 68,300 件
  • Teeda の検索結果 約 182,000 件
  • Ymir の検索結果 約 349,000 件

CubbyがダントツですがSeaser以外の「何か」もヒットしています。Cubby seaserだと約 287,000 件でした。
S2Strutsが私の周りでは一番よく聞くような気がしますが、主流派では無いようです。
CubbyYmirがシンプルそうで気になりますが、
なんとなくはStrutsの拡張っぽくて新しそうなSAStrutsを使ってみることにしました。

導入

公式サイトのセットアップに沿って進めていきます。

1.プラグイン

http://eclipse.seasar.org/updates/3.2/から「Tomcat Launcher」プラグインEclipseにインストールしろとありますが、見つかりませんでした。「WebLauncher」というそれっぽいものがあるので、代わりに入れてみます。カンです。
http://eclipse.seasar.org/updates/3.3/から「ResourceSynchronizer」と「SAStrutsPlugin」は見つかったのでインストール。

2.チュートリアルプロジェクトの設定

チュートリアルEclipseプロジェクトをダウンロード。
sa-struts-tutorial-1.0.4-sp8.zip
インポートしたら、Tomcatのjarがうまくパスが通らなかったので、適当に直します。

3.WebLauncherの設定

チュートリアルのプロジェクトはWTPのサーバから認識できませんでした。
先ほど入れたWebLauncherで試してみます。


「WebLauncherダイアログ」
プロジェクト右クリック>[プロジェクト]メニュー>[WebLauncher]を選択
  ・「Winstoneを使用する。」:チェックをつける
  ・コンテキスト名:""(空白)
  ・動作ディレクトリ:/sa-struts-tutorial/src/main/webapp
  ・ポート番号:8080
  ・設定ファイル:""(空白)


SAStrutsダイアログ」
ついでに、SAStrutsの設定
  ・Webappルート:/src/main/webapp
  ・メインJavaソース・パス:/src/main/java
  ・convention.diconパス:/src/main/resources/convention.dicon
  ・Webサーバ:http://localhost:8080
  ・コンテキスト:

4.サーバ起動

プロジェクト右クリック>[WebLauncher]メニュー>[サーバを起動する]メニューを選択
http://localhost:8080/にアクセスすると、「Welcome to Super Agile Struts Tutorial」画面が開きました。


とりあえず

動作する環境ができたところで、チュートリアルを見てみたのですが、、、長い。

DI部分はどこなのでしょうか??

覗いていると時間がかかりそうなので、次のプロジェクトへ移ります。

JS Button Editor

JS Button Editor

HTML5になってCanvasで絵が描けるようになったので、試しにボタン画像のエディタを試作してみました。
JS Button Editor
Windows+FireFox3.5 or Safari or Chromeで動きます。
ちなみに、ボタン画像は右クリックで手動で保存してください。PING画像で落とせると思います。

TODO
・フォントとブラウザの組み合わせで文字の高さが違うので微調整が必要
・配色の調整が必要

Wicketの認証機能

概要

前回Wicketに認証機能を追加しようとしてSpring Securityを導入してみたのですが、そもそも方向性が間違っていました。Wicketには、wicket-auth-rolesという認証機能のサブプロジェクトがあります。

情報源

Wicket公式サイトwicket-auth-rolesの情報を探したのですが、見つかりませんでした。公式サイトからリンクされているWikiにあるかも知れませんが、現在(2010/04/13)メンテナンス中でアクセスできない状態です。


ソースは、Apachiのsvnにそれらしきリリースタグのブランチがありました。
mavenのセントラルリポジトリに見当たらないため、svnリポジトリから直接入手するしかなさそうです。
http://svn.apache.org/repos/asf/wicket/releases/wicket-1.4.7/wicket-auth-roles


また、examplesディレクトリに、wicket-auth-rolesのサンプルがあります。
http://svn.apache.org/repos/asf/wicket/releases/wicket-1.4.7/wicket-examples/src/main/java/org/apache/wicket/examples/authentication


さらに、下記サイトを参考にさせていただきました。
rio's blog
Wicket の勉強 (4) wicket-auth-roles を使って認証/認可を実現する」
http://rio1218.blog26.fc2.com/blog-entry-82.html



サンプル

svnのexamplesを元に、既存のWicketアプリケーションに「最小限の認証機能」を分かる限り最短で導入する手順を紹介します。

1.pom.xmlwicket-auth-rolesを追加

<dependency>
  <groupId>org.apache.wicket</groupId>
  <artifactId>wicket-auth-roles</artifactId>
  <version>1.4.7</version>
</dependency>


2.AuthenticatedWebSessionの導入
AuthenticatedWebSessionを継承した新規クラスを追加します。

import org.apache.wicket.Request;
import org.apache.wicket.authentication.AuthenticatedWebSession;
import org.apache.wicket.authorization.strategies.role.Roles;

public final class MyAuthenticatedWebSession extends AuthenticatedWebSession {

	private static final long serialVersionUID = 1L;

	public MyAuthenticatedWebSession(Request request) {
		super(request);
	}

	@Override
	public boolean authenticate(final String username, final String password) {
		return username.equals("wicket") && password.equals("wicket");
	}

	@Override
	public Roles getRoles() {
		if (isSignedIn()) {
			return new Roles(Roles.ADMIN);
		}
		return null;
	}

}

3.AuthenticatedWebApplicationの導入
Applicationクラスの継承元をWebApplicationから、AuthenticatedWebApplicationに変更

import 既存のインポートクラス・・・
import org.apache.wicket.authentication.AuthenticatedWebApplication;
import org.apache.wicket.authentication.AuthenticatedWebSession;
import org.apache.wicket.authentication.pages.SignInPage;

public class FooApplication extends AuthenticatedWebApplication {

	@Override
	protected void init() {
		super.init();
		既存の実装・・・
	}

	@Override
	public Class<? extends Page> getHomePage() {
		既存の実装・・・
	}

	@Override
	protected Class<? extends WebPage> getSignInPageClass() {
		return SignInPage.class;

	}

	@Override
	protected Class<? extends AuthenticatedWebSession> getWebSessionClass() {
		return MyAuthenticatedWebSession.class;
	}
}

4.@AuthorizeInstantiationの導入
ログインしないと遷移できないページのクラスに@AuthorizeInstantiationアノテーションを追加
@AuthorizeInstantiation("ADMIN")

 @AuthorizeInstantiation("ADMIN")
 public class AdminPage extends BasePage {
  ・・・

結果

上記例では、未ログインの状態でAdminPageに遷移すると、自動的にログイン画面に当たるSignInPageが開きます。
ここで、ユーザ名="wicket"、パスワード="wicket"を入力しないと先に進めません。


補足説明

  • 今回はログイン画面として、組み込みのSignInPageを使用していますが、本来はこのクラスを継承した独自のPageを作成する必要があります。
  • MyAuthenticatedWebSession#authenticate(String,String)では、オンコードで判定を行っていますが、実際はここでDB等からユーザ情報を取得する必要があります。
  • MyAuthenticatedWebSession#getRoles()でisSignedIn()を使用してログイン判定をしていますが、スーパークラスのAuthenticatedWebApplicationでauthenticate()の戻り値をsignedInフィールドに保持しています。
public final boolean isSignedIn() {
    return signedIn;
}
public final boolean signIn(final String username, final String password) {
    return signedIn = authenticate(username, password);
}
  • 今回は、getRoles()でオンコーディングでRoles.ADMINを返していますが、ログインユーザによりロールを変える場合は、authenticate()でユーザ情報をセッション内に保持し、getRoles()内でユーザ情報から判定する必要があります。

その他

  • trunkのAuthenticatedWebSessionのインタフェースにauthenticate(String,String)が無い。将来は実装が変る?
  • IAuthorizationStrategyを使用すればWicket単体でも認証機能が導入できそう。wicket-auth-rolesを使うほうが多分簡単。
  • Wicketのexamplesは非常に多くのパッケージがあり、今回使用したauthenticationは他のサンプルに依存しているため、動作未確認。

 今回は、examplesのソースを元に手元のテストアプリを拡張して動作確認した。

iBATIS3

概要

iBatis3を導入する目的で評価用のサンプルコードを書いてみました。

参考にしたサイト

記載は書かれた日時が新しい順です。

Oboe吹きプログラマの黙示録

「iBATIS3 と Google guice」(2010/04/10の記事)
http://blogari.zaq.ne.jp/oboe2uran/
SqlSessionFactoryをシングルトンで使用することの問題についての考察があります。

「iBATIS3入門」(2010/03/14の記事)

http://sourceforge.jp/projects/artery/releases/46311
http://sourceforge.jp/projects/artery/downloads/46311/ibatis3.pdf/
ArteryというORマッパープロジェクト内にある、iBATIS3に関する日本語で書かれた入門書です。
分かりやすい内容でおすすめです。
xmlのにSQLのIN句を指定する方法
xmlSQLを書かずにアノテーションで指定する方法
等々についても説明があります。

公式サイトhttp://ibatis.apache.org/index.html

「User Guide (English)」(2010/02/15づけ)
http://svn.apache.org/repos/asf/ibatis/java/ibatis-3/trunk/doc/en/iBATIS-3-User-Guide.pdf
本家サイトの英語で書かれたガイドです。
最新の内容と思われますが、サンプルコードが不親切です。

CodeZine

iBATISを使ったO/RマッピングによるDBアクセスの実例」(2007/06/07の記事)
http://codezine.jp/article/detail/1289
バージョン2.3に対しての説明です。


サンプルの内容

MySQLのUSERSテーブルに対しCRUDを行う。

DROP TABLE IF EXISTS users;
CREATE TABLE users (
  user_id INT NOT NULL AUTO_INCREMENT,
  user_name VARCHAR(10),
  version INT,
  PRIMARY KEY (user_id)
) ENGINE=InnoDB;

・idは自動採番
・更新時にはvarsionで楽観的排他制御をおこなう
・varsionはSQLで自動インクリメンタル(Javaでは意識しない)

pom.xml

下記を追加

<dependency>
  <groupId>org.apache.ibatis</groupId>
  <artifactId>ibatis-sqlmap</artifactId>
  <version>3.0-beta-10</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.10</version>
</dependency>

サンプルソース構成

src/test/ibatis
    /configuration
        /AppSqlSessionFactory.java
        /iBatisConfiguration.xml

    /entity
        /User.java
        /UserId.java
        /UserMapper.xml

サンプルソース

AppSqlSessionFactory.java

package test.ibatis.configuration;

import java.io.IOException;
import java.io.Reader;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class AppSqlSessionFactory {
	private final static String XML_PATH = "test/ibatis/configuration/iBatisConfiguration.xml";

	public static SqlSessionFactory get() {
		try {
			Reader reader = Resources.getResourceAsReader(XML_PATH);
			return new SqlSessionFactoryBuilder().build(reader);
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}
}

iBatisConfiguration.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-config.dtd">
<configuration>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.jdbc.Driver" />
				<property name="url" value="jdbc:mysql://localhost/iBatisTest" />
				<property name="username" value="root" />
				<property name="password" value="ponyo" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="test/ibatis/entity/UserMapper.xml" />
	</mappers>
</configuration>

User.java

package test.ibatis.entity;

public class User {
	private long id;
	private String name;
	private long version;

	// 新規追加の場合
	public User(String name) {
		this(-1L, -1L);
	}

	// iBatisにより生成される場合
	public User(Long id, Long version) {
		this.id = id;
		this.version = version;
	}

	public long getId() {
		return id;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public long getVersion() {
		return version;
	}

}

UserId.java

package test.ibatis.entity;

public class UserId {
	private long id;

	public long get() {
		return id;
	}
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="USERS">

	<resultMap id="UserMapping" type="test.ibatis.entity.User">
		<constructor>
			<idArg column="user_id" javaType="long" />
			<idArg column="version" javaType="long" />
		</constructor>
		<result property="name" column="user_name" />
	</resultMap>

	<select id="findById" parameterType="long" resultType="test.ibatis.entity.User" resultMap="UserMapping" >
		SELECT user_id, user_name, version
		FROM users
		WHERE user_id = #{value}
	</select>

	<insert id="insert" parameterType="test.ibatis.entity.User">
		INSERT INTO users (user_name, version)
		VALUES (#{name},1)
	</insert>

	<select id="getInsertedId" resultType="test.ibatis.entity.UserId" >
		SELECT LAST_INSERT_ID() id;
	</select>

	<update id="updateById" parameterType="test.ibatis.entity.User">
		UPDATE users
		SET
			user_name = #{name},
			version = version + 1
		WHERE
			user_id = #{id} AND
			version = #{version}
	</update>

	<delete id="deleteById" parameterType="long">
		DELETE FROM users
		WHERE user_id = #{id}
	</delete>
</mapper>

テスト

検証のためさらにUnitTestを追加します。
pom.xmlに追加

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.8.1</version>
  <scope>test</scope>
</dependency>

UserTest.java

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;

import test.ibatis.configuration.AppSqlSessionFactory;
import test.ibatis.entity.User;
import test.ibatis.entity.UserId;

public class UserTest {

	@Test
	public void test() throws Exception {
		SqlSessionFactory factory = AppSqlSessionFactory.get();
		SqlSession session = factory.openSession();

		try {

			// 新規追加
			User user = new User("ユーザ名");
			user.setName("ユーザ名");
			long count = session.insert("USERS.insert", user);
			assertEquals(1, count);// 追加件数が1件であることを確認

			// 採番されたidを取得
			UserId userId = (UserId) session.selectOne("USERS.getInsertedId");
			long id = userId.get();

			// 新規追加したUserを取得
			user = (User) session.selectOne("USERS.findById", id);
			assertEquals(id, user.getId());
			assertEquals("ユーザ名", user.getName());
			assertEquals(1, user.getVersion());

			// 更新
			user.setName("ユーザ名改");
			count = session.update("USERS.updateById", user);
			assertEquals(1, count);// 更新件数が1件であることを確認

			// 更新結果確認
			user = (User) session.selectOne("USERS.findById", id);
			assertEquals(id, user.getId());
			assertEquals("ユーザ名改", user.getName());
			assertEquals(2, user.getVersion());//バージョンがインクリメントされたことを確認

			session.commit();

		} catch (Exception e) {
			e.printStackTrace();
			session.rollback();
			fail(e.getMessage());
		} finally {
			session.close();
		}
	}

}

結果

テストが全て通過しました。うまく動いているようです。

追加説明

SqlSessionFactoryBuilder

公式サイトのユーザーズガイド(後述)には、SqlSessionFactoryBuilderについて下記のように書かれています。

This class can be instantiated, used and thrown away. There is no need to keep it around once you've
created your SqlSessionFactory. Therefore the best scope for instances of SqlSessionFactoryBuilder is
method scope (i.e. a local method variable). You can reuse the SqlSessionFactoryBuilder to build
multiple SqlSessionFactory instances, but it's still best not to keep it around to ensure that all of the XML
parsing resources are freed up for more important things.

SqlSessionFactoryBuilderのベストスコープはメソッドスコープだとありますが、ビジネスロジック(=1トランザクション)毎にnewという解釈でいいのでしょうか?
今回のサンプルでは毎回newしていますが、SqlSessionFactoryをシングルトンで管理できるならそちらの方がよさそうです。

UserId.java

usersテーブルの自動採番されたidを取得するだけのためにあるクラスです。マッパーXMLのresultMapは、テーブルのカラム と JavaBeanのフィールド が1対1で対応している場合は省略できるため、UserIdに対するresultMapは記述していません。SELECT結果をJavaBeanにマッピングする際はクラスのフィールドに直接挿入されるようで、setterは無くても大丈夫でした。

Eclipse3.5 + Maven(m2eclipse) + WTP

概要

EclipseMavenプラグインのm2eclipseを使用した場合、WTPとの連携が悪いのか、mavenプロジェクトを作ってもサーバに追加できるプロジェクトとしては認識されていませんでした。今までは「動的Webプロジェクト」をMavenのディレクトリ構成にあわせて作成してから、「プロジェクトメニュー>Maven>依存関係管理を使用可能にする」 を選択という手順で対応していたのですが、今日調べてみたら状況が改善されています。

以下の記述は、Windows上でPleiades All In One Ultimate(3.5.2 Galileoベース)を使用して作業しています。

Eclipseへのインストール手順

1.m2eclipseのupdateサイトからプラグインをインストール
http://m2eclipse.sonatype.org/sites/m2e/
EclipseMaven 統合


2.WTP用のプラグインをインストール
http://m2eclipse.sonatype.org/sites/m2e-extras
WTPMaven 統合

プロジェクトの作成

1.プロジェクト・エクスプローラーで右クリック > 新規 > プロジェクト
2.新規プロジェクトダイアログで、MavenMaven Project
3.新規 Maven プロジェクトダイアログで、次へ > グループ Idで「maven-archetype-webapp」を選択
4.以下省略

結果

この手順で、プロジェクトがMavenのディレクトリ構成で作られて、サーバの「追加および除去」ダイアログでも選択可能になりました。
相変わらずsrc/main/javaディレクトリが作られてないのが惜しい。

m2eclipseのサイト

m2eclipse
http://m2eclipse.sonatype.org/index.html

その他参考にさせていただいたサイト

@//メモ
Maven Eclipseとの連携
http://hondou.homedns.org/pukiwiki/pukiwiki.php?Maven%20Eclipse%A4%C8%A4%CE%CF%A2%B7%C8



ぼそっと
EclipseWTPApache Ivy v.s m2eclipseその1 - 最近のm2eclipse
http://d.hatena.ne.jp/chiba_mk3/20080718/1216460791