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)

本文

Struts2のメカニズム

アーキテクチャ

  1. クライアントからのリクエストがサーバ(apache)に届く
  2. apacheは、httpd.confの設定に基づき、特定のURLへのリクエストをajp経由でtomcatへパスする
  3. tomcatは、web.xmlの設定に基づき、特定のURLへのリクエストをstruts2へパスする
  4. struts2内では、まずdispatherがリクエストを受け取る
  5. dispatcherは、struts.xmlの設定に基づき、URLを処理すべきActionクラスとメソッドを決める
  6. dispatcherは、Actionクラスのインスタンスを作る
  7. dispatcherは、struts.xmlの設定に基づき、適切なInterceptor群(Interceptorスタック)をコールバックする
  8. dispatcherは、Actionインスタンスのメソッドをコールバックする
  9. Actionインスタンスのメソッドが結果を返す
  10. dispatcherは、Interceptorスタックを逆順にコールバックする
  11. dispatcherは、Actionインスタンスが返した結果に基づき、レスポンス用のJSPを決める
  12. tomcatがJSPをレンダリングしてapacheに返す
  13. apacheがレスポンスをクライアントへ返す

Struts2アプリのディレクトリ構成例

Eclipseの「動的Webプロジェクト(Dynamic Web Project)」をベースにした例を示します。

proj1/
  src/
    struts.xml
    log4j.xml
    *.java
    *.properties
  WebContent/
    META-INF/
    WEB-INF/
      lib/
        *.jar
      jsp/
        *.jsp
      web.xml
  build.xml

lib/*.jarの一例を挙げます。

  • antlr.jar
  • cglib.jar
  • commons-collections.jar
  • commons-fileupload-1.2.1.jar
  • commons-io-1.3.2.jar
  • commons-logging-1.0.4.jar
  • commons-logging-api-1.1.jar
  • dom4j.jar
  • ehcache.jar
  • freemarker-2.3.16.jar
  • hibernate3.jar
  • javassist-3.7.ga.jar
  • jta.jar
  • log4j-1.2.14.jar
  • ognl-3.0.jar
  • postgresql-8.1-415.jdbc2.jar
  • struts2-config-browser-plugin-2.1.8.1.jar
  • struts2-convention-plugin-2.2.1.jar
  • struts2-core-2.2.1.jar
  • xwork-core-2.2.1.jar

antの使用は必須ではありませんが、ビルドファイルの例を挙げておきます。

  • build.xml
<?xml version="1.0"?>

<project name="GPLib" default="archive" basedir=".">
  <description>
    GP Library
  </description>

  <property file="build.properties"/>

  <target name="clean">
    <delete dir="${build.home}"/>
    <delete file="./${app.name}.war"/>
  </target>

  <target name="init">
    <mkdir dir="${build.home}" />
  </target>

  <target name="compile" depends="init">
    <mkdir dir="${build.home}/WEB-INF/classes" />
    <javac
      srcdir="${source.home}"
      destdir="${build.home}/WEB-INF/classes"
      debug="${compile.debug}"
      deprecation="${compile.deprecation}"
      optimize="${compile.optimize}"
      source="1.6"
      target="1.6"
      includeantruntime="no"
      >
      <classpath>
        <path>
          <fileset dir="${lib.home}" />
        </path>
      </classpath>
    </javac>
  </target>

  <target name="build" depends="compile">
    <copy todir="${build.home}">
      <fileset dir="${webapp.home}" excludes="**/*.class" />
    </copy>
    <copy todir="${build.home}/WEB-INF/classes">
      <fileset dir="${source.home}" excludes="**/*.java,message.properties" />
    </copy>
    <native2ascii src="${source.home}" dest="${build.home}/WEB-INF/classes">
      <exclude name="**/*.java, **/*.xml" />
      <include name="message.properties" />
    </native2ascii>
  </target>

  <target name="archive" depends="build">
    <jar
      jarfile="./${app.name}.war"
      basedir="${build.home}" />
    <mkdir dir="${dist.home}" />
    <copy file="./${app.name}.war" todir="${dist.home}" />
  </target>
</project>
  • build.properties
app.name=gplib
app.version=0.1

source.home=./src
lib.home=./WebContent/WEB-INF/lib
webapp.home=./WebContent
build.home=./build
dist.home=../apps

