DeltaSpikeの例外ハンドラを試してみる

DeltaSpike 0.2には例外ハンドラという機能が追加になりました。今回はバージョン0.2でこの例外ハンドラを早速試したいと思います。

FW開発経験者ならば少なくとも例外のハンドリングをどうするかということを考えたことがあると思います。DeltaSpikeではCDIのイベントモデルを利用した例外ハンドラがあります。

では、ユーザ登録を行う以下のようなBeanがあるとします。

import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.constraints.NotNull;

import org.apache.deltaspike.core.api.exception.control.event.ExceptionToCatchEvent;

@Named("userRegister")
@RequestScoped
public class UserRegistration {

    @NotNull
    private String userId;

    @NotNull
    private String password;

    @NotNull
    private String confirmingPassword;

    @Inject
    private Event<ExceptionToCatchEvent> catchEvent;

    public String register() {
        try {
            checkPassword();
            // 登録処理...
            return "endRegister.xhtml";
        } catch (Exception e) {
            catchEvent.fire(new ExceptionToCatchEvent(e));
            return "userRegister.xhtml";
        }
    }

    private void checkPassword() {
        // ユーザIDとパスワードが同じ場合は不正なInvalidPasswordExceptionをスロー
        if (userId.equals(password)) {
            throw new InvalidPasswordException();
        }

        // パスワードと確認用パスワードが等しくない場合はPasswordConfirmExceptionをスロー
        if (!password.equals(confirmingPassword)) {
            throw new PasswordConfirmException();
        }
    }
    ...
}

ユーザID、パスワード、確認用パスワードを受け取ってユーザ登録を行う簡単なBeanクラスです。このBeanではcheckPasswordというパスワード妥当性検証用のメソッドがあり、上記サンプルソースのコメントに合致する条件の場合に例外をスローします。

またBeanのフィールドにはExceptionToCatchEventという型を総称型に持つCDIのイベントクラスがDIされます。

ExceptionToCatchEventはDeltaSpikeで定義されているクラスで、catch節でnewしてイベントのfireメソッドの引数として渡しています。このクラスはDeltaSpikeにおけるイベントモデルによる例外ハンドリングのエントリポイントのようなもののようです。

それでは例外ハンドラを作成します。このサンプルではInvalidPasswordExceptionとPasswordConfirmExceptionという2つの例外をハンドリングするための例外ハンドラを作成しますが、いずれも内容にほとんど違いはありません。

【InvalidPasswordException用の例外ハンドラ】

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.apache.deltaspike.core.api.exception.control.annotation.BeforeHandles;
import org.apache.deltaspike.core.api.exception.control.annotation.ExceptionHandler;
import org.apache.deltaspike.core.api.exception.control.annotation.Handles;
import org.apache.deltaspike.core.api.exception.control.event.ExceptionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ExceptionHandler
public class InvalidPasswordExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(InvalidPasswordExceptionHandler.class);
    
    public void beforeHandle(@BeforeHandles ExceptionEvent<InvalidPasswordException> e) {
        logger.error(e.getException().getClass().getSimpleName() + " was thrown.", e.getException());
    }

    public void handle(@Handles ExceptionEvent<InvalidPasswordException> e) {
        FacesContext context = FacesContext.getCurrentInstance();
        UIComponent component = UIComponent.getCurrentComponent(context);
        FacesMessage message = new FacesMessage();
        message.setDetail("Password and UserId must not be same.");
        message.setSummary("Password and UserId must not be same.");
        message.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(component.getClientId(), message);
        e.handledAndContinue();
    }
}

【PasswordConfirmException用の例外ハンドラ】

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.apache.deltaspike.core.api.exception.control.annotation.BeforeHandles;
import org.apache.deltaspike.core.api.exception.control.annotation.ExceptionHandler;
import org.apache.deltaspike.core.api.exception.control.annotation.Handles;
import org.apache.deltaspike.core.api.exception.control.event.ExceptionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ExceptionHandler
public class PasswordConfirmExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(PasswordConfirmExceptionHandler.class);
    
    public void beforeHandle(@BeforeHandles ExceptionEvent<PasswordConfirmException> e) {
        logger.error(e.getException().getClass().getSimpleName() + " was thrown.", e.getException());
    }
    
    public void handle(@Handles ExceptionEvent<PasswordConfirmException> e) {
        FacesContext context = FacesContext.getCurrentInstance();
        UIComponent component = UIComponent.getCurrentComponent(context);
        FacesMessage message = new FacesMessage();
        message.setDetail("Password and Comfirming password must be same.");
        message.setSummary("Password and Comfirming password must be same.");
        message.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(component.getClientId(), message);
        e.handledAndContinue();
    }
}

2つの例外ハンドラの構造はほぼ同じです。DeltaSpikeで例外ハンドラを作成する際は最低2つのルールを守り必要があります。

1. 例外ハンドラクラスには必ず@ExceptionHandlerアノテーションを付与する。
2. 例外ハンドラメソッドに@HandlesアノテーションとExceptionEvent型のパラメータを設定する。

サンプルでは一歩進んで@BeforeHandlesというアノテーションを定義したメソッドを定義しています。

DeltaSpikeではデプロイ時に@ExceptionHandlerアノテーションが付与されたクラスをロードし、そのクラスのメソッドをリフレクションを用いて取得し、@Handlers、@BeforeHandlesというアノテーションが付与されているメソッドを例外ハンドラメソッドとして登録しています。(かなり簡単に説明しましたがデプロイ時はこんな感じです。省略しすぎ?)

