Struts2のチュートリアル(後半)

前置き

タイトルのままで、前半からの続きです。主に公式サイトのチュートリアルを参考にしました。環境は、Gentoo、Apache2、Tomcat6、Struts2です。

strutsにはパッケージという概念があります。文脈によりjavaのpackageとは区別できると思いますが、念のため、strutsのパッケージは「パッケージ」や<package>と表記し、javaのpackageはpackageと表記します。

目次

前半の記事では、下記のトピックについて述べました。

  • Struts2のメカニズム
  • Struts2アプリのディレクトリ構成例
  • Hello, world的アプリ
  • Interceptor
  • デバッグ
  • メッセージリソース

本記事(後半)は、こんな感じ:

  • convention-plugin(struts2-convention-plugin.jar)
  • Exception
  • JSPのタグ
  • フォーム関連のタグ
  • Validation
  • Theme
  • ワイルドカード・メソッド・選択(Wildcard Method Selection)

本文

convention-plugin(struts2-convention-plugin.jar)

convention-pluginを使うと、struts.xmlを簡略化することができます(場合によっては、struts.xmlを廃止できるかもしれません)。例えば、アクション名やActionクラス名を以下の規則で命名する場合、struts.xmlの<action>は不要です。

  • アクション名は、aaa-bbb-cccのようにハイフンで区切る(aaaのように1単語でも可)
  • Actionクラス名は、AaaBbbCccActionのように、アクション名をキャメルケース化して"Action"を付ける
  • com.opensymphony.xwork2.Actionをimplementsしていれば、"Action"は付けなくてもいいかも
  • Actionクラスのpackageは、.struts, .struts2, .action, .actionsで終わるか、そのサブpackageとする
  • メソッド名は、execute()
  • JSPは/WEB-INF/content/の下に置く
  • JSP名は、aaa-bbb-ccc.jspか、cccの後ろにexecute()が返す文字列を付けたもの

一例を挙げます。

アクション名(URLのパス)/APPNAME/xxx/yyy/aaa-bbb-ccc
アクションクラスcom.example.struts.xxx.yyy.AaaBbbCccAction
メソッドexecute()
JSPaaa-bbb-ccc.jsp、aaa-bbb-ccc-success.jsp、aaa-bbb-ccc-error.jsp

convention-pluginとstruts.xmlの<action>を併用することも可能ですが、アクション名が重複しないように注意して下さい。例えば、aaaというアクション名に関する定義がstruts.xmlの<action>にあり、かつcom.example.struts.AaaActionクラスも存在するような場合は、<action>の方が無視されconventionの方が採用されるようです。なので、対応するJSPを/WEB-INF/content/の下に置いておかないとエラーになります。

ところで上記の規則は、プロパティやAnnotationによりカスタマイズすることが可能です。

プロパティ

struts.convention.result.path
JSP置き場。ただし/WEB-INF直下はダメみたい。
struts.convention.package.locators
package名。デフォルト値は"struts,struts2,action,actions"
struts.convention.exclude.packages
特定のパッケージを検索対象から除外。やってみたけど効き目が無かった。
struts.convention.action.packages
特定のパッケージを検索対象に指定。excludeと組み合わせて使うのかな?

Annotation

@Actionにより、アクションとメソッドを対応付けることができます。

@Action("/custom/myhello")
public String execute() {
    return SUCCESS;
}

この場合、アクション名(URLのパス)は/APPNAME/custom/myhelloとなり、JSPは/WEB-INF/content/custom/myhello.jsp(またはmyhello-success.jspとか)になります。

複数のアクションを1つのメソッドに対応付ける場合は、@Actionを@Actions()で囲みます。

@Actions({
    @Action("/custom/myhello"),
    @Action("/custom/mylogin")
})
public String execute() {
    return SUCCESS;
}

アクション結果とJSPを対応付けるには、@Resultsと@Resultを使います。

@Results({
  @Result(name="input", location="search-books-form.jsp"),
  @Result(name="success", location="search-books.jsp")
})
public class SearchBooksAction extends ActionSupport {
  ...
}