compile.debug=true
compile.deprecation=false
compile.optimize=true

Hello, world的アプリ

Hello, world的アプリを使って、主人公たちの役割を見ていきましょう。

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
  xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  <display-name>GP Library</display-name>
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

  <filter>
    <filter-name>struts2</filter-name>
      <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
    </filter>

  <filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

ここでは、struts2という名前のフィルタを作り、"/*"とマッチするURL(つまり全てのURL)をStrutsPrepareAndExecuteFilterに処理させるよう、Tomcatへ伝えています。

また、デフォルトのindexページをindex.jspとしています。

struts.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
  <constant name="struts.devMode" value="true" />

  <package name="struts2" extends="struts-default">
    <action name="top" class="jp.dip.gpsoft.gplib.pub.WelcomeAction" method="execute">
      <result name="success">/welcome.jsp</result>
    </action>
  </package>
</struts>
  • <package>はアクションをグループ化するための概念
  • javaのpackageとは別物
  • パッケージ間には、継承関係を持たせることが可能
  • 上記例では、struts-defaultというパッケージを継承している
  • struts-defaultは、struts2-core.jarのstruts-default.xmlで定義されたパッケージ
  • <action>には、アクション名と、Actionクラス&メソッドと、メソッドの結果に対するJSPとの対応を書く
  • <constant>にはプロパティを設定(struts.propertiesの代わり)

<result>のtype属性を使うと、別アクションへリダイレクトできます。下記の2つは同じ意味ですが、アクションの拡張子に依存しない前者の方がベターでしょう。

<result name="success" type="redirect-action">index</result>
<result name="success" type="redirect">index.action</result>

Actionクラスの基本

  • 普通は、com.opensymphony.xwork2.ActionSupportの派生クラスとする
  • struts.xmlで、アクションに対応付けられたメソッドを実装する
  • input()、execute()、error()辺りが定番
  • struts.xmlで指定したメソッド名が例えばinputの場合、Actionクラスに実装するメソッド名はinput()でもdoInput()でもいい
  • ActionSupportにはexecute()とinput()が実装済み
  • これらのアクションメソッドは、結果を文字列で返す
  • 定番の結果文字列("success"とか"error"とか)を返す場合は、ActionSupportが定義した定数を使う方が良さそう
  • アクション結果を表示するJSPにデータ(MVCのM)を渡せるように、getterを実装する
public class WelcomeAction extends ActionSupport {
    private static final long serialVersionUID = 1L;

    public String execute() throws Exception {
        return SUCCESS;
    }
    public String getMessage() {
        return "Huh! Look at this!";
    }
}

