コントローラ層は、クライアントからのリクエストを受け付け、ビュー層とドメイン層を調整します。コントローラ層は画面遷
移、セッション管理やメッセージの管理などを担当します。コントローラの実装はプレゼンテーション形態によって異なり
ます。United Front 2 のプレゼンテーションは Web です。Web に関するクラスは org.unitedfront2.web
以下のパッケージに、コントローラの Web 実装は org.unitedfront2.web.controller
に収録しています。
United Front 2 の Web コントローラ (HTML/XML) は Spring Web Flow をベースとしています。ウィジェットなどの画面の一部分に対するコントローラは Apache Tiles 2 を利用しています。ここでは Spring Web Flow や Tiles に関する詳細な説明は避け、サンプルソースコードを中 心に解説します。これらのライブラリの詳細な使用方法は本家のドキュメントを参照して下さい。
Web (HTML) の画面遷移は Spring Web Flow を採用しています。
サンプルで登場するシナリオはメールアドレスの変更機能で、ユースケースは次のとおりです。メールアドレス変更時に パスワードを再発行しているのは、入力されたメールアドレスが正しいものであることを確認するためです。

アクションクラスは次のような処理を担当します。
トランザクション制御には、 宣言的トランザクション制御 を採用しているため、プログラマがトラ
ンザクションに関するコードを書くことはほとんどありません。トランザクション制御の範囲は
Action
インターフェースの execute メソッドの開始から終わりまでです。アクションクラスのアクションメ
ソッドが例外を放った時点でロールバックを実行します。
United Front 2 では入力フォーム処理用のアクションクラス
org.unitedfront2.web.controller.FormAction
を提供しています。このクラスは Spring オリジナルの org.springframework.webflow.action.FormAction
を継承しており、さらに以下の機能を加えています。
United Front 2 におけるフォーム処理は通常 United Front 2 の FormAction を継
承して実装します。
Spring MVC は Struts (1.x) に代表される従来 の Web MVC フレームワークからいくつかの改良点が加えられています。その改良点の一つに、クライアントからのリク エストを直接 JavaBean オブジェクトにバインドする仕組みが挙げられます。この機能を利用すれば、United Front 2 のドメインオブジェクトをビュー層からドメイン層まで一貫して利用できます。また、Spring Web Flow を 採用する利点には次のようなものがあります。
MultiAction
が持つべき代表的なアクションメソッドを次に示します。アクションメソッドについては Spring Web Flow のドキュメ
ント ActionState XML - multi action
を参照ください。
| メソッド名 | 説明 |
|---|---|
init |
フローの初期処理を行います。 |
setupForm |
フォーム画面表示前の処理を行います。例えば、フォーム画面に表示するデータをリクエストスコープに設定します。 |
bindAndValidate |
クライアントリクエストからドメインオブジェクトを作成(バインド)し、ドメインオブジェクトの検証を行います。実際のバインドのロジックおよび検証ロジックは、それぞれ doBind メソッド、 doValidator メソッドに記述します。 |
view |
情報を画面に表示します。 |
sendMail |
メールを送信します。実際の送信ロジックは doSendMail メソッドに記述します。 |
finish |
フローの終了処理を行います。 |
アクションクラスのサンプル MailAddrChangeAction
を示します。initAction メソッドでは、
ドメインモデルの指定と e メール送信用テンプレート MailAddrChange-mail を設定しています。メールテンプ
レートについてはビュー層で解説しています。完全なコードは MailAddrChangeAction のソースコード
を参照してください。
// アノテーションベースの Spring Bean 定義。
// Bean 名は ${機能名}.${先頭が小文字のクラス名}
@Repository(value = "account.mailAddrChangeAction")
public class MailAddrChangeAction extends FormAction {
@Autowired
private AccountTable accountTable;
@Override
protected void initAction() {
super.initAction();
setFormObjectClass(Account.class);
setMailTemplateName("MailAddrChange-mail");
}
/** メールアドレスの変更とパスワードの再発行 */
public Event changeMailAddrAndReissuePassword(RequestContext context)
throws MailAddrUsedByOtherException {
int id = WebUtils.getAccount(context).getId();
String newMailAddr = ((Account) getFormObject(context)).getMailAddr();
String newPassword = Account.createRandomPassword(4, 8);
Account newAccount = accountTable.get(id);
newAccount.setMailAddr(newMailAddr);
newAccount.setPassword(newPassword);
newAccount.encrypt();
try {
newAccount.store();
} catch (MailAddrUsedByOtherException e) {
// メールアドレスが登録済みであるため、エラー処理
// エラーメッセージを設定した後、例外をそのまま投げています。
// 例外発生後の動作はフロー定義ファイル側で定義されます。
logger.warn(e.getMessage());
Errors errors = getBindingErrors(context, newAccount);
errors.rejectValue("mailAddr",
"account.validation.mailAddrUserdByOther",
new Object[] {e.getMailAddr()}, e.getMessage());
throw e;
}
// ビューへ転送するデータを設定
context.getRequestScope().put("mailAddrAndPassword",
new MailAddrAndPassword(newAccount.getMailAddr(), newPassword));
return success();
}
// 検証クラスをインジェクション
@Override
@Resource(name = "account.mailAddrFormValidator")
public void setValidator(Validator validator) {
super.setValidator(validator);
}
}
メールアドレスが既に使用されていた際に、error() メソッドではなく例外を発生させてメソッドを
終了している点に注意してください。これにより、トランザクションをロールバックしています。
error() メソッドを使用した場合、ロールバックは実行されません。
initBinder をオーバーライドする
必要に応じて initBinder メソッドをオーバーライドし、クライアントからのリクエストを制限てくだ
さい。Spring のデフォルトの設定では、クライアントから送信された全てのプロパティをドメインオブジェクトに設定しよ
うと試みます。これは場合によってはセキュリティ上大変危険です。予期しない値がドメインオブジェクトのプロパティに設
定されないようにするためには、FormAction の initBinder メソッドを
オーバーライドし、DataBinder
に許可するプロパティを設定します。
@Override
protected void initBinder(RequestContext context, DataBinder binder) {
super.initBinder(context, binder);
binder.setAllowedFields(new String[] {"mailAddr"});
}
画面入力項目の検証クラスはドメイン層の検証クラスを用いて実装します。画面入力項目の検証クラスは
SpringValidator
インターフェースを実装する必要があります。実装は SpringValidatorSupport
を使って簡素化できます。
SpringValidator は United Front 2 のフォームアクション FormAction
で使われています。FormAction は SpringValidator から
オリジナルの検証クラスを取り出し、それをビューへ転送しています。