プロパティの定義場所

プロパティは普通struts.xmlの<constant>で定義しますが、web.xmlに移管することも可能です。

<filter>
  <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
      <init-param>
        <param-name>struts.devMode</param-name>
        <param-value>true</param-value>
      </init-param>
</filter>

アクションのチェーン

例えばアクション名がfooで、execute()が"bar"をreturnしたとき、以下の条件を満たせば、アクションがチェーンします。

  • "bar"に対応するJSPが無い
  • 同一package内に、"foo-bar"というアクションに対応するActionクラスとメソッドがある

Exception

  • uncaughtなexceptionへの対処をstruts.xmlに記述する
  • <package>の子要素として<global-results>と<global-exception-mappings>を書く
  • <global-results>の子要素には<result>を列挙する
  • <global-exception-mappings>の子要素には<exception-mapping>を列挙する
  • <global-exception-mappings>よりも先に<global-results>を書くこと(不可解だが、逆に書くと全ページが404エラーになる)
<package name="struts2" extends="struts-default">
  ....

  <global-results>
    <result name="securityerror">/securityerror.jsp</result>
    <result name="error">/error.jsp</result>
  </global-results>

  <global-exception-mappings>
    <exception-mapping
      exception="org.apache.struts.register.exceptions.SecurityBreachException"
      result="securityerror" />
    <exception-mapping
      exception="java.lang.Exception"
      result="error" />
  </global-exception-mappings>
</package>

convention-pluginでマッピングされたアクション内で例外が発生しても、<global-exception-mappings>では捕まりません。convention-pluginをすり抜けて、struts.xml内の<action>でマッピングされたアクションの場合は例外が捕まります。

例外処理を特定のActionのみに適用したい場合は、<action>の子要素として<exception-mapping>と<result>を書きます。

<action name="top" class="jp.dip.gpsoft.gplib.pub.WelcomeAction" method="execute">
  <result name="success">/welcome.jsp</result>

  <exception-mapping exception="java.lang.Exception" result="error" />
  <result name="error">/error.jsp</result>
</action>

ExceptionオブジェクトはValueStackにプッシュされるのでJSPから参照することができます。このときの識別名はexceptionですが、なぜか、#は付けません。

<h1>エラー発生</h1>
<s:property value="%{exception.message}" />

JSPのタグ

property

  • ValueStackのデータを参照する
<s:property value="%{message}" />
<s:property value="book.title" />

url

  • アクションに飛ばす
  • 単純なリンク
  • リクエストパラメータ付きで定義してアクションに飛ばす
<a href="<s:url action='hello' />">Hello</a>

<link rel="stylesheet" type="text/css" href="<s:url value='/main.css' />" />

<s:url action="new-books" var="newBooksLink">
  <s:param name="state">new</s:param>
</s:url>
<a href="${newBooksLink}">新着図書</a></p>

${}を使った表記はJSPのEL(Expression Language)によるもので、StrutsやOGNLとは無関係です。var属性ではなく、id属性で名前を指定するとOGNLで参照できます。

<s:url action="new-books" id="newBooksLink">
  <s:param name="state">new</s:param>
</s:url>
<a href="<s:property value='%{#newBooksLink}' />">新着図書</a></p>

if/else

  • test属性に条件を指定
  • <else>は必要に応じて
<s:if test="%{new}">[新着]</s:if>
<s:else>[]</s:else>

この場合、Actionクラスには、booleanを返すisNew()を実装しておきましょう。

iterator

<table>
  <tr>
    <th>Title and Author</th>
    <th>Description</th>
  </tr>
  <s:iterator value="bookList" var="book" status="sta">
    <tr class="<s:if test='#sta.even'>even</s:if><s:else>odd</s:else>">
      <td><s:property value="top"/></td>
      <td><s:property value="description"/></td>
    </tr>
  </s:iterator>
</table>