JSPの基本

  • ValueStackとOGNL(Object Graph Navigation Language)を使って、表示するデータ(MVCのM)を取得する
  • ValueStackはオブジェクトのスタックで、OGNLはオブジェクトやその属性を参照する記法
  • 例えば、スタック上のfooというオブジェクトのbarという属性を参照するなら%{#foo.bar}と書く
  • fooがスタックのトップにある場合は、#foo.は省略できる
  • アクションの結果としてJSPをレンダリングする場合、ValueStackのトップにはActionインスタンスがある
  • 例えば"%{book.title}"なら、Actionインスタンスに対してgetBook().getTitle()が呼ばれる
  • getTitle()が文字列以外を返すなら、レンダリング時にtoString()で文字列化される
  • 簡略化して"book.title"と書いてもいい(時と場合によるかも)
  • OGNLではなくリテラルとして"book.title"を指定したい場合は、"%{'book.title'}"と書けば良い
  • "#{'JP':'Japan', 'US':'United States'}"のようにハッシュリテラルも書ける
  • ValueStack経由で、#session、#application、#request、#parametersなどを参照可能
  • トップのオブジェクトそのものを参照するには、"top"を使う
<%@ page language="java" contentType="text/html; utf-8"
    pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC
  "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>GP図書館</title>
</head>
<body>
  <h1>Welcome to GP Library!</h1>
  <s:property value="%{message}" />
</body>
</html>

スクリーンショット

Welcome

Interceptor

Interceptorは、アクションメソッドが呼ばれる前と呼ばれた後に実行されるフックです。複数のInterceptorがInterceptorスタックを形成します。アクションメソッドが呼ばれる前にはスタックに登録されたInterceptorが順番に実行され、アクションメソッドの後で、今度はスタックの逆順にInterceptorが実行されます。

Strutsのstruts-defaultパッケージには、いくつかのInterceptorとInterceptorスタックが定義されており、その内容はstruts2-core.jarのstruts-default.xmlを見れば分かります。一部を抜粋します。

<interceptor name="alias" class="com.opensymphony.xwork2.interceptor.AliasInterceptor"/>
<interceptor name="chain" class="com.opensymphony.xwork2.interceptor.ChainingInterceptor"/>
<interceptor name="conversionError" class="org.apache.struts2.interceptor.StrutsConversionErrorInterceptor"/>
<interceptor name="debugging" class="org.apache.struts2.interceptor.debugging.DebuggingInterceptor" />
<interceptor name="exception" class="com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor"/>
<interceptor name="fileUpload" class="org.apache.struts2.interceptor.FileUploadInterceptor"/>
<interceptor name="i18n" class="com.opensymphony.xwork2.interceptor.I18nInterceptor"/>
<interceptor name="logger" class="com.opensymphony.xwork2.interceptor.LoggingInterceptor"/>
<interceptor name="modelDriven" class="com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor"/>
<interceptor name="scopedModelDriven" class="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor"/>
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor"/>
<interceptor name="actionMappingParams" class="org.apache.struts2.interceptor.ActionMappingParametersInteceptor"/>
<interceptor name="prepare" class="com.opensymphony.xwork2.interceptor.PrepareInterceptor"/>
<interceptor name="staticParams" class="com.opensymphony.xwork2.interceptor.StaticParametersInterceptor"/>
<interceptor name="servletConfig" class="org.apache.struts2.interceptor.ServletConfigInterceptor"/>
<interceptor name="timer" class="com.opensymphony.xwork2.interceptor.TimerInterceptor"/>
<interceptor name="validation" class="org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor"/>
<interceptor name="workflow" class="com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor"/>
<interceptor name="checkbox" class="org.apache.struts2.interceptor.CheckboxInterceptor" />
<interceptor name="multiselect" class="org.apache.struts2.interceptor.MultiselectInterceptor" />

<interceptor-stack name="basicStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="checkbox"/>
    <interceptor-ref name="multiselect"/>
    <interceptor-ref name="actionMappingParams"/>
    <interceptor-ref name="params">
        <param name="excludeParams">dojo\..*,^struts\..*</param>
    </interceptor-ref>
    <interceptor-ref name="conversionError"/>
</interceptor-stack>

<interceptor-stack name="defaultStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="alias"/>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="i18n"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="chain"/>
    <interceptor-ref name="debugging"/>
    <interceptor-ref name="scopedModelDriven"/>
    <interceptor-ref name="modelDriven"/>
    <interceptor-ref name="fileUpload"/>
    <interceptor-ref name="checkbox"/>
    <interceptor-ref name="multiselect"/>
    <interceptor-ref name="staticParams"/>
    <interceptor-ref name="actionMappingParams"/>
    <interceptor-ref name="params">
      <param name="excludeParams">dojo\..*,^struts\..*</param>
    </interceptor-ref>
    <interceptor-ref name="conversionError"/>
    <interceptor-ref name="validation">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
    <interceptor-ref name="workflow">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
</interceptor-stack>

デフォルトではdefaultStackが適用されますが、以下の方法で任意のInterceptorスタックを適用することもできます。

  • struts.xmlの<action>の子要素として<interceptor-ref>を列挙
  • struts.xmlの<package>の子要素として<default-interceptor-ref>を定義
<package name="struts2" extends="struts-default">
  <action name="top" class="jp.dip.gpsoft.gplib.pub.WelcomeAction" method="execute">
    <interceptor-ref name="timer" />
    <interceptor-ref name="defaultStack" />
    <result name="success">/welcome.jsp</result>
  </action>

  <default-interceptor-ref name="basicStack" />
</package>

struts.xmlの中で、独自のInterceptorスタックを定義することもできます。試しに、defaultStackをベースにして、TimerInterceptorとLoggingInterceptorを付け加えたInterceptorスタックを定義してみましょう。

  • struts.xml
<package name="struts2" extends="struts-default">
  ....

  <interceptors>
    <interceptor-stack name="myStack">
      <interceptor-ref name="timer" />
      <interceptor-ref name="logger" />
      <interceptor-ref name="defaultStack">
       <param name="exception.logEnabled">true</param>
       <param name="exception.logLevel">ERROR</param>
      </interceptor-ref>
   </interceptor-stack>
  </interceptors>

  <default-interceptor-ref name="myStack" />
</package>

上記例では、defaultStack内のログを有効化しログレベルも変えています。

PrepareInterceptor

  • com.opensymphony.xwork2.Preparableをimplementsすると、アクションメソッドの前にprepare()をコールバックしてもらえる
  • ここで、アクションメソッドの準備を行う

DefaultWorkflowInterceptor

  • validate()をオーバーライドすると、アクションメソッドの前にvalidate()をコールバックしてもらえる
  • ここで、Actionインスタンスの値を検証する

ParametersInterceptor

  • リクエストパラメータの値をActionインスタンスへセットする
  • Actionクラスにgetterとsetterが必要(インスタンスメソッドとして)

デバッグ

config-browser-plugin(struts2-config-browser-plugin.jar)

config-browser-pluginを使うと、Strutsの設定内容を確認することができます。適当なページに以下のリンクを書いてジャンプして下さい。

<a href="<s:url action='index' namespace='config-browser' />">コンフィグ内容確認</a>

DebuggingInterceptor

  • アクション発動時の各オブジェクトの内容を確認できる
  • DebuggingInterceptorを有効化する
  • dojo-plugin(struts2-dojo-plugin.jar)を組み込む
  • JSPに<s:head />を追加する
  • リクエストパラメータdebugに、xmlかbrowserを指定する

ログ

log4j用のログカテゴリが2つ用意されています。

  • com.opensymphony
  • org.apache.struts

メッセージリソース

  • 文言をJSPとは別のファイルで管理する
  • 言語ごとにリソースファイルを用意すれば多言語対応も楽
  • 特定のActionクラス専用なら、当該Actionクラスのソースと同じ場所にXxxAction.propertiesを置く
  • 特定のpackage専用なら、当該packageのディレクトリにpackage.propertiesという名前で置く
  • 全体に共通なら、適当な場所に適当な名前(拡張子はproperties)で置き、プロパティstruts.custom.i18n.resourcesで名前を指定する
  • リソースファイルの書式は、一般のプロパティファイルと同じ
  • JSP内で参照するときはtextタグのname属性にキー名を指定する
  • OGNLが使える場所なら、getText()にキー名を渡すことにより参照できる

例を挙げます。

  • src/struts.xml
<constant name="struts.custom.i18n.resources" value="message" />
  • src/message.properties
welcome=Welcome to GP Library!
  • welcome.jsp
<h1><s:text name="welcome" /></h1>
<h1><s:property value="%{getText('welcome')}" /></h1>

リソースファイル内で日本語を使う場合は、unicodeエスケープする必要があります。antならnative2asciiを使うなどして、\uXXXX形式に変換したうえでデプロイして下さい。

welcome=GP図書館へようこそ!

↓ unicodeエスケープ後

welcome=GP\u56f3\u66f8\u9928\u3078\u3088\u3046\u3053\u305d!

多言語対応

  • message.propertiesの代わりに、message_ja.propertiesやmessage_en.propertiesを用意する
  • システムのデフォルトロケールにマッチするものが無い場合は、message.propertiesが採用される
  • ロケールはプロパティstruts.localeで変更可能
  • struts.i18n.encodingというプロパティもあるが、いまいち用途が分からない(デフォルトはutf-8)
<constant name="struts.locale" value="ja_JP" />
<constant name="struts.i18n.encoding" value="utf-8" />

リクエストパラメータrequest_localeでもロケールは指定可能なので、一時的に変えたいときは便利です。URLの最後に"?request_locale=en_US"などを付けてみましょう。

後半へ

後半では、以下のトピックを扱います。

  • convention-plugin(struts2-convention-plugin.jar)
  • Exception
  • JSPのタグ
  • フォーム関連のタグ
  • Validation
  • Theme
  • ワイルドカード・メソッド・選択(Wildcard Method Selection)
Last modified:2011/06/20 12:14:48
Keyword(s):
References:[言語Tips] [Struts2のチュートリアル] [Struts2のチュートリアル(後半)]
This page is frozen.