2012年4月28日土曜日

例外処理

データベースアクセスの際に使われる決まり文句にはトランザクション境界を明示する begin, commit/rollback の他、例外を捕まえるための tyr {..} catch {..} [final {..}] があります。前者については @Transactional アノテーションや『トランザクションを“Declarative”に管理する』で紹介した“Declarative transaction management(宣言的トランザクション管理)”が使えます。一方、例外処理においても Spring Framework には便利な機能が用意されています。

HandlerExceptionResolver
その一つが HandlerExceptionResolver です。リファレンスの“16.11 Handling exceptions”には「Spring の HandlerExceptionResolver 実装は、コントローラー実行中に発生した予測不能の例外を処理する」とあります。

このインターフェースを使えば、Spring に DAO レイヤーから上げられた例外を特定のビューに解決させることができます。例えば以下のような感じです。

HibernateExceptionResolver.java
HandlerExceptionResolver を実装したカスタム例外リゾルバーです。同インターフェースに定義された resolveException() メソッドを実装しています。このメソッド内では、受け取った例外を instanceof で判別し、戻り値として ModelAndView に特定のビューをセットしています。必要とあらば例外情報をログに記録するなどの処理を挟むこともできます。このクラスでタッチしない例外の場合は null を返します。

尚、このクラス(HibernateExceptionResolver)では、抽象クラス AbstractHandlerExceptionResolver の作法を参考に HandlerExceptionResolver の他、Ordered を実装しています。order の値が大きいほど優先順位が低くなります。デフォルトとして Integer.MAX_VALUE を設定しています。
package wrider.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.hibernate.HibernateException;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

public class HibernateExceptionResolver 
    implements HandlerExceptionResolver, Ordered {
  
  private int order = Integer.MAX_VALUE;
  
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    ModelAndView mav = new ModelAndView();
    
    if (ex instanceof HibernateException) {
      mav.addObject("pageTitle", "Sorry");
      mav.setViewName("ex/dae");
      return mav;
    }
    else {
      return null;
    }
  }
  
  public int getOrder() {
    return this.order;
  }

}

上記リゾルバーを applicationContext.xml に登録します。
<!-- Exception Resolver -->
  <bean class="wrider.controller.HibernateExceptionResolver"/>

SimpleMappingExceptionResolver
もしも、これといった例外処理は必要なく、単純に特定のビューに解決するだけでいいのであれば SimpleMappingExceptionResolver が簡単です。以下のような感じで applicationContext.xml に SimpleMappingExceptionResolver を登録し、例外クラスとビュー名を設定するだけです。
<!-- Exception Resolve & Translate -->
  <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
      <map>
        <entry key="org.hibernate.HibernateException" value="ex/dae"></entry>
      </map>
    </property>
  </bean>

@ExceptionHandler
また、コントローラークラス/メソッドに @ExceptionHandler アノテーションを付ける方法もあります。以下のような感じです。

AccountController.java(抜粋)
HibernateException が上がってきたら HibernateExceptionHandler()メソッドで処理するように定義しています。
@ExceptionHandler(org.hibernate.HibernateException.class)
  public ModelAndView HibernateExceptionHandler(org.hibernate.HibernateException ex, HttpServletRequest request) {
    ModelAndView mav = new ModelAndView();
    mav.setViewName("ex/dae");
    return mav;
  }

DAO メソッドのスリム化
トランザクションや例外に関する設定、処理を本体コードの外側に定義することで、本体コードを大幅にスリム化できます。例えば、以前作った ProfileManagement.javagetUserProfile()メソッドは以下のようになります。

使用前
public UserProfile getUserProfile(IdCard idCard) {
    
    final String HQL = 
      "from UserProfile as up " + 
      "where up.email = :email and up.password = :password";
    
    /*
     * Boilerplate pattern
     */
    
    UserProfile result = null;
    Session session = txManager.getSessionFactory().getCurrentSession();
    Transaction tx = session.getTransaction();
    
    try {
      tx.begin();
      
      result = (UserProfile)session.createQuery(HQL)
            .setParameter("email", idCard.getEmail())
            .setParameter("password", idCard.getPassword())
            .uniqueResult();
      
      tx.commit();
      
    } catch (Exception e) {
      tx.rollback();
      System.out.println("error occured: " + e.toString());
    }
    
    return result;
    
  }

使用後
決まり文句の排除に加え、setParameter()メソッドで個々にパラメータをセットする代わりに、setProperties()メソッドを使い HQL のプレースホルダーに、IdCard オブジェクトのフィールド値を割り当てています。
public UserProfile getUserProfile(IdCard idCard) {
    
    final String HQL = 
      "from UserProfile as up " + 
      "where up.email = :email and up.password = :password";
    
    return (UserProfile)txManager.getSessionFactory().getCurrentSession()
            .createQuery(HQL)
            .setProperties(idCard)
            .uniqueResult();
  }

実験
試しに上記コードの where up.email = :email .. の部分を up.mail に変えて例外を発生させてみました。すると上のほうで紹介したどの方法でも、図のような「ごめんなさい画面」が表示されました。

ある処理に関連する処理を一つのコードにまとめて書くより、特定の処理を専門的に扱うコードの連携として設計した方が、一つ一つのコードの目的が明確になりますし、重複やミス、見落としの確率も下がることを実感しました。何より、作りたい本来の機能に集中できます。Spring のようなフレームワークを使うことの本質を垣間見たような気がします。

0 件のコメント:

コメントを投稿