広告

■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2010年12月04日

    Java総合講座 - 初心者から達人へのパスポート
                  vol.210

                                セルゲイ・ランダウ
 バックナンバー: http://www.flsi.co.jp/Java_text/
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■


========================================================
◆ 00.お知らせ(バックナンバーの閲覧に関して)
========================================================


-------------------------------------------------------
・現在、このメールマガジンは以下の2部構成になっています。
[1] 当初からのコース:vol.xxx(xxxは番号)が振られています。
   これは現在、中級レベルになっています。
[2] 2009年11月開講コース:xxx号(xxxは番号)が振られています。
   これは現在、初心者向けのレベルになっています。
・このメールマガジンは、画面を最大化して見てください。
小さな画面で見ていると、不適切な位置で行が切れてしまう
など、問題を起すことがあります。
・このメールマガジンに掲載されているソース・コード及び
文章は特に断らない限り、すべて筆者が著作権を所有してい
ます。また、これらのソース・コードは学習用のためだけに
提供しているものです。
-------------------------------------------------------


========================================================
◆ 01.Strutsのアプリケーション開発(プロジェクト:StrutsShop)
========================================================

再度DbManagerのソース・ファイルに戻りましょう。

その中のconnect()メソッドの部分

--------------------------------------------------------
   public void connect() throws StruShopDbException {
      logger.info("Start ...............");
      try{
         if (conn == null || conn.isClosed()) {

            if ("junit".equals(System.getProperty("ut"))) {
                  Class.forName("com.mysql.jdbc.Driver");
                  conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/STRUSHOP", "root", "rootpass");
                  conn.setAutoCommit(false);
               return;
            }

            Context initCtx = new InitialContext();
            if(initCtx == null) throw new StruShopDbException("Error: InitialContext could not be generated!");
            DataSource ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/STRUSHOP");
            if (ds != null) {
               conn = ds.getConnection();
               if(conn == null) throw new StruShopDbException("Error: Connection does not exist!");
               else logger.info("Connection has been gotten.");
            }
            else {
               throw new StruShopDbException("Error: DataSource does not exist!");
            }
         }
      } catch (NamingException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (SQLException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (StruShopDbException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
   }
--------------------------------------------------------

を見ると、この中の

      catch (Throwable e) {
         logger.error(e, e);
      }

の部分ではログ出力以外は何もしていません。つまり、
         throw new StruShopDbException("Error: Connect() failed!", e);
のような処理は行っていません。

これでは、何らかの不慮の例外が投げられてこのcatchブロックが処理をしても、
その例外は呼び出し元には伝わりません。
呼び出し元であるItemListActionクラスのexecute()メソッドに戻っても、
execute()メソッドの中では正常な処理が続行されることになります。そして、
forwardの値が"success"のまま

      return mapping.findForward(forward);

の行までたどり着いてしまうことになります。


試しにこのconnect()メソッドの部分を

--------------------------------------------------------
   public void connect() throws StruShopDbException {
      logger.info("Start ...............");
      try{
         int zero = 0;
         int zeroDivided = 100 / zero;
         if (conn == null || conn.isClosed()) {

            if ("junit".equals(System.getProperty("ut"))) {
                  Class.forName("com.mysql.jdbc.Driver");
                  conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/STRUSHOP", "root", "rootpass");
                  conn.setAutoCommit(false);
               return;
            }

            Context initCtx = new InitialContext();
            if(initCtx == null) throw new StruShopDbException("Error: InitialContext could not be generated!");
            DataSource ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/STRUSHOP");
            if (ds != null) {
               conn = ds.getConnection();
               if(conn == null) throw new StruShopDbException("Error: Connection does not exist!");
               else logger.info("Connection has been gotten.");
            }
            else {
               throw new StruShopDbException("Error: DataSource does not exist!");
            }
         }
      } catch (NamingException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (SQLException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (StruShopDbException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
   }
--------------------------------------------------------

のように書き換えてみて下さい。つまり、わざと0で割り算するという誤りを
おかすことによって、ArithmeticExceptionを発生させます。
(わざとこんな誤りを組み込むことは馬鹿げていますが、何らかの不慮のエラー
の代わりにこの例外を発生させることによって実験を行うのです。)

この状態で再度ItemListActionTestのJUnitテストを実行してみると、
やはりforwardの値は"success"になってしまい、テスト結果には、やはり

junit.framework.AssertionFailedError: was expectiong '/systemerror.jsp' but received '/itemList.jsp'

というエラー・メッセージが表示されてしまいますね。


そこで、このconnect()メソッドの部分を、例えば

--------------------------------------------------------
   public void connect() throws StruShopDbException {
      logger.info("Start ...............");
      try{
         if (conn == null || conn.isClosed()) {

            if ("junit".equals(System.getProperty("ut"))) {
                  Class.forName("com.mysql.jdbc.Driver");
                  conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/STRUSHOP", "root", "rootpass");
                  conn.setAutoCommit(false);
               return;
            }

            Context initCtx = new InitialContext();
            if(initCtx == null) throw new StruShopDbException("Error: InitialContext could not be generated!");
            DataSource ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/STRUSHOP");
            if (ds != null) {
               conn = ds.getConnection();
               if(conn == null) throw new StruShopDbException("Error: Connection does not exist!");
               else logger.info("Connection has been gotten.");
            }
            else {
               throw new StruShopDbException("Error: DataSource does not exist!");
            }
         }
      } catch (NamingException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (SQLException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (StruShopDbException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      }
      finally {
         logger.info("End ...............");
      }
   }
--------------------------------------------------------

のように書き換えたくなりますが、不慮のエラーに対してStruShopDbExceptionを
投げるのはちょっと変です。

StruShopDbExceptionはデータベースがらみの例外につけて用意したものであり
(そのためにDbという名前を含めている)、また、データベースがらみのエラー
はほとんどの場合、原因が容易につきとめられるので特別扱いできます。
(ほとんどの場合は、RDBMSがダウンしていた等であり、予め対処手順をマニュアル化
可能で、オペレーターが手順通りに操作すれば済む場合が多い。)

その点、その他の不慮のエラーに対してStruShopDbExceptionを用いるのは不適切だし、
混乱の元になります。
(対処法が不確定であり、万全の体制をとって原因を究明しなければわからない
場合が多いので、それを識別するためにStruShopDbExceptionとは別の名前の例外
クラスにしたほうがよい。)


◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆

そこで、別の例外クラスを用意することにします。ここでは

StruShopOtherException

という名前で例外クラスを作成しましょう。

(1) プロジェクト・エクスプローラー内のStrutsShopの配下の「Javaリソース: src」
の配下のjp.co.flsi.lecture.strutsを右クリックし、「新規」→「クラス」を
選択します。

(2) 「名前」欄に

StruShopOtherException

と入力し、「スーパークラス」欄にはjava.lang.Exceptionを指定して、
「完了」ボタンをクリックします。

(3) StruShopOtherException.javaのエディターが開いたら、下記のように
編集しましょう。

--------------------------------------------------------
package jp.co.flsi.lecture.struts;

public class StruShopOtherException extends Exception {

   public StruShopOtherException() {
   }

   public StruShopOtherException(String message) {
      super(message);
   }

   public StruShopOtherException(Throwable cause) {
      super(cause);
   }

   public StruShopOtherException(String message, Throwable cause) {
      super(message, cause);
   }

}
--------------------------------------------------------


このファイルを保管(Ctrl+S)し、閉じましたら、DbManager.javaファイル
に戻りましょう。


◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆

そして先ほどの

      catch (Throwable e) {
         logger.error(e, e);
      }

の部分を

      catch (Throwable e) {
         logger.error(e, e);
         throw new StruShopOtherException("Error: Connect() failed!", e);
      }

に書き換えましょう。(コンテンツ・アシストの機能を利用してimport文も入れて
おきます。)そして保管(Ctrl+S)しましょう。

すると、
throw new StruShopOtherException("Error: Connect() failed!", e);
の部分がコンパイル・エラーになりますね。(そこに赤い下線が引かれ、その行の
左側に赤い×マークが付きます。)

これは、このStruShopOtherExceptionが処理されていない(catchもthrowsもされて
いない)からです。

この赤い下線が引かれたStruShopOtherExceptionの中にカーソルを入れ、Ctrl+1キー
(Ctrlキーを押しながら数字の1のキーを押す)を押しましょう。

そして、ポップアップ・メニューの中から「スロー宣言の追加」を選択(クリックして
Enterキーを押す)します。
すると、connect()のthrowsにStruShopOtherExceptionが追加されますね。

念のため、その結果できたDbManager.javaのソース・コード全体を下に提示して
おきます。

--------------------------------------------------------
package jp.co.flsi.lecture.struts.db;

import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Connection;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import jp.co.flsi.lecture.struts.StruShopOtherException;

import org.apache.log4j.Logger;

public class DbManager {
   protected Connection conn = null;
   private static Logger logger = Logger.getLogger(DbManager.class);

   public void connect() throws StruShopDbException, StruShopOtherException {
      logger.info("Start ...............");
      try{
         if (conn == null || conn.isClosed()) {

            if ("junit".equals(System.getProperty("ut"))) {
                  Class.forName("com.mysql.jdbc.Driver");
                  conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/STRUSHOP", "root", "rootpass");
                  conn.setAutoCommit(false);
               return;
            }

            Context initCtx = new InitialContext();
            if(initCtx == null) throw new StruShopDbException("Error: InitialContext could not be generated!");
            DataSource ds = (DataSource) initCtx.lookup("java:comp/env/jdbc/STRUSHOP");
            if (ds != null) {
               conn = ds.getConnection();
               if(conn == null) throw new StruShopDbException("Error: Connection does not exist!");
               else logger.info("Connection has been gotten.");
            }
            else {
               throw new StruShopDbException("Error: DataSource does not exist!");
            }
         }
      } catch (NamingException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (SQLException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (StruShopDbException e) {
         logger.error(e, e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      }
      catch (Throwable e) {
         logger.error(e, e);
         throw new StruShopOtherException("Error: Connect() failed!", e);
      }
      finally {
         logger.info("End ...............");
      }
   }

   public void disconnect() {
      logger.info("Start ...............");
      try {
         if (conn != null && !conn.isClosed()) conn.close();
      } catch(SQLException e) {
         logger.error(e, e);
      }
      catch (Throwable e) {
         logger.error(e, e);
      }
      finally {
         logger.info("End ...............");
      }
   }

}
--------------------------------------------------------

なお、disconnect()メソッドのほうのcatchブロックでもStruShopDbExceptionや
StruShopOtherExceptionをthrowする必要があるのではないか、と気にする人も
いるかも知れませんが、disconnect()メソッドが呼び出される時点では、アプリ
ケーションの本質的な作業(データベースへの書き込みや検索)は終わっている
ので、気にする必要はないのです。したがって、ここで例外をthrowする必要は
ありません。(このままほうっておくと、次回の接続のときconnect()メソッド
でエラーが出るかも知れませんが、そのときには例外がthrowされます。)


では、このファイルを保管(Ctrl+S)しておきましょう。


◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆

すると、この変更に影響を受けるクラスが出てきますね。プロジェクト・エクスプローラー
を見て下さい。
StrutsShop配下の「Javaリソース: src」の配下のjp.co.flsi.lecture.struts配下
のItemSelectForm.javaの左側にコンパイル・エラーのマーク(赤い×マーク)が
付いていますね。

このItemSelectForm.javaファイルを開いてみて下さい。

このソース・コードの中のreset()メソッドの中の

--------------------------------------------------------
      try {
         request.setCharacterEncoding("UTF-8");
         ServletContext serv = getServlet().getServletContext();
         Vector<Category> categoryList = (Vector<Category>) serv.getAttribute("categoryList");
         if (categoryList == null) {
            dbManager.connect();
            categoryList = dbManager.getDataByName("");
            serv.setAttribute("categoryList", categoryList);
         }
      } catch (UnsupportedEncodingException e) {
         logger.error(e, e);
      } catch (StruShopDbException e) {
         logger.error(e, e);
      }
--------------------------------------------------------

の中の

            dbManager.connect();

の部分に赤い下線が引かれていますから、そこにマウス・ポインターを持っていくと
StruShopOtherExceptionが処理されていないためのエラーであることがわかりますね。
したがって、StruShopOtherExceptionに対するcatchブロックを追加すればエラーは
消えますので、下記のように編集しましょう。

--------------------------------------------------------
      try {
         request.setCharacterEncoding("UTF-8");
         ServletContext serv = getServlet().getServletContext();
         Vector<Category> categoryList = (Vector<Category>) serv.getAttribute("categoryList");
         if (categoryList == null) {
            dbManager.connect();
            categoryList = dbManager.getDataByName("");
            serv.setAttribute("categoryList", categoryList);
         }
      } catch (UnsupportedEncodingException e) {
         logger.error(e, e);
      } catch (StruShopDbException e) {
         logger.error(e, e);
      }
      catch (StruShopOtherException e) {
         logger.error(e, e);
      }
--------------------------------------------------------


なお、DbManagerのときと同様に、ここでもcatchブロックに

      catch (StruShopOtherException e) {
         logger.error(e, e);
         throw new StruShopOtherException("・・・");
      }

のようにthrow文を入れることによって、reset()メソッドの外に例外を伝えたい
と思うかもしれませんが、それはできません。

なぜかというと、このreset()メソッドはActionFormのreset()メソッドを
オーバーライド(override = 上書き)したものであり、メソッドに
throwsを勝手に付け足して

public void reset(ActionMapping mapping, HttpServletRequest request) throws Exception

のような定義形式にすることはできないからです。

┌補足─────────────────────────┐
ここでいう定義形式とは、メソッドの(publicなどの)修飾子、
戻り値、メソッド名、引数、およびthrows節を組み合わせた形式
(たとえば

public int nantokaMethod(int param1, String param2) throws NantokakantokaException

のようなもの)を言い、Javaにおけるオーバーライド(override)
では、定義形式に一定のルールがあります。

オーバーライド(override)は(当メールマガジン3回目の
オブジェクト指向の話のときに出てきましたが)Javaではサブ
クラスとスーパークラスでメソッド名と引数が同じ(ここでいう
「引数が同じ」とは、引数の個数および各引数の型が同じである
ことを意味する。つまり、引数のパターンが同じであることを
意味する)メソッドが存在すると、オーバーライドされたと
みなされます。
つまり、同じ呼び出し形式(= シグネチャー(signature))のメ
ソッドが存在すると、オーバーライドされたとみなされます。

このとき両者のメソッドで定義形式に食い違いがあると
(たとえば戻り値の型に互換性がなかったり、throws節の例外
の型に互換性がないと)コンパイル・エラーになります。
(サブクラスにおける戻り値やthrows節の例外の型はそれぞれ
スーパークラスのものと同一か、もしくはそれぞれスーパー
クラスにおける戻り値やthrows節の例外のサブクラスになって
いなければならない。つまり、サブクラスのものはスーパー
クラスのものと同一の型かもしくはサブクラスになっていなけ
ればならないのである。)

また、アクセス制御の修飾子は同一か、もしくは、protectedを
publicに書き換えるなど公開度を上げる方向に修飾子を書き換
えることはできますが、逆はできません。(「元々非公開だっ
たものを公開するのは簡単だが、既に公開してしまって皆に
知れ渡ってしまったものを非公開にするのは簡単ではない。」
と覚えておきましょう。)

なお、privateのメソッドをオーバーライドすることはできま
せん。もしprivateのメソッドと同じメソッド名、同じ引数の
パターン(同じシグネチャー)のメソッドをサブクラスに作成
した場合は、それは別のメソッドと見なされます。なぜなら、
privateはそのクラス外には非公開でサブクラスからさえも見え
ないので、サブクラスにはスーパークラスにおける同じメソッ
ドの存在がわからないからです。
つまり、ちょっと紛らわしいですが、スーパークラスの
privateのメソッドと同じシグネチャーのメソッドをサブクラス
に作成した場合は、それはオーバーライドとは呼ばないのです。

また、修飾子にstaticが指定されているメソッド(staticメソッド)
の場合はサブクラスでstaticをはずすことはできませんし、
その逆もできません。
つまり、staticの指定の有無は両者で一致していなければなり
ません。
└───────────────────────────┘

では、このreset()メソッドの外に例外の情報を伝えたいときにはどうすれば
いいかと言うと、request(HttpServletRequest)オブジェクトやsession
(HttpSession)オブジェクトの属性を通して情報を受け渡してやります。


では、実際にやってみましょう。

でもその前に、現時点でこのWebアプリケーションがどのような動きになるか
確認してみましょう。

例によってMySQLを停止してから、ItemListActionTestのJUnitテストを実行して
みて下さい。テスト結果はOK(緑色)になりますね。

ところが、StrutsTestCaseではあくまでActionクラス(ここではItemListAction)
のexecute()メソッド(およびexecute()メソッドの中で呼び出される各オブジェクト
のメソッド)のテストだけをしていることに注意して下さい。ActionForm(ここでは
ItemSelectForm)のreset()メソッドなどのテストはしてくれません。


そこで、ActionForm(のreset()メソッドなど)の動作に問題がないかどうか検証する
ために、Tomcatを起動してテストします。

(1) 「サーバー」ビューの中の「ローカル・ホストのTomcat v5.5サーバー」を
右クリックし、「開始」を選択します。しばらくして、

ローカル・ホストのTomcat v5.5サーバー[始動済み,同期済み]

というように、後ろに「始動済み」の表示が出たら、Tomcatの起動が完了しています。

(2) Webブラウザー(Internet Explorer)を起動して、URL

http://localhost:8080/StrutsShop/itemSelect.jsp

を入力しましょう。

すると「商品の検索」のWebページは開かずに何やらエラー・メッセージが出ますね。

その中に

org.apache.jasper.JasperException: categoryList という名前のbeanが見つかりません

というようなメッセージがあるはずです。

Eclipseのコンソールの中のログにも同様に「categoryList という名前のbeanが見つかりません」
というメッセージが出ているはずですが、そのスタック・トレースを追っかけていくと

   at org.apache.jsp.itemSelect_jsp._jspx_meth_html_005foptions_005f0(itemSelect_jsp.java:251)

というような行がありますね。これはitemSelect.jspから自動生成されたJavaファイルの中で
エラーが起こっていることを意味していますが、itemSelect.jspの中に指定されているcategoryList
をItemSelectForm.javaのソース・コードの中で探せば、原因がわかります。

ItemSelectForm.javaの中にある

--------------------------------------------------------
         if (categoryList == null) {
            dbManager.connect();
            categoryList = dbManager.getDataByName("");
            serv.setAttribute("categoryList", categoryList);
         }
--------------------------------------------------------

という部分の中の

dbManager.connect();

のところでデータベースへの接続時に例外が発生したためにその下のcatch文のほうに
飛んでしまい、

            categoryList = dbManager.getDataByName("");
            serv.setAttribute("categoryList", categoryList);

の行は実行されなかった訳ですね。
すると、categoryListはnullのままだし、いずれにしても

            serv.setAttribute("categoryList", categoryList);

が実行されていないためにcategoryListの情報はJSPのほうには伝わりません。

というわけで、「categoryList という名前のbeanが見つかりません」というエラーに
なるわけです。

そこで、データベースに接続できず例外が発生したときには(catchブロックの中などで)

         categoryList = new Vector<Category>();
         Category aCategory = new Category();
         aCategory.setName("すべて");
         aCategory.setNum("99999");
         categoryList.add(aCategory);
         serv.setAttribute("categoryList", categoryList);

のようなコードで、categoryListをちゃんと用意するようにしておけば、
先ほどのエラーは出なくなります。

しかしながら、データベースに接続できないのであれば、結局のところ
それから先の操作には進められないことになりますから、こういった小細工
をしても無駄になります。
というわけで、こういった無駄な抵抗は、やめることにしましょう。

そして、データベースに接続できず例外が発生したときには直ちに
前回のsystemerror.jspのWebページ、つまり

      まことに申し訳ございません。
      現在システムがダウンしています。
      数時間程度で回復する予定ですので、数時間のちに再度
      初めから操作をやり直してみて下さい。
      あるいは、お急ぎの場合はTel:123-456-7890までお問合せ下さい。
     
というようなメッセージ内容のWebページを表示するようにしましょう。

では、先ほどのrequest(HttpServletRequest)オブジェクトやsession(HttpSession)
オブジェクトの属性を通して例外の情報を受け渡す話に戻り、このWebページを表示
するためのプログラミングをすることにします。

(話が長くなりましたので、いったんここで区切ります。)


◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆ ◆


(次回に続く)



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      http://www.flsi.co.jp/Java_text/
★このメールマガジンは
     「まぐまぐ(http://www.mag2.com)」
 を利用して発行しています。
★バックナンバーは
      http://www.flsi.co.jp/Java_text/
 にあります。
★このメールマガジンの登録/解除は下記Webページでできます。
      http://www.mag2.com/m/0000193915.html
★このメールマガジンへの質問は下記Webページにて受け付けて
 います。わからない所がありましたら、どしどしと質問をお寄
 せください。
      http://www.flsi.co.jp/Java_text/
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Copyright (C) 2010 Future Lifestyle Inc. 不許無断複製