iteratorは、value属性に指定したコレクションからオブジェクトを順に取り出してValueStackにプッシュします。そのオブジェクトはvar属性に指定した名前で参照できます。よって上記の例では、"top"の代わりに"%{#book}"や"#book"と書いても構いません。2列目の値は(Actionクラスではなく)BookクラスのgetDescription()で得ています。ループの中でActionインスタンスにアクセスする場合の表記は、#actionでOKのようです。あるいは、ループの前に<s:set>するという手もありますね。

status属性に指定した名前を使って、ループの状態を参照することができます。これはorg.apache.struts2.views.jsp.IteratorStatusクラスのインスタンスで、isEven()やisLast()といったメソッドを持っています。

CSSでtr.evenやtr.oddに適当な背景色を付けると、こんな感じに表示されます。

tableタグ

text

  • メッセージリソースから文字列を取り出す
  • name属性でリソースのキー名を指定
<s:text name="welcome" />

action

  • JSP内でアクションを実行し、ActionインスタンスをValueStackにプッシュ(トップに来る訳ではない)
  • name属性にアクション名
  • var属性に、OGNLで参照するときの名前
<s:action name="inc-counter" var="counter" />
You're the <s:property value="#counter" />th visitor.

set

  • 「setする」の意味(集合という意味ではない)
  • 何度も参照するデータをValueStackにプッシュしておく(トップに来る訳ではない)
  • value属性に値
  • var属性に、OGNLで参照するときの名前
<s:set value="book.primeAuthor.firstName" var="fn" />
FirstName: <s:property value="#fn" />

head

  • <head>内に、いくつかの子要素を追加する
  • デフォルトでは、CSSとJavaScriptを読み込む
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<s:head />
<title>GP図書館</title>
</head>

actionerror

  • エラーメッセージを表示
  • エラーメッセージは、アクション実行時にaddActionError()で登録する
  • <s:actionerror />

例を挙げます。

  • WelcomeAction.java
public String execute() throws Exception {
    addActionError("You're NOT welcome.");
    addActionError("Please get out of here.");
    return SUCCESS;
}
  • welcome.jsp
<body>
<s:actionerror />
<h1><s:text name="welcome" /></h1>

アクションエラー

メッセージが赤字になっているのは、<s:head />によって読み込まれたCSSのおかげです。

includeとpush

  • 別JSPをインクルード
  • value属性にJSPファイル名
  • インクルードするJSPに引数的な情報を渡したい場合は、<s:push>でValueStackにプッシュしておく
  • <s:push>のvalue属性にOGNLのハッシュを使うと、複数の情報を渡せる

例を挙げます。

  • all-books.jsp
<body>
<h1>All Books</h1>

<s:push value="#{ 'actionName' : 'all-books' }">
  <s:include value="pager.jsp"/>
</s:push>
  • search-books.jsp
<body>
<h1>Search Result</h1>

<s:push value="#{ 'actionName' : 'search-books' }">
  <s:include value="pager.jsp"/>
</s:push>
  • pager.jsp
<%@ page language="java" contentType="text/html; utf-8"
    pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<div class="pager">

<s:url action="%{actionName}" var="goNextPage">
  <s:param name="pageIx"><s:property value="pager.nextPageIx"/></s:param>
</s:url>
...

<a href="${goNextPage}">Next</a>
...

</div>

フォーム関連のタグ

まずは例を示します。

<s:form action="search-books">
  <s:textfield name="title" label="タイトル" />
  <s:textfield key="author" />
  <s:select key="language" list="langStringList" />
  <s:radio key="language" list="langList" listKey="key" listValue="value" />
  <s:checkboxlist name="languages" label="言語(複数可)" list="%{#{'JP':'Japan', 'US':'United States'}}" />
  <s:textfield name="ndc" label="NDC" value="%{defaultNdc}"/>
  <s:checkbox key="freeOnly" />
  <s:submit value="検索" align="center" />
</s:form>

こんな風に表示されます。

フォーム例

form

