とりあえず雑記帳(跡地)
Struts 2
最終更新:
fujiyan
-
view
とりあえず、Struts 2.3.8現在の内容です。
環境構築
パッケージングの種類によって、いくつかのファイルがあります。
"Full Distribution"だと、とんでもない数のファイルが格納されているので、解凍が面倒くさいです。
手っ取り早くjarファイルだけ欲しい場合は、"Essential Dependencies Only"(ファイル名がstruts-x.x.x-lib.zip)をダウンロードしましょう。
"Full Distribution"だと、とんでもない数のファイルが格納されているので、解凍が面倒くさいです。
手っ取り早くjarファイルだけ欲しい場合は、"Essential Dependencies Only"(ファイル名がstruts-x.x.x-lib.zip)をダウンロードしましょう。
まずは最小構成を目指す
最初のサンプルとして
- 極力少ないjarファイル
- 極力少ない設定ファイル
で、アプリを作ってみます。
jarファイル
必要なのは
- asm-x.x.jar
- asm-commons-x.x.jar
- commons-fileupload-x.x.x.jar
- commons-io-x.x.x.jar
- commons-langx-x.x.jar
- freemarker-x.x.x.jar
- javassist-x.x.x.GA.jar
- ognl-x.x.x.jar
- struts2-convention-plugin-x.x.x.jar
- struts2-core-x.x.x.jar
- xwork-core-x.x.x.jar
の11個。これらをWEB-INF/libに配置します。
Actionクラス
Convention PluginがActionクラスとして認識するクラスは、幾つかの条件があります。
例として、下記のActionクラスを作成してみました。
package jp.fujiyan.strutstest.actions.sub;
import com.opensymphony.xwork2.ActionSupport;
@SuppressWarnings("serial")
public class TestAction extends ActionSupport {
private String message;
public String getMessage() {
return message;
}
public String execute() throws Exception {
message = "てすと";
return SUCCESS;
}
}
パッケージ
まずパッケージですが、下記のサブパッケージ名以下に格納されている必要があります。
- struts
- struts2
- action
- actions
上記の例の場合、jp.fujiyan.strutstest.actions.subなので、用件を満たしています。
なお、上記サブパッケージ以下のパッケージ階層は、そのままActionのnamespaceに対応します。
なお、上記サブパッケージ以下のパッケージ階層は、そのままActionのnamespaceに対応します。
パッケージ | Actionのnamespace |
jp.fujiyan.strutstest.actions.sub | /sub |
jp.fujiyan.strutstest.struts2.foo.bar | /foo/bar |
jp.fujiyan.strutstest.actions | (Default namespace) |
コンテキストルートにマッピングさせるつもりで、actions等の直下にActionクラスを配置しても、Root namespaceではなく、Default namespaceに割り当てられます(この仕様はどうなんでしょう…?)。
※Root namespceとDefault namespaceの違いは後述「Root namespaceとDefault namespace」を参照
※Root namespceとDefault namespaceの違いは後述「Root namespaceとDefault namespace」を参照
コンテキストルートにマッピングさせたい場合は、@Namespaceアノテーションで明示的に"/" namespaceを指定することで、Root namespaceにします。
クラス
次にクラスですが、上記サブパッケージに格納されているクラスのうち、下記のいずれかに該当するクラスがActionクラスとして認識されます。
- クラス名の末尾が"Action"
- com.opensymphony.xwork2.Actionをimplementsしている
上記の場合、TestActionなので、Actionクラスとして認識されます。
…が、実は、com.opensymphony.xwork2.Actionをimplementsしている、com.opensymphony.xwork2.ActionSupportをextendsしているので、末尾"Action"でなくてもActionクラスとして認識されます。
今回は、定数SUCCESSが使いたかったのでActionSupportをextendsしましたが、何かと便利なので通常はどのActionクラスでもActionSupportをextendsしておきます。
今回は、定数SUCCESSが使いたかったのでActionSupportをextendsしましたが、何かと便利なので通常はどのActionクラスでもActionSupportをextendsしておきます。
Actionクラス名とURLの対応は下記の通りです。
- 末尾"Action"は、URLから省かれる
- 先頭の大文字は、URLでは小文字になる。
- キャメルケースは、URLではハイフン繋ぎで全て小文字になる。
クラスのFQCN | namespace | コンテキストルートからの、ActionへのURLパス |
jp.fujiyan.strutstest.actions.sub.TestAction | /sub | /sub/test |
jp.fujiyan.strutstest.actions.foo.Sample(com.opensymphony.xwork2.Actionをimplements) | /foo | /foo/sample |
jp.fujiyan.strutstest.struts2.foo.bar.ExampleBazAction | /foo/bar | /foo/bar/example-baz |
メソッドと戻り値
ActionクラスにString execute()メソッドを定義し、その中に処理を記述します。
execute()の戻り値によって、Action実行後に処理を行うJSPファイルを切り替えることができます。
詳細は後述。
execute()の戻り値によって、Action実行後に処理を行うJSPファイルを切り替えることができます。
詳細は後述。
JSPファイル
JSPファイルは、/WEB-INF/contentに格納します。
名前は、Actionクラス名に対応するURL名に、Actionクラスの戻り値の文字列をハイフンで繋いだものにします。
なお、"success"を戻り値とした場合は、JSPファイル名には戻り値の文字列"success"は省略可能です。
namespace階層と、/WEB-INF/content以下のパス階層を合わせておきます。
名前は、Actionクラス名に対応するURL名に、Actionクラスの戻り値の文字列をハイフンで繋いだものにします。
なお、"success"を戻り値とした場合は、JSPファイル名には戻り値の文字列"success"は省略可能です。
namespace階層と、/WEB-INF/content以下のパス階層を合わせておきます。
クラスのFQCN | namespace | コンテキストルートからの、ActionへのURLパス | 戻り値 | 処理されるJSPファイル |
jp.fujiyan.strutstest.actions.sub.TestAction | /sub | /sub/test | "success" | /WEB-INF/content/sub/test.jsp(test-success.jspでも可) |
jp.fujiyan.strutstest.actions.sub.TestAction | /sub | /sub/test | "error" | /WEB-INF/content/sub/test-error.jsp |
jp.fujiyan.strutstest.struts2.foo.bar.ExampleBazAction | /foo/bar | /foo/bar/example-baz | "abc" | /WEB-INF/content/foo/bar/example-baz-abc.jsp |
今回は、successのみなので、/WEB-INF/content/sub/test.jspを用意しました。
<%@page contentType="text/html; charset=utf-8"%>
<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
メッセージ:${message}
</body>
</html>
web.xmlファイル
Struts 2のFilterだけの、最小定義です。
念のため、Dynamic Method Invocationを無効にしています。
念のため、Dynamic Method Invocationを無効にしています。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="StrutsTest" version="3.0">
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
<init-param>
<param-name>struts.enable.DynamicMethodInvocation</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
struts.xmlファイル
…は、不要です。
まとめ
Root namespaceとDefault namespace
Root namespaceとDefault namespaceは、似ているようで全然違うので気をつけましょう。
Root namespaceは、その名の通り、ルートコンテキスト"/"に対応するnamespaceです。
一方、Default namespaceは、リクエストに対応するActionが、指定されたURLに対応するnamespaceに存在しない場合の、代替の検索場所となります。
一方、Default namespaceは、リクエストに対応するActionが、指定されたURLに対応するnamespaceに存在しない場合の、代替の検索場所となります。
例えば、/sub/test.actionというリクエストがあった場合、"/sub" namespaceにTestActionクラスが存在しなかった場合、Default namespaceのTestActionクラスが呼び出されます。
/sub/aa/test.actionでも、/sub/aa/bbb/test.actionでも、該当namespaceにActionクラスが無ければ、Default namespaceのTestActionクラスが呼び出されます。
言い換えれば、Default namespaceとは「グローバルなnamespace」とも言えます。
/sub/aa/test.actionでも、/sub/aa/bbb/test.actionでも、該当namespaceにActionクラスが無ければ、Default namespaceのTestActionクラスが呼び出されます。
言い換えれば、Default namespaceとは「グローバルなnamespace」とも言えます。
そのため、Default namespaceのActionは、思わぬURLで呼び出される場合があるので、よく計画した上で利用しましょう。
考えるのが面倒くさい場合は、利用を避けるのがベターでしょう。
考えるのが面倒くさい場合は、利用を避けるのがベターでしょう。
Dynamic Method Invocationの罠
Struts 2で、Actionクラスの数を減らすためにDynamic Method Invocation(DMI)を使いたくなりますが、よく注意しないと大変なことになるお話。
古い記事ですが、念のため。
古い記事ですが、念のため。
Dynamic Method Invocationを有効にすると、Actionクラスの引数無しpublicメソッドが
"!" + (メソッド名).action
で呼び出すことが出来ます。
そのため、本来公開させるつもりではなかったメソッドまで、外部から呼び出すことが出来ます。
そのため、本来公開させるつもりではなかったメソッドまで、外部から呼び出すことが出来ます。
「そんなの、どーせエラーになるだけでしょ?」と思いますが、そのとき出力されるエラーメッセージが曲者なのです。
想定外のメソッドの呼び出しのため、当然、戻り値に対応するJSPも準備されていないので、404エラーのメッセージの詳細に
想定外のメソッドの呼び出しのため、当然、戻り値に対応するJSPも準備されていないので、404エラーのメッセージの詳細に
No result defined for action (アクションクラス名) and result (メソッドの戻り値)
と出力されます。「アクションクラスからの戻り値に対応する結果は定義されていません」ということです。
これが、呼び出したメソッドが、例えばgetPassword()というパスワードを返すメソッドだった場合、その戻り値であるパスワードがモロに(メソッドの戻り値)の部分に乗っかってしまうのです。
これを防ぐために、設定ファイルにて、DMIで呼び出し可能なメソッドを制限することができます。
ただ、そのために設定ファイルの記述をするくらいなら、そもそもDMI要らないよね、という話にもなってきたり。
ただ、そのために設定ファイルの記述をするくらいなら、そもそもDMI要らないよね、という話にもなってきたり。
もちろん、DMIは便利な機能でもあるので、全く使うな、というわけでもないですが、気を使うのが面倒なら無効にしておきましょう。
Config Browser Plugin
Convention Pluginを使えば、struts.xmlを記述しなくていいので手間は省けますが、逆にどのようにActionとURLがマッピングされているかが一目で把握できません。
そこで、このConfig Browser Pluginが用意されています。
そこで、このConfig Browser Pluginが用意されています。
設定は簡単で、
- struts2-config-browser-plugin-x.x.x
を/WEB-INF/libに格納するだけです。あとはサーバ起動後に
http://localhost:8080/(コンテキスト名)/config-browser/index.action
にアクセスするだけです。
特に、想定外のマッピングがされてしまっている場合の状況把握に役立ちます。
…が、全てのマッピングを晒してしまうので、通常ユーザーに公開したくない、管理画面用のAction等も知られてしまい、クラッキングの手掛かりとなってしまう可能性があります。
なので、あくまでデバッグ利用にとどめ、プロダクション環境にはConfig Browser Pluginは省いたほうがイイかもしれないですね。
…が、全てのマッピングを晒してしまうので、通常ユーザーに公開したくない、管理画面用のAction等も知られてしまい、クラッキングの手掛かりとなってしまう可能性があります。
なので、あくまでデバッグ利用にとどめ、プロダクション環境にはConfig Browser Pluginは省いたほうがイイかもしれないですね。
Actionクラスのメソッド毎にActionを割り当てたい
例えば、あるデータの
- 登録
- 更新
- 削除
- 参照
といったアクションは、関連性が高いので、1つのクラスに納めたほうが見通しがイイですね。処理も似てますし。
ということで、1つのActionクラスのメソッド毎にActionを割り当てたくなりますが、DMIは無効が基本なので、地道にマッピングを記述していきます。
折角Convention Pluginを使うので、struts.xmlを記述せずに、アノテーションで乗り切って見ます。
ということで、1つのActionクラスのメソッド毎にActionを割り当てたくなりますが、DMIは無効が基本なので、地道にマッピングを記述していきます。
折角Convention Pluginを使うので、struts.xmlを記述せずに、アノテーションで乗り切って見ます。
jp.fujiyan.strutstest.actions.sub.TestAction
package jp.fujiyan.strutstest.actions.sub;
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.Result;
import com.opensymphony.xwork2.ActionSupport;
@SuppressWarnings("serial")
@Result(name="success", location="test.jsp")
public class TestAction extends ActionSupport {
private String testMessage;
private String inputValue;
public void setInputValue(String inputValue) {
this.inputValue = inputValue;
}
public String getInputValue() {
return inputValue;
}
public String getTestMessage() {
return testMessage;
}
@Action(value="show-test")
public String show() throws Exception {
testMessage = "開始";
return SUCCESS;
}
@Action(value="submit-test")
public String submit() throws Exception {
testMessage = "結果「" + inputValue + "」";
return SUCCESS;
}
}
/WEB-INF/content/sub/test.jsp
<%@page contentType="text/html; charset=utf-8"%>
<%@taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
<s:form action="submit-test">
<s:textfield name="inputValue" label="値"/>
<s:submit value="実行"/>
</s:form>
メッセージ:${testMessage}
</body>
</html>
/sub/show-test.actionで初期表示を行い、/sub/submit-test.actionでフォーム入力の送信結果を表示します。
@Actionアノテーションを使って、show()にshow-test.actionを、submit()にsubmit-test.actionを割り当てています。
Actionを記述する際に、先頭に"/"が無いので、NamespaceはActionクラスが属するNamespaceになります。
この場合は、"/sub" Namespaceになります。
Actionを記述する際に、先頭に"/"が無いので、NamespaceはActionクラスが属するNamespaceになります。
この場合は、"/sub" Namespaceになります。
@Actionアノテーションだけだと、それぞれのActionの実行後に呼び出されるJSPは
Action | JSP |
/sub/show-test.action | /WEB-INF/content/sub/show-test-success.jsp |
/sub/submit-test.action | /WEB-INF/content/sub/submit-test-success.jsp |
となり、Action毎にJSPが必要になってしまいます。
そのため、@Resultアノテーションで、TestActionクラス全てのメソッドについて、"success"の場合は/WEB-INF/content/sub/test.jspを実行するように指定しています。
そのため、@Resultアノテーションで、TestActionクラス全てのメソッドについて、"success"の場合は/WEB-INF/content/sub/test.jspを実行するように指定しています。