入力値検証クラスのサンプル MailAddrFormValidator
を示します。MailAddrFormValidator はアカウントのメールアドレスの値の検証を行います。
完全なソースは MailAddrFormValidator のソースコード
を参照してください。
@Repository(value = "account.mailAddrFormValidator")
public class MailAddrFormValidator
extends SpringValidatorSupport<AccountValidator, Account> {
@Override
protected void doValidate(Account account, Errors errors) {
try {
getOriginalValidator().validateMailAddr(account);
} catch (ValidationException e) {
rejectValue("mailAddr", e, errors);
}
}
@Override
@Autowired
public void setOriginalValidator(AccountValidator originalValidator) {
super.setOriginalValidator(originalValidator);
}
}
画面遷移を含む処理の流れを Flow 定義ファイルに記述します。主要な Flow ID には次のようなものがあります。
| ID | 説明 |
|---|---|
init |
フローの初期処理実行状態に付与する ID です。通常 action-state タグに付与されます。通常アクションメソッド init を利用します。 |
form |
フォーム処理実行状態に付与する ID です。通常 action-state タグに付与されます。通常アクションメソッド setupForm と bindAndValidate を利用します。 |
mailSend |
メール送信処理実行状態に付与する ID です。通常 action-state タグに付与されます。通常アクションメソッド sendMail を利用します。 |
finish |
フローの終了処理実行状態に付与する ID です。通常 end-state タグに付与されます。 |
メールアドレス変更フォームから完了までの流れをフロー定義 mailAddrChange-flow.xml
に記述しています。実際に認証を行っているのは認証サブフロー authentication-sub-flow.xml
です。メールアドレスの変更とパスワードの再発行を行うアクションステート
changeMailAddrAndReissuePassword では、メールアドレスが既に他者に使われてしまっ
ている場合に生じる MailAddrUsedByOtherException をキャッチすると、再度フォームス
テート form へ遷移します。次のサンプルは説明のため実際のコードから若干変更しています。
<start-state idref="form"/>
<!-- メールアドレス変更フォーム -->
<view-state id="form" view="account.MailAddrChangeForm">
<render-actions>
<action bean="account.mailAddrChangeAction" method="setupForm"/>
</render-actions>
<transition on="submit" to="authenticate">
<action bean="account.mailAddrChangeAction" method="bindAndValidate"/>
</transition>
</view-state>
<!-- 認証サブフローへ -->
<subflow-state id="authenticate" flow="account/authentication-sub-flow">
<transition on="authenticated" to="change"/>
</subflow-state>
<!-- メールアドレス変更処理 -->
<action-state id="change">
<action bean="account.mailAddrChangeAction" method="changeMailAddrAndReissuePassword"/>
<transition on="success" to="mailSend"/>
<transition on-exception="org.unitedfront2.domain.MailAddrUsedByOtherException" to="form"/>
</action-state>
<!-- e メールの送信 -->
<action-state id="mailSend">
<action bean="account.mailAddrChangeAction" method="sendMail"/>
<transition on="success" to="finish"/>
</action-state>
<!-- メールアドレス変更完了画面 -->
<end-state id="finish" view="account.MailAddrChangeSuccess"/>
この Web Flow を Spring IDE を使って図示すると 次のようになります。