フォームをsubmitすると、各コントロールに入力された値がGET/POSTのリクエストパラメータとしてサーバに送られます。それを受け取ったStrutsが<s:form>のaction属性に指定されたアクション名を発動するわけですが、その前に、ParametersInterceptorがActionクラスのsetterメソッドを使ってリクエストパラメータの値をActionインスタンスにセットしてくれます。このときのsetterメソッド名を決めるのがリクエストパラメータ名で、リクエストパラメータ名を決めるのがコントロール(textfieldやcheckbos)のname属性かkey属性です。

<form>の子要素に共通の属性

name、label、value、key属性は、多くのコントロールに共通する属性です。

  • name属性には、リクエストパラメータ名を指定
  • label属性には、そのコントロールのキャプションを指定
  • value属性には、コントロールの初期値を指定
  • key属性はname/label/valueを兼ねる
  • key属性の値は、リクエストパラメータ名として使われると共に、それをキー名としてメッセージリソースからコントロールのキャプションがlookupされる
  • また、key属性の値に基づいて、getterによりコントロールの初期値が取得される

textfield

上記の例の最初のtextfieldでは、nameとlabelを使っており、2番目のtextfieldではkeyを使いました。3番目ではname/label/valueを使っています。valueには"%{defaultNdc}"と指定しており、ActionクラスのgetDefaultNdc()が"007"を返すようになっています。valueには、"%{new java.util.Date()}"のような文を書くことも可能です。

select、radio、checkboxlist

<s:select>と<s:radio>と<s:checkboxlist>の構文はほとんど同じです。まずlist属性には、コレクション名を指定します。上記の例のselectでは、list="langStringList"としたので、setLangStringList()が返すStringのコレクションを使ってドロップダウンリストがレンダリングされます。setLangStringList()の実装は以下の通りです。

public String[] getLangStringList() {
    return new String[]{"何でも", "日本語", "英語"};
}

list属性にはString以外のコレクションを指定しても構いません。上記例のradioに指定した"langList"はLangクラスのコレクションです。listKey属性とlistValue属性にも、"key"と"value"を指定してあります。Actionクラスの実装は、こんな感じです。

public class SearchBooksAction extends ActionSupport {

    private String langKey;

    public ArrayList<Lang> getLangList() {
        ArrayList<Lang> list = new ArrayList<Lang>();
        list.add(new Lang("JP", "日本語"));
        list.add(new Lang("US", "英語"));
        return list;
    }

    public void setLanguage(String key) {
       langKey = key;
    }

    public class Lang {
        private String key;
        private String value;
        public Lang(String k, String v) {
            key = k;
            value = v;
        }
        public String getKey() {
            return key;
        }
        public String getValue() {
            return value;
        }
    }
}

例えば、ラジオボタンの「日本語」を選んでsubmitすると、setLanguage("JP")が呼ばれます。

またコレクションは、Actionクラスから入手しなくても構いません。上記のcheckboxlistの例では、list="%{#{'JP':'Japan', 'US':'United States'}}"としており、OGNLの#{}表記を使ったハッシュリテラルを指定しています。例えば、JapanとUnited Statesを両方チェックしてsubmitすると、setLanguages()の引数にはString配列{"JP", "US"}が指定されます(つまり、String配列を引数に取る、languages用のgetterが必要ということです)。

checkbox

checkboxには、name/label/valueかkey属性を指定します。

submit

  • value属性にキャプション文字列
  • align属性に配置("center"とか"right"とか)

Validation

前述の通り、フォームへの入力値はParametersInterceptorがActionインスタンスへセットしてくれます。Strutsには、セットされた値の正当性をチェックする仕組みが2つ用意されています。

  • Actionクラスのvalidate()メソッド
  • Actionクラスのソースファイルと同じ場所に置かれたxml

どちらも、リクエストパラメータに対してではなく、あくまでもActionインスタンスから取得した値に対するValidationである。

validate()メソッドによるValidation

  • DefaultWorkflowInterceptorが管理
  • アクションに対応するメソッド(executeとか)を呼ぶ前にvalidate()を呼んでくれる
  • その中で、エラーを見つけたらaddFieldError()でエラーを登録
  • 1つ以上のエラーが登録された場合は、そのアクションに対応するメソッド(executeとか)は呼ばれず、結果は自動的に"input"となる
