Agusa Lab. > Webware Project > GrapeWeb
 

エラーメッセージから学ぶ Struts の基本

概要

Struts アプリケーションの雛形である StrutsBlank から、どのように自分の Web アプリケーションを構築していくかを学びます。 チュートリアルでは、実際に StrutsBlank から Step by step で "Hello World" 程度のアプリケーションを作成します。 Struts のエラーメッセージをひとつずつ理解していくことで、最終的に Struts アプリケーションの全体像を理解します。

はじめに (今日のお題の前準備)

本題に入る前に、いくつかの下準備をします。 具体的には 1). struts-blank.war の展開、2) 開発アプリケーションを Tomcat へ配備 の2つです。

struts-blank.war の展開

struts-blank.war は全ての Struts アプリケーションの雛形です。 つまり、そう言い切れるくらい、"何もしないアプリケーション"(= blank アプリケーション)です。 そこで、この struts-blank.war を解凍してアプリケーション開発をスタートさせます。

適当な作業用ディレクトリを作り、ディレクトリに struts-blank.war をコピーして、jar コマンドを使って解凍してください。

	% mkdir helloworld
	% cd helloworld
	% cp <STRUTS_HOME>/webapps/struts-blank.war ./
	% jar xvf struts-blank.war
			
:Note.
「struts-blank.war を <TOMCAT_HOME>/webapps/ 以下にコピーして、Tomcat を再起動してください。」 という文面をよく見ますが、この方法はお勧めしません。 詳しい説明は省略しますが、この方法だと Tomcat の設定次第で war が解凍されなかったり、 解凍されるファイルのオーナーが tomcat を起動するユーザになったり、何度もコピーをしなければならなかったり、 と、非常に手間が増えます。

開発アプリケーションを Tomcat へ配備

struts-blank.war を解凍した Web アプリケーションを Tomcat へ配備します。 配備の仕方はいろいろありますが、ここでは Web アプリケーション毎の設定ファイルを記述する方法を取ります。

まず、以下のような XML ファイルを用意してください。 ただし、"/your/application/directory/" は、先程 struts-blank.war を解凍したディレクトリへの絶対パスを指定してください。 (説明の都合上、ここではこのファイルを "apps-lec1-helloworld.xml" という名前にします。)

	<?xml version='1.0' encoding='utf-8'?>
	<Context
	  docBase="/your/application/directory/"
	  path="/apps/lec1/helloworld">
		<Logger
		  className="org.apache.catalina.logger.FileLogger"
		  prefix="helloworld-"
		  suffix=".log"
		  timestamp="true"
		  verbosity="4"
		  directory="logs/apps/lec1" />
	</Context>
			

"apps-lec1-helloworld.xml" を作成したら、 <TOMCAT_HOME>/conf/[enginename]/[hostname]/apps-lec1-helloworld.xml としてコピーしてください。 デフォルトの <TOMCAT_HOME>/conf/server.xml の設定であれば、enginename=Catalina, hostname=localhost です。
(以下、デフォルトを仮定)

	% cp apps-lec1-helloworld.xml <TOMCAT_HOME>/conf/Catalina/localhost/
			