@BeforeHandlesアノテーションが付与された例外ハンドラメソッドが定義されている場合、対象の例外を受け取ると、通常の@Handlesアノテーションが付与された例外ハンドラメソッドより先に、@BeforeHandlesアノテーションが付与されたメソッドが実行され、その後通常の例外ハンドラメソッドが実行されます。

上記サンプルでは、いずれの例外ハンドラも、対応する例外を受け取ると、まずbeforeHandleメソッドが実行され、その後handleメソッドが実行されます。

beforeHandleメソッドの説明は特に不要だと思います。handleメソッドはただ単にJSFのメッセージを作成しているだけです。

ここで、e.handledAndContinue();という1文がありますが、これはまあ、例外ハンドリングを続行するということを明示的に示しており、デフォルトの振る舞いであるため、あえて記述しなくてもいいようです。

実際に動きを確認するためのViewを以下のように定義します。

【userRegister.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:head>
        <title>User registration.</title>
    </h:head>
    <h:body>
        <h:outputText value="Fill in the form to register your information." style="font-style: italic; fontsize: 1.5em" />
        <h:messages />
        <h:form>
            <h:panelGrid columns="2">
                UserId : 
                <h:inputText value="#{userRegister.userId}" />
                Password : 
                <h:inputSecret value="#{userRegister.password}" />
                Password(confirm) : 
                <h:inputSecret value="#{userRegister.confirmingPassword}" />
            </h:panelGrid>
            <br />
            <h:commandButton value="Register" action="#{userRegister.register}" />
        </h:form>
    </h:body>
</html>

先のUserRegistrationクラスで処理成功時に遷移する登録終了用のView(endRegister.xhtml)は何でもいいのでここでは省略します。

APサーバにプログラムをデプロイして、登録画面からまずはユーザIDとパスワードに同じ文字列を設定して実行すると、画面には"Password and UserId must not be same."というメッセージが表示され、ログにはInvalidPasswordExceptionのスタックトレースが出力されていると思います。
パスワードと確認用のパスワードに異なる文字列を設定して実行すると、今度は"Password and Comfirming password must be same."というメッセージが画面に出力され、同じくログにスタックトレースが出力されていると思います。

例外ハンドラで定義した通りの結果ですね。

ここまでは、ただの例外処理ですが、ネストした例外はどうなるでしょうか。DeltaSpikeではネストした例外もその順序で例外ハンドリングを行うことが可能です。

だらだら解説するよりコードで見た方が早いのでUserRegistrationのcheckPasswordメソッドを次のように修正します。

    private void checkPassword() {
        try {
            // ユーザIDとパスワードが同じ場合は不正なInvalidPasswordExceptionをスロー
            if (userId.equals(password)) {
                throw new InvalidPasswordException();
            }
    
            // パスワードと確認用パスワードが等しくない場合はPasswordConfirmExceptionをスロー
            if (!password.equals(confirmingPassword)) {
                throw new PasswordConfirmException();
            }
        } catch (Exception e) {
            throw new RegistrationException(e);
        }
    }

RegistrationExceptionという例外でInvalidPasswordExceptionとPasswordConfirmExceptionをラップしています。この場合例外の直接原因となっているのはRegistrationExceptionではなく、ラップされたInvalidPasswordExceptionかPasswordConfirmExceptionです。DeltaSpikeではこの種の例外ハンドリングを実にうまく行ってくれます。

例えば、RegistrationExceptionに対応した例外ハンドラがあったとして、その呼び出し順は、InvalidPasswordException→RegistrationExceptionとなります。

実際にRegistrationExceptionを処理するハンドラを以下のように定義します。

【RegistrationException用の例外ハンドラ】

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.apache.deltaspike.core.api.exception.control.annotation.BeforeHandles;
import org.apache.deltaspike.core.api.exception.control.annotation.ExceptionHandler;
import org.apache.deltaspike.core.api.exception.control.annotation.Handles;
import org.apache.deltaspike.core.api.exception.control.event.ExceptionEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ExceptionHandler
public class RegistrationExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(RegistrationExceptionHandler.class);
    
    public void beforeHandle(@BeforeHandles ExceptionEvent<RegistrationException> e) {
        logger.error(e.getException().getClass().getSimpleName() + " was thrown.", e.getException());
    }
    
    public void handle(@Handles ExceptionEvent<RegistrationException> e) {
        FacesContext context = FacesContext.getCurrentInstance();
        UIComponent component = UIComponent.getCurrentComponent(context);
        FacesMessage message = new FacesMessage();
        message.setDetail("User registration was failed.");
        message.setSummary("User registration was failed.");
        message.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(component.getClientId(), message);
        e.handledAndContinue();
    }
}

再デプロイし、ユーザIDとパスワードに同じ文字列を設定して実行すると、まず、InvalidPasswordExceptionの例外ハンドリングが行われ、次に、RegistrationExceptionの例外ハンドリングが行われていることがログ上からわかると思います。

また、@Handlesアノテーションにはordinalという属性を設定することが可能なようですが、ドキュメントを参照すると、ようは同じ型の例外を処理する例外ハンドラメソッドが存在した場合にその呼び出し順を制御することが可能で、例えばordinal=100とordinal=10のメソッドがあった場合100のメソッドが先に実行されるということらしいです。デフォルトは0のようです。

この辺は、まだ試してないのですが、多分試さないと思います。

例外ハンドラの解説において、細かい実装ルールなど、省略した部分も多いので、興味のある方はドキュメントを参照してください。