広告

■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2010年12月10日

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

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


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


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


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

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


まずItemSelectForm.javaを下記のように編集して下さい。

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

import java.io.UnsupportedEncodingException;
import java.util.Vector;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import jp.co.flsi.lecture.struts.db.Category;
import jp.co.flsi.lecture.struts.db.CategoryDbManager;
import jp.co.flsi.lecture.struts.db.StruShopDbException;

import org.apache.log4j.Logger;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMapping;

public class ItemSelectForm extends ActionForm {
   private static Logger logger = Logger.getLogger(ItemSelectForm.class);
   private String keyword = null;
   private String categoryNum = null;

   public void reset(ActionMapping mapping, HttpServletRequest request) {
      logger.info("Start ...............");
      CategoryDbManager dbManager = new CategoryDbManager();
      String exceptionInfo = "";
      request.setAttribute("exceptionInfo", exceptionInfo);
      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);
         exceptionInfo = "systemerror";
         request.setAttribute("exceptionInfo", exceptionInfo);
      }
      catch (StruShopOtherException e) {
         logger.error(e, e);
         exceptionInfo = "systemerror";
         request.setAttribute("exceptionInfo", exceptionInfo);
      }
      finally {
         try {
            dbManager.disconnect();
         }
         catch (Exception e) {
            logger.info("Disconnect failed or there is no connection.");
         }
         logger.info("End ...............");
      }
   }

   public void setKeyword(String keyword) {
      this.keyword = keyword;
   }

   public String getKeyword() {
      return keyword;
   }

   public void setCategoryNum(String categoryNum) {
      this.categoryNum = categoryNum;
   }

   public String getCategoryNum() {
      return categoryNum;
   }

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


変更したのは、先頭のほうに

      String exceptionInfo = "";
      request.setAttribute("exceptionInfo", exceptionInfo);

を追加し、適切なcatchブロックに

         exceptionInfo = "systemerror";
         request.setAttribute("exceptionInfo", exceptionInfo);

という行を追加したところですね。

つまり、requestオブジェクトのsetAttribute()メソッドを実行することによって、
requestオブジェクトの属性として例外の情報を保存しています。
(sessionオブジェクトを使ってもいいのですが、今回は一時的に情報を引き渡す
だけなのでrequestオブジェクトで充分です。)

なお、catch (UnsupportedEncodingException e)のブロックのほうには
このような行は追加していませんが、これは"UTF-8"のエンコーディングが
サポートされていないはずがない(つまり、このブロックが実行されること
はありえない)ため、無視したものです。



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

では、これに対応して、itemSelect.jspファイルのほうも編集しましょう。

Tomcatはいったん停止しておきましょう。(起動したままでも編集はできますが、
Javaファイルを編集したときに自動的にアプリケーションの再ロードをしようと
して負荷がかかりますので、余計なロードをしないように停止しておきます。)


itemSelect.jspファイルを下記のように編集してみて下さい。

--------------------------------------------------------
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
<%@ taglib uri="/tags/struts-logic" prefix="logic" %>
<%@ taglib uri="/tags/struts-html" prefix="html" %>
<%@ taglib uri="/tags/struts-nested" prefix="nested" %>

<html:html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
      <title>商品の検索</title>
   </head>
   <body bgcolor="#77ffff" text="#fa5a00">
      <h1>商品の検索</h1>
      <html:form method="POST" action="/itemlist">
         <logic:match name="exceptionInfo" value="systemerror">
            <logic:redirect page="/systemerror.jsp"/>
         </logic:match>

         <logic:notMatch name="exceptionInfo" value="systemerror">
            キーワード(商品名の一部)やカテゴリーを入力し、「商品検索」ボタンをクリックしてください。
            <br>
            <br>
            キーワード:<html:text property="keyword" size="20" />
            <br>
            カテゴリー:
            <html:select property="categoryNum">
               <html:options collection="categoryList" property="num" labelProperty="name" />
            </html:select>
            <br>
            <br>
            <html:submit property="submit" value="商品検索" />
         </logic:notMatch>
      </html:form>
   </body>
</html:html>
--------------------------------------------------------


ここで、

         <logic:match name="exceptionInfo" value="systemerror">

というタグはexceptionInfoの値が"systemerror"のときに以下(</logic:match>まで
の行)を実行するというStruts専用のタグで、

         <logic:notMatch name="exceptionInfo" value="systemerror">

というタグはその逆(exceptionInfoの値が"systemerror"でないときに以下
(</logic:notMatch>までの行)を実行)です。

また、

            <logic:redirect page="/systemerror.jsp"/>

というタグはsystemerror.jspのWebページへの遷移を実行するものです。

これらのコードによって、requestオブジェクトのexceptionInfo属性の値が
"systemerror"のときにはsystemerror.jspのWebページへ遷移することになり、
それ以外のときは今まで通りのWebページが表示されることになります。



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