@Override
public void validate() {
    if ( title == null || title.length() <= 0 ) {
        addFieldError("title", "本のタイトルを入力して。");
    }
    if ( author == null || author.length() <= 0 ) {
        addFieldError("author", "本の著者を入力して。");
    }
}

メソッドによるValidation

xmlによるValidation

  • AnnotationValidationInterceptorが管理
  • Actionクラスのソースファイルと同じ場所にXxxAction-validation.xmlを置く
  • <validators>の中に<validator>を列挙
  • <validator>に検証ルールを記述
  • type属性にルール種別を指定
  • ルール種別には、requiredstring, email, regex, fieldexpression, required, intなどがある
  • short-circuit属性がtrueの場合、その検証ルールでNGなら以降の検証ルールは無視される
  • <validator>の中に、<param name="fieldname">と<message>でフィールド名とエラーメッセージを指定
  • <message>を<message key="xxx" />とすれば、メッセージリソースからlookupする
  • ルール種別がregexの場合、<param name="expression"><![CDATA[正規表現]]></param>で正規表現を指定
  • ルール種別がfieldexpressionの場合、<param name="expression">book.year > 1970</param>のように条件式を指定
  • ルール種別がintの場合、<param name="min">0</param>や<param name="max">100</param>で範囲を指定
<!DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0.2//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
  <validator type="requiredstring">
    <param name="fieldname">title</param>
    <message>本のタイトルを入力してちょ。</message>
  </validator>
  <validator type="requiredstring">
    <param name="fieldname">author</param>
    <message key="requireauthor" />
  </validator>
  <validator type="regex">
    <param name="expression"><![CDATA[[0-9]{3}]]></param>
    <param name="fieldname">ndc</param>
    <message>3桁の数字で!!</message>
  </validator>
</validators>

xmlによるValidation

Theme

  • Strutsのthemeには、simple, xhtml, css_xhtml, ajaxなどがある
  • <s:form>のtheme属性や、プロパティstruts.ui.themeで指定
  • デフォルトはxhtml
  • xhtmlなら、2列の<table>でフォームを整形する
  • 1列目(ラベル列)にはCSSクラス名tdLabelが指定される

themeの正体は、CSSとJavaScriptとFreeMarker形式のテンプレートファイルです。themeを自作するには、既存のthemeをベースにして改造するのが良いでしょう。例えばxhtmlをベースにしてmy_themeを作成するなら、struts2-coreのjarファイルから/template/xhtml/*.*を取り出して、WebContent/template/my_theme/の下にコピーします。あとは改造するだけですが、このとき注意することが2つあります。

  • テンプレートの中で別のテンプレートをincludeしている場合、そのパスをmy_themeに変更する
  • CSSのパスは/struts/xhtml/styles.cssになっているので、/template/my_theme/styles.cssに変更する

ワイルドカード・メソッド・選択(Wildcard Method Selection)

  • struts.xmlの<action>の記述を簡略化する仕組み
  • name属性をワイルドカードで指定
  • method属性で、マッチ文字列を参照
<action name="*Book" class="foo.bar.BookAction" method="{1}">
  <result name="input">input-book.jsp</result>
  <result name="success">view-book.jsp</result>
  <result name="list">list-books.jsp</result>
</action>

上記の例では、アクションxxxBookをメソッドxxx()に対応付けています。createBook、editBook、deleteBookといったアクションを個々に書く代わりに1つの<action>で済ませることができます。

また、ちょっと毛色が違いますが、Dynamic Method Invocationという仕組みもあります。これはJSP内でアクションを発動するときに、メソッド名まで指定しまう機能です。!記号を使って表記します。しかし、この機能はセキュリティ面に問題があるらしいので使わない方が良さそうです。プロパティstruts.enable.DynamicMethodInvocationをfalseにすれば無効化できます。

Last modified:2011/06/20 12:17:52
Keyword(s):
References:[言語Tips] [Struts2のチュートリアル] [Struts2のチュートリアル(前半)]
This page is frozen.