URL は WEB-INF/flows フォルダの階層とフロー定義ファイル名で決まります。
WEB-INF/flows/account/mailAddrChange-flow.xml
に配置されているメールアドレス変更画面遷移は次の URL になります。
http://HOST_NAME/unitedfront2/account/mailAddrChange.html
例外として、*-sub-flow.xml という名前のフロー定義ファイルはサブフローとみなされ、URL から直接アクセスす ることはできないようになっています。
URL は mailAddrChange.html とすべきでしょうか?それとも changeMailAddr.html とすべきでしょうか ?前者は単語、後者は動詞を表現しているように思えます。URL - Uniform Resource Locator - は「リソースを指すもの」という考えから、United Front 2 では前者を採用しました。もしこの URL がメールアドレスに関して包括的に扱うページなら、単に mailAddr.html としても良いかもしれません。
URL の仕組みは United Front 2 の独自クラス HierarchyXmlFlowRegistryFactoryBean
と UriFlowExecutorArgumentHandler
を使って実現しています。
資源への識別子はパラメータではなく URL 本文に埋め込んでいます。ここでいう資源とは、Resource
インターフェースを実装したドメインオブジェクトのことです。例えば、プロフィールへの URL はパラメータを用いて通常①
のように表現できますが、United Front 2 では基本的に②のようにパラメータを埋め込んだ URL を使うようにし
ています。
① http://HOST_NAME/unitedfront2/profile/view.html?userCode=${userCode}
② http://HOST_NAME/unitedfront2/profile/${userCode}/index.html
これによって、各資源へクエリなしでアクセスできるようになります。この実装は UrlRewriteFilter を使っています。設定ファイ ル urlrewrite.xml に次のように記述するだけで簡単に実装できます。
<rule> <from>profile/([a-zA-Z0-9]+)/index\.html</from> <to>profile/view.html?userCode=$1</to> </rule>
なお、ディレクトリを直接参照した際には index.html ファイルへリダイレクトします。
このルールは、ブラウザのアドレスバーに表示される - つまりユーザが認識できる - URL にのみ適用しています。 例えば、アクセスした後、他の URL にリダイレクトするような一時的に使われるだけの URL にはこのルールを適用し ていません。
HTML の TITLE タグは、TitleInterceptor
によってビュー名 - ビュー層の項で説明する Tiles 定義で属性 name に設定される値 - と一
致するメッセージコードのメッセージが選択されます。ただしこれはデフォルトの挙動で、コントローラ内でタイトルを変更
することも可能です。
RequestContext context..
context.getRequestScope().put(
TitleInterceptor.TITLE_CODE_PARAM_NAME, "message.code");
context.getRequestScope().put(
TitleInterceptor.TITLE_ARGS_PARAM_NAME, new Object[] {"argument"});
システムロールのアクセス制御を Spring Security を使って実装しています。システムロールとは、アカウントの認証や管理などのシステム運用上必要となる権限のことで す。細かな承認処理に対するアクセス制御は各アクションクラスで実装します。システムロールには次の3種類が存在 します。
| ロール名 | 説明 |
|---|---|
USER_ROLE |
アカウントを保持する一般ユーザ。 |
ADMIN_ROLE |
システム管理者。 |
ROLE_ANONYMOUS |
アカウントを持っていないか、持っていてもログインしていないユーザ。 |
このアクセス制御では、ユーザからのアクセスを URL レベルで制御できます。デフォルトの設定では、全ての HTML
ファイルは ROLE_USER からのみのアクセスを許可しています。必要に応じて設定ファイル applicationContext-security.xml
に定義されている filterInvocationInterceptor Bean のプロパティを編集します。次
の例では、URL '/informationUpdate.html' に対するアクセスをシステム管理者のみに制限しています。
<bean id="filterInvocationInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="objectDefinitionSource">
<security:filter-invocation-definition-source>
<!-- ... -->
<security:intercept-url pattern="/informationUpdate.html" access="ROLE_ADMIN"/>
<!-- ... -->
<security:intercept-url pattern="/**" access="ROLE_USER"/>
</security:filter-invocation-definition-source>
</property>
</bean>
この設定では、匿名ユーザによる不正な HTML ファイルのアクセスは「アクセス拒否」となります。存在しない HTML ファイルにアクセスした際も「アクセス拒否」扱いとなります。このとき、「アクセス拒否」ページではなく、トップページへ 遷移しますが、これは Spring Security のデフォルトの動作になります。
「Page Not Found」や「Internal Server Error」といったページに遷移させたい場合、アクションクラスで指 定された例外を発生させることで簡単に実現できます。例外の対応表を次に示します。
| エラー内容 | エラーコード | 例外 |
|---|---|---|
| リクエストが不正 | 400 | org.unitedfront2.web.BadRequestException
,
org.unitedfront2.web.flow.BadRequestException
,
ServletRequestBindingException
,
ConversionException
|
| アクセス拒否 | 403 | org.unitedfront2.web.AccessDeniedException
,
org.unitedfront2.web.flow.AccessDeniedException
|
| ページが見つからない | 404 | org.unitedfront2.web.PageNotFoundException
,
org.unitedfront2.web.flow.PageNotFoundException
,
NoSuchFlowDefinitionException
|
| 内部エラー | 500 | 他の例外 |
United Front 2 が提供する例外クラスは org.unitedfront2.web.flow
および org.unitedfront2.web
の二つのパッケージに存在します。アクションクラスで例外を発生する際は前者を、それ以外は後者のパッケージを利
用します。アクションクラスにおける例外処理のサンプルコードを次に示します。
@Override
public Event doExecute(RequestContext context) throws PageNotFoundException {
String code = context.getRequestParameters().get(codeParamName);
String code = context.getRequestParameters().get("code");
Blog blog = blogTable.findByCode(code);
if (blog == null) {
String message = "The blog [Code='" + code + "'] not found.";
logger.warn(message);
throw new PageNotFoundException(context, this, message);
}
// ...
}
Apache Tiles 2 の View Preparers 機能を使って、画面の一部に小さな Web コントローラを設けることができます。
公開中のプロフィールを、プロフィールウィジェットに渡す PublicProfileWidgetPreparer
を示します。完全なコードは PublicProfileWidgetPreparer のソースコード
を参照ください。実際のコードでは、キャッシュを使ってデータアクセスを効率化しています。
@Repository(value = "profile.publicProfileWidgetPreparer")
public class PublicProfileWidgetPreparer implements ViewPreparer {
/** プロフィールテーブル */
@Autowired
private ProfileTable profileTable;
@Override
public void execute(TilesRequestContext tilesContext,
AttributeContext attributeContext) throws PreparerException {
attributeContext.putAttribute("users",
new Attribute(profileTable.getPublicProfileOwners()));
}
}
RSS の実装には Rome を利用しています。RSS のコン トローラは Spring によって管理されますが、Web Flow ではなく、Spring MVC のコントローラとして実装しま す。
サンプルコードとして、ブログの RSS 用コントローラ RssController
の一部を紹介します。このコントローラでは、取得したブログドメインオブジェクトからフィードを生成し、ビューへ転送して
います。フィードタイプやエンコードはビュー側で設定されます。また、setLink に設定している
URL はプロトコル、ホスト名、コンテキスト名が設定されていませんが、これらもビュー側で正しく設定されるためここで
は設定不要です。ビューは RSS 専用の RssView
です。RssController の完全なコードは RssController のソースコード
を参照してください。
@Repository(value = "blog.rssController")
public class RssController extends AbstractController {
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request,
HttpServletResponse response) throws Exception {
Blog blog = getBlog(request);
SyndFeed feed = toFeed(blog, WebUtils.getLocale(request));
ModelAndView mav = new ModelAndView(new RssView());
mav.addObject(RssView.FEED_PARAM_NAME, feed);
return mav;
}
private Blog getBlog(HttpServletRequest request) {
// ...
}
private SyndFeed toFeed(Blog blog, Locale locale) {
SyndFeed feed = new SyndFeedImpl();
// 概要
feed.setTitle(blog.getOverview().getSubject(locale));
feed.setLink("/blog/" + blog.getCode() + "/index.html");
feed.setDescription(blog.getOverview().getBody(locale));
feed.setAuthor(HtmlUtils.htmlEscape(blog.getOwner().getName()));
// 記事
List<SyndEntry> entries
= new ArrayList<SyndEntry>(blog.getEntries().size());
for (BlogEntry be : blog.getEntries()) {
SyndEntry entry = new SyndEntryImpl();
entry.setTitle(be.getEntry().getSubject(locale));
entry.setLink("/blog/" + blog.getCode() + "/entry-"
+ be.getCode() + ".html");
SyndContent description = new SyndContentImpl();
description.setType("text/html");
description.setValue(be.getEntry().getBody(locale));
entry.setDescription(description);
entry.setPublishedDate(be.getEntry().getRegistrationDate());
entry.setUpdatedDate(be.getEntry().getLastUpdateDate());
entries.add(entry);
}
feed.setEntries(entries);
return feed;
}
}
RSS を保持するページでは、RSS への参照を HEAD タグ内の LINK タグで
設定する必要があります。これには、対象ページのコントローラで RSS への参照を保持する _feedUrl
変数を定義します。
@Repository(value = "blog.viewAction")
public class ViewAction extends AbstractAction {
@Override
protected Event doExecute(RequestContext context) {
// ...
context.getRequestScope().put("_feedUrl", "/blog/" + blog.getCode()
+ "/index.xml");
return success();
}
}
最後に、applicationContext-servlet.xml にコントローラと URL をマッピングします。
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<!-- ... -->
<property name="mappings">
<props>
<prop key="/**/*.html">flowController</prop>
<prop key="/blog/view.xml">blog.rssController</prop>
</props>
</property>
</bean>