:Note.
"apps-lec1-helloworld.xml" に関する詳しい説明は省略しますが、簡単に説明すると、上記の記述によって以下が設定されています。
・アプリケーションのファイルシステム上のディレクトリ: /your/application/directory/
・ブラウザからのアクセス: http://[host]/apps/lec1/helloworld/
 (例えば、http://localhost:8080/apps/lec1/helloworld)
・アプリケーションのログファイル: <TOMCAT_HOME>logs/apps/lec1 以下に生成
・ログファイルの接頭辞: "helloworld-"
・ログファイルの接尾辞: ".log"
・ログファイル名にタイムスタンプを入れる: true
・ログファイルの出力レベル: 4 (詳細)

用意ができたら、Tomcat を起動して http://[host]/apps/lec1/helloworld/ にアクセスしてください。 StrutsBlank アプリケーションが表示されれば、準備は完了です。

StrutsBlank の構成

StrutsBlank から独自の Web アプリケーションを開発する前に、StrutsBlank の構成について説明します。 StrutsBlank アプリケーションは以下のファイルから成っています。

index.jsp トップページ
pages jsp ファイルが格納されているディレクトリ※1
META-INF jar のマニフェストファイルを格納するディレクトリ※2
WEB-INF/classes Web アプリケーション内で動作する java クラスは、このディレクトリに格納されてなければいけません
WEB-INF/lib Web アプリケーションで利用するライブラリ(.jar ファイル)は、このディレクトリに格納されなければなりません
WEB-INF/src Web アプリケーション内で動作する java クラスのソースファイル※3
WEB-INF/struts-config.xml Struts フレームワークの設定ファイル
WEB-INF/web.xml Web アプリケーション設定ファイル
WEB-INF/*.tld タグライブラリファイル
WEB-INF/*.xml Commons ValidatorTiles Framework の設定ファイルなど
:Note.
※1: 必須ではありませんが、Web アプリケーションを構成するページ要素(.jsp や .css、.html など)を個別のディレクトリに 格納しておくのは主流のようです(管理がしやすいのでしょう)。
※2: Web アプリケーションアーカイブ (war) は jar (Java アーカイブ) の派生物なのでその名残であるものと思われます。
※3: ただし、このディレクトリは WEB アプリケーションディレクトリに含めないのが主流です。

ユーザが StrutsBlank にアクセスすると、画面には "Welcome" から始まるお馴染のページが表示されます。 この時、アプリケーションは以下のような動作をしています。
StrutsBlank アプリケーションの動作

 1. ユーザが Web ブラウザを利用して URL をリクエスト
2. Tomcat (Web アプリケーションコンテナ) は index.jsp (web.xml に記述されている <welcome-file> の要素)をブラウザに返す
3. index.jsp の内部では <logic:redirect forward="welcome"/> が記述されているため、フォワード要求 welcome が Struts に送られる
4. Struts は welcome を処理する。
4.1. フォワード名 "welcome" に該当するフォワードを struts-config.xml に記述されている設定から探す。
4.2. <forward name="welcome" path="/Welcome.do"/> が記述されているので、Struts は "/Welcome.do" というアクションを開始する。
5. "/Welcome.do" に相当するアクションは "/pages/Welcome.jsp" へのフォワードなので、"/pages/Welcome.jsp" をブラウザに表示する。

このように Struts アプリケーションは Tomcat と Struts の双方に深く関係しながら、アプリケーションとして動作しています。

Hello World の作成

さて、ダラダラと前書きを書きましたが、いよいよ、今日の本題です。 世界一有名なプログラム "Hello World" を作成します。 ただし、Web アプリケーションは interaction system なので、単なる "Hello World" ではなく、 ユーザが自分の名前(xxxx)を入力し、その名前に対して "Hello xxxx!" と応えるアプリケーションを作成します。

要件の整理

"Hello World" プログラムを作成するにあたり、アプリケーションに必要な要件をまとめます。 これから作成する "Hello World" アプリケーションは機能を以下のように設定します。

  • ユーザが名前を入力する。
  • ユーザが入力した名前(e.g. "Taro")に対して "Hello Taro!" と表示されたページを表示する。
    この時、もしユーザが入力した名前が空であったら、エラーページを表示する。

Hello World プログラム (以後、説明の都合上、この図を"図1"として参照します。)

Step 1. フォームを持つページの作成

それでは、実際に "Hello World" プログラムを作成していきます。 まず最初に作成するのはユーザに名前を入力してもらうページ (name-input.jsp) です。 StrutsBlank を解凍したディレクトリ(以降、<HELLO_APP>)/pages/Welcome.jsp の名前を name-input.jsp に変更し、 以下のファイルの中身を以下のように書き換えてください。

	<%@ taglib uri="/tags/struts-html" prefix="html" %>

	<html>
	<head>
	<title>Hello World (Input User's Name)</title>
	</head>
	<body bgcolor="white">
	<h3>Input your name</h3>
	
	<form>
	name: <input type="text" name="username" />
	<input type="submit" />
	</form>
	
	</body>
	</html>
		

上記の修正が終わった後、"Hello World" アプリケーション(以降、helloworld)にアクセスしてください。

HTTP ステータス 404: The requested resource (/pages/Welcome.jsp) is not available.

helloworld にアクセスすると、404 エラーに遭遇するハズです。 これは、struts-config.xml においてアクション "Welcome" は /pages/Welcome.jsp にフォワードすると定義しているのに、 helloworld に /pages/Welcome.jsp というファイルが存在しないために発生するエラーです。

そこで、strus-config.xml を修正し、"Welcome" アクションのフォワード先を /pages/name-input.jsp に変更してください。 すると、今度は正しく name-input.jsp が表示されます。

ここまでで、図1 の "1. 名前を入力" が作成されました。

Step 2. アクションの追加

さて、フォーム入力を持つページ (name-input.jsp) が Web アプリケーションに追加されたので、 今度はフォームに対するアクションを追加します。

Struts アプリケーションにおいてフォームに対するアクションを追加するためには、
1. フォームに入力されたデータを格納するオブジェクト
2. アクションを実装するメソッドを持つオブジェクト
を作成し、それらに関する設定を struts-config.xml に記述しなければなりません。
Struts フレームワークでは、これらを簡潔に記述するためにいくつかのタグライブラリが用意されています。 (もちろん、実際の使用目的はこれだけではありません。)

今回は、Struts が提供するタグライブラリの中でも、最も頻繁に利用する struts-html タグライブラリを利用します。 先程の name-input.jsp の <form> 要素を <html:form> に変更し、再び helloworld にアクセスしてください。

:Note.
タグライブラリで記述した部分が実際にどのような HTML を生成しているかは、ブラウザの"ソースを表示"を利用するとよくわかります。

org.apache.jasper.JasperException: /pages/name-input.jsp(10,0) TLD又はタグファイルによると、属性 action はタグ form には必須です

すると、ブラウザに上記のエラーメッセージが表示されます。 これは <html:form> タグに必須の属性である "action" が指定されていないためです。 action 属性では、submit 押下時に起動させるアクションのパスを指定します。

そこで、<html:form> タグに属性 action を追加します。 今回は、アクションのパスを "/helloAction" とします。
<html:form action="/helloAction"> と修正したら、再び、helloworld にアクセスしてください。

javax.servlet.ServletException: アクション /helloAction に対応するマッピングが見つかりません

次は、ブラウザに上記のエラーメッセージが表示されます。 これは、struts-config.xml に "/helloAction" というパスを持つアクションが存在しないために発生するエラーです。 そこで、struts-config.xml 中、action-mappings の子要素として、以下のようなアクションを加え、 Tomcat を再起動し、helloworld にアクセスしてください。

<action
  path="/helloAction" />
			

javax.servlet.ServletException: アクション /helloAction のフォームbean null に対する定義が見つかりません

次に、ブラウザ上に表示されるメッセージは上記です。 これは、パス "helloAction" であるアクションに対して属性 name が指定されていないため、 このアクションに対するフォームの名前として "null" が設定され、名前 "null" というフォームに対する定義が無いために発生したエラーです。

:Note.
アクションに対するフォームは属性 name で指定します (form では無いことに注意してください) 。

とりあえず、path="/helloAction" であるアクションに属性 name="inputForm" を追加し、 Tomcat を再起動、helloworld に再アクセスしてください。

今度は、先程の "null" が "inputForm" に代わったエラーが表示されています。 すなわち、アクションで利用するフォームは action とは別に定義しなければなりません。

フォームに対する定義は form-bean タグを利用して記述します。 struts-config.xml 中、タグ <struts-config> の直後に <form-beans> を記述し、 その子要素に <form-bean> を加えてください。 その後、Tomcat を再起動、helloworld に再アクセスしてください。

	<struts-config>
		<form-beans>
			<form-bean name="inputForm" />
		</form-beans>
		・・・
			

javax.servlet.ServletException: クラス null のbeanを生成する際の例外: {1}

次に表示されるエラーは上記です。 エラーの内容は空文字列で指定された bean を生成しようとしたがために発生したエラーです。 実際の原因は、先程の form-bean の設定で type 要素を記述しなかったことにあります。

フォーム Bean の作成

先に説明したように、Struts ではフォームに入力されたデータをオブジェクトに格納し、 そのオブジェクトを操作することでフォーム入力データを操作します。 このためのメカニズムとして用意されているのが、ActionForm です。 Struts アプリケーションでは、フォーム入力データはActionForm を継承した JavaBeans を利用して操作します。 具体的には、以下の規約に従う JavaBeans を利用します。

フォーム Bean 規約
  • org.apache.struts.action.ActionForm を継承していること
  • HTML のリクエスト変数(<input> タグの name 属性) に対するアクセサメソッド(seter, getter)を持つこと。
  • HTML のリクエスト変数はオブジェクトであること(= int などのプリミティブな型は NG)

上記を踏まえて、フォーム Bean を作成します。 <HELLO_APP>/WEB-INF/src/java/helloworld/InputForm.java を以下のように作成してください。

	package helloworld;
	
	import org.apache.struts.action.ActionForm;
	
	public class InputForm extends ActionForm {
		private String username;
		
		public String getUsername() {	return username; }
		
		public void setUsername(String username) {	this.username = username; }
	}
			

上記の InputForm.java を作成した後、コンパイルし、 <HELLO_APP>//WEB-INF/classes/ 以下にパッケージ構成を考慮しながらクラスファイルを配置してください。 (この辺りの作業は eclipse の自動ビルド機能を使うと便利です。)

<HELLO_APP>//WEB-INF/classes/helloworld/InputForm.class の作成後、 この Bean を inputForm に対する Bean として利用するように、struts-config.xml を編集します。 編集後、Tomcat を再起動し、helloworld にアクセスしてください。

	<struts-config>
		<form-beans>
			<form-bean name="inputForm" type="helloworld.InputForm" />
		</form-beans>
		・・・
			

The server encountered an internal error (パス /helloAction に対するアクションのインスタンスがありません) that prevented it from fulfilling this request.

次に表示されるエラーは上記です。 エラーの内容は "/helloAction を実行するアクションのインスタンスが存在しない" というものです。 Struts では、アクションを実装するメソッドを持つオブジェクトがアクションを実行します。 つまり、今回の原因は以下の通りです。
/helloAction の指定でアクションを実装しているクラスを指定していないため、クラス名が "null" として登録されている。
<HELLO_APP>//WEB-INF/classes/null というクラスが存在しないため、オブジェクトがインスタンス化されていない

アクションクラスの作成

上記エラーを解決するために、アクションクラスを作成し、/helloAction を実行するクラスとして登録します。 まずは、アクションクラスの作成です。 Struts アプリケーションのアクションクラスは以下の規約に従って実装されなければなりません。

アクションクラス規約
  • org.apache.struts.action.Action を継承していること
  • execute メソッドが記述されていること。
    ただし、execute メソッドは
    返り値の可視性: public
    返り値の型: ActionForward
    引数の型: ActionMapping, ActionForm, HttpServletRequest, HttpServletResponse
    でなければならない。
:Note.
Struts 1.0 までは execute メソッドではなく、perform メソッドでした。

上記を踏まえて、アクションクラスを作成します。 <HELLO_APP>/WEB-INF/src/java/helloworld/HelloWorldAction.java を以下のように作成してください。

	package helloworld;
	
	import javax.servlet.http.HttpServletRequest;
	import javax.servlet.http.HttpServletResponse;
	import org.apache.struts.action.Action;
	import org.apache.struts.action.ActionForm;
	import org.apache.struts.action.ActionForward;
	import org.apache.struts.action.ActionMapping;
	
	public class HelloWorldAction extends Action {
		public ActionForward execute(
				ActionMapping mapping, ActionForm form,
				HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			return mapping.findForward("error");;
		}
	}
			

InputForm.java の時と同様に、上記の HelloWorldAction.java をコンパイルし、 <HELLO_APP>/WEB-INF/classes/ 以下に配置してください。

次に、このアクションクラスを /helloAction に対するアクションとして利用するように、 /helloAction のアクションに属性 type を追加します。 編集後、Tomcat を再起動し、helloworld にアクセスしてください。

	<action
	  name="inputForm"
	  path="/helloAction"
	  type="helloworld.HelloWorldAction" />
			

ブラウザにはブランクページ(何も表示されていないページ)が表示されています。
しかし、ここまでで、図1 の "2. " が作成されました。
ブランクページはアクションの返り値が不正であった場合、表示されるページです。 今回の場合、アクションは "null" を返しています。しかし、struts-config.xml には返り値 "null" に対する記述が存在しません。 この結果、このアクションの返り値は"不正な返り値"としてみなされ、ブランクページが表示されます。

Step 2.b. エラーページの追加

次は、エラーページを表示させます。 まず、<HELLO_APP>/pages/error.jsp を以下のように作成してください。

	<html>
	<head>
	<title>Hello World (Error!!)</title>
	</head>
	<body bgcolor="white">
	<h3>Error!!</h3>
	Please input your name...
	</body>
	</html>
		

エラーページを表示させるためには、アクションの返り値がどのページにフォワードするのかを struts-config.xml に記述しなければなりません。
ここでは、返り値が "error" であった場合、エラーページにフォワードされるように定義します。 struts-config.xml を以下のように修正してください。

	<action
	  name="inputForm"
	  path="/helloAction"
	  type="helloworld.HelloWorldAction" >
	  <forward path="/pages/error.jsp" name="error" />
	</action>
		

次に、HelloWorldAction の返り値を return null; から return mapping.findForward("error"); に変更します。 この結果、フォームに入力された値に関わらず、submit ボタンが押されるとエラーページへ遷移するようになりました。

これまでと同様に、再コンパイルして、Tomcat を再起動し、helloworld にアクセスしてください。

フォームデータへのアクセス

今度は、フォームデータの値によって遷移先を変更します。 具体的には、name-input.jsp で username にデータが入っていない (username == null || username.length() == 0) 場合、 エラーページへと遷移させるようにアクションクラスを変更します。

execute メソッドへのフォームデータの引き渡しは、execute メソッドの引数によって行われます。 execute メソッドの引数である ActionForm オブジェクトが、そのアクションに対するフォームデータを格納したオブジェクトです。 フォームデータへのアクセスは、引数である ActionForm オブジェクトの setter, getter を利用します。

上記を踏まえて、HelloWorldAction.java を以下のように変更し、Tomcat の再起動、hellowworld へのアクセスを行ってください。

	... 略 ...
	public class HelloWorldAction extends Action {
		public ActionForward execute(
				ActionMapping mapping, ActionForm form,
				HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			InputForm inputForm = (InputForm) form;
			if (inputForm.getUsername() == null || inputForm.getUsername().length() == 0) {
				return mapping.findForward("error");
			} else {
				return null;
			}
		}
	}
			

フォームに値を入れた時はブラウザにブランクページが、フォームに値を入れない時はエラーページが表示されるようになりました。
ここまでで、図1 の "2.b. エラーページを表示" が作成されました。

Step 2.a. "Hello" ページの追加

最後に "Helo" ページを追加して、アプリケーションを完成させます。 "Hello" ページでは、ユーザが入力したデータを表示するので、アクションは jsp に対して何らかの方法でデータを渡さなければなりません。

アクションからページへデータを渡すには HTTP リクエストを利用する方法、HTTP セッションを利用する方法などがあります。 今回は、アクションで "username1" という名前でセッションに名前を登録し、ページ側でセッションに登録されたデータを参照するという方法で データ渡しを実現します。

まず、先程の HelloWorldAction.java 中、return null; となっていた部分を return mapping.findForward("hello");に変更し、 struts-config.xml に <forward path="/pages/hello.jsp" name="hello" /> を追加してください。 その後、<HELLO_APP>/pages/hello.jsp を以下のように作成し、Tomcat の再起動後、helloworld にアクセスしてください。

	<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
	
	<html>
	<head>
	<title>Hello World (Hello User)</title>
	</head>
	<body bgcolor="white">
	<h3> Hello, <bean:write name="username1"/> </h3>
	</body>
	</html>
		

javax.servlet.ServletException: どのスコープにもBean username1 がありません

最後のエラーメッセージは上記です。 これは <HELLO_APP>/pages/hello.jsp 中で参照しようとしているデータ "username1" が HTTP リクエストにも、HTTP セッションにも登録されていないことを意味しています (<bean:write name="username1"/> の部分)。

そこで、アクションの中で HTTP セッションとして、データ "username1" にフォーム入力の文字列を代入します。 具体的には、HelloWorldAction を以下のように変更してください。

	... 略 ...
	if (inputForm.getUsername() == null || inputForm.getUsername().length() == 0) {
		return mapping.findForward("error");
	} else {
		request.getSession().setAttribute("username1", inputForm.getUsername());
		return mapping.findForward("hello");
	}
			

この後、コンパイル、Tomcat の再起動をすることで "Hello World" アプリケーションが完成です。

終わりに

今回は、Struts が出力するエラーメッセージを基に Strus アプリケーションの構造を紹介しました。 今回、紹介したエラーメッセージは、ある程度の Struts エラーメッセージをカバーしています(無理矢理、出力させたからですが…)。 今後、Struts アプリケーションを開発する際に、このエラーメッセージがバグ追及の鍵になります。 何かエラーメッセージに遭遇した時は、このページで検索をかけてみてください。

演習問題 (おまけ)

ログインアプリケーションを作成しなさい。
具体的には、ユーザはユーザ ID とパスワードを入力する。 ログインに成功した場合はログイン成功ページ、失敗した場合はログイン失敗ページが表示される Struts アプリケーションを作成しなさい。

解答例はこちら
上記の解答例では、ユーザ名とパスワードに同じ文字列が入力された時、ログイン成功としています。



by Nobuyuki KANEKO