では、Tomcatを開始し、(MySQLは停止した状態で)テストしてみましょう。

期待通り、Webページに

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

というメッセージが表示されるようになりましたね。これはsystemerror.jspの
Webページです。意図通りになりました。



では、今度はMySQLを開始してから再度テストしてみて下さい。

今度は、正常時の商品検索のWebページになりますね。これでOKです。



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

さて、このItemSelectFormに

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

というブロックが書かれていないことを不思議に思った人もいるかも
知れません。

今回このItemSelectFormに修正が必要なことに気づくことができたのは、
catch (Throwable e)ブロックを入れていなかったためであり、もし
catch (Throwable e)のブロックを入れていたら、tryブロックの中で呼び
出しているメソッド(ここではdbManager.connect())が新たな例外を投げ
るように修正されたとしても、コンパイル・エラーにはならず(なぜなら
catch (Throwable e)はどんな例外でもとらえてしまいますので)、見過ご
してしまったかも知れません。

そして、それが特殊な例外で、catchブロックの中で特別な処理を必要とする
ような場合には、これを見過ごすことはバグを作りこむことを意味します。

つまり、catch (Throwable e)ブロックを入れないほうが問題を発見しやすい
のです。

そのことを話すためのきっかけを作るために、わざとcatch (Throwable e)
ブロックを書かなかったのです。


という訳で、catch (Throwable e)ブロックのような、どんな例外でも
とらえてしまうcatchブロックは最初は入れないようにしておいて、
テストをし尽くして、発生しえる個々の例外を確認し、それらの個々
の例外に対処するcatchブロックを追加し終えた後で、(不慮の例外を
とらえてログに書き出すために)catch (Throwable e)ブロックを最後
に追加するという手順にするのが望ましいのです。

とは言え、旧式の開発手順を採用している開発現場など、現場によっては
このような手順はとれない場合が多いです。

最近のオブジェクト指向に基づいた迅速(agile)な開発手法ではこのような
手順が取れるのですが、こういった開発手順の話は、後ほど開発方法論の話
をするときに詳しく解説していきます。



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

さて、これまでログを何度か見てきましたが、同じスタック・トレースが
複数回繰り返し出力されていることに気づいたことと思います。

これについて見てみましょう。


DbManager.javaファイルをもう一度開いてもらって、その中の

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

というcatchブロックを見て下さい。このcatchブロックではStruShopDbException
をとらえてログ出力した後、再度StruShopDbExceptionをthrowしています。

そして、このthrowされたStruShopDbExceptionは呼び出し元のメソッド(たとえば
先ほど(前回)の例ではItemListActionのexecute()メソッド)の中で再度catchされ、

--------------------------------------------------------
      } catch (StruShopDbException e) {
         logger.error(e, e);
         forward = "systemerror";
      }
--------------------------------------------------------

のように再度ログ出力されます。

そうするとStruShopDbExceptionに対して2回ログが出力されることになります。
そしてどちらのログ出力でもスタック・トレースが取られます。
ところが、スタック・トレースは1回出力するだけで例外の発生原因の所まで
追跡することができますので、1回出力するだけで充分であり、重複して2回以上
出力することは無駄になります。

一応、DbManagerのcatchブロックではStruShopDbExceptionを再度throwするときに
"Error: Connect() failed!"という情報を付け加えてnewしたStruShopDbException
でStruShopDbExceptionを包んでいるので、新たな情報が付加されている訳ですが、
少なくともスタック・トレースのほうはここでは必要ないわけです。

特にスタック・トレースは出力量が多いので、重複して出力することは、ハード
ディスクなどの資源の浪費になり、好ましくありません。


という訳で、「例外をcatchし、スタック・トレースを出力後、再度
例外をthrowし直す」ということはなるべく避けるべきです。つまり、
再度例外をthrowし直すcatchブロックではスタック・トレースは出力
しないようにすべきです。
これは例外の種類にかかわらず、catchブロック全般に言えることです。
そしてスタック・トレースの出力は最終的に例外を処理するcatchブロック
(例外をthrowし直さないcatchブロック)のみで行うようにします。


という訳で、最終的な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);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (SQLException e) {
         logger.error(e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      } catch (StruShopDbException e) {
         logger.error(e);
         throw new StruShopDbException("Error: Connect() failed!", e);
      }
      catch (Throwable e) {
         logger.error(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 ...............");
      }
   }

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

のようにしましょう。
つまり、
         logger.error(e, e);

         logger.error(e);
に書き換えることによって、スタック・トレースを出力しないようにします。



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

他にも修正すべきJavaファイルはたくさんありますが、他のJavaファイル
の修正作業もこれまでに述べてきたことと同様ですので、説明を省略し、
あとは各自の自主課題とさせて頂きます。



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


(次回に続く)



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      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. 不許無断複製