はじめに

こんにちは。デザイン&エンジニアリング部の西川 武です。

Nablarchフレームワークには、個別の業務機能を実行する前後の共通処理を担う部品として、ハンドラが存在します。
フレームワークで予め用意されているハンドラも多くありますが、システム固有の要件がある場合は、ハンドラを自作して組み込むことも可能です。

そこで本記事では、主に初めてNablarchのハンドラを自作するという方に向けて、実際の現場での開発事例をご紹介し、そこで必要になった思考プロセスについて重点的にお伝えします。

前提知識

本記事では以下の資料にある知識を前提として解説します。
そもそもの「ハンドラ」の用語の意味や役割、また基本的な実装方法の解説についてはこちらに譲りますので、必要に応じてご参照ください。
Nablarch解説書 | アーキテクチャ | ハンドラキュー(handler queue)
Nablarchトレーニングコンテンツ | 設定・機能拡張編

対象バージョン

本記事で取り扱うソフトウェアについては、以下のバージョンを前提とします。

ソフトウェア バージョン
Nablarch 6u2
Java 21.0

実装イメージの組み立て方

ここでは、自作するハンドラが実現すべき機能(以下、「要件」と呼ぶ)が明確に決まっているというシチュエーションを仮定し、
その要件を基に実装イメージを組み立てるための思考プロセスを解説します。

処理の切り分け

ハンドラの処理は往路処理、復路処理、例外処理の3種類の処理に分かれます。
実際にハンドラを自作する際には、まずハンドラの要件が3種類のどの処理に該当するかを切り分けることが最も重要です。

切り分ける際には、要件を実現するための処理をどのタイミングで実行すべきかを基準にします。
具体的な処理の種類ごとの実行タイミングは以下の通りです。

処理の種類 実行タイミング
往路処理 業務機能の実行前
復路処理 業務機能の実行後
例外処理 業務機能または後続ハンドラでの例外発生時

切り分けの例

以下に、要件とその切り分けの簡易な例を3つ記載します。

  • 特定のIPアドレスの端末からのみ業務機能を使用できるようにする。
    →業務機能の実行前にIPアドレスのチェック処理を行うため、往路処理である。
  • HTTPレスポンスのヘッダに特定の項目を設定する。
    →業務機能の実行結果に対する加工を行うため、復路処理である。
  • 業務機能で特定の例外が発生した場合にログ出力を行う。
    →例外発生時に処理を行うため、例外処理である。

プログラム構造への落とし込み

ハンドラのプログラム構造は以下の通りです。
切り分けた処理の種類に応じて以下の通りに記述場所が決定し、プログラムの全体像が明確になります。

public class SampleHandler implements Handler<Object, Object> {

    @Override
    public Object handle(Object input, ExecutionContext context) {
        try {

            // ここに往路処理を記述

            Object result = context.handleNext(input);

            // ここに復路処理を記述

            return result;

        // 捕捉対象の例外・エラーを指定
        } catch (Exception e) {
            // ここに例外処理を記述
        }
    }
}

context.handleNextが後続ハンドラの呼び出しを行うメソッドで、後続ハンドラが無い場合は業務機能の実行を行います。
よって、context.handleNextより前の処理が往路処理、後の処理が復路処理です。
例外処理はtry-catch文のcatch句に記述します。

実際に作ってみる

ここからは、私がウェブアプリケーションの開発現場で実際に自作した「ログインチェックハンドラ」を題材にして、要件を基に実装イメージを組み立て、実際のプログラムを完成させるまでの流れを実演します。

なお、実装にあたっては以下のサンプルコードを参考にしました。
nablarch-example-web LoginUserPrincipalCheckHandler.java

背景

本題に入る前に、そもそも今回の現場でハンドラを自作することになった背景に軽く触れます。
今回のシステムには以下のセキュリティ要件がありました。

ユーザがログイン操作をせずに、個別の業務機能へ直接アクセスすることを禁止する。

この要件はすべての業務機能において実現したいですが、同じ処理をすべての業務機能に記述するのは非常に保守性が低いです。
そこで、要件を共通処理として実現するための手段としてハンドラを選択しました。

ハンドラ要件

ハンドラに求められる要件は以下の通りです。

リクエストを送信したユーザのログイン状態をチェックし、未ログインの場合には業務機能へのアクセスを禁止してログイン画面に遷移させる。

実装イメージの組み立て

それでは、ハンドラ要件を具体的な処理に切り分けます。
処理の実行タイミングを考えましょう。
「未ログインの場合には業務機能へのアクセスを禁止して」とあることから、業務機能の実行前にログイン状態のチェックを実施する必要がありそうです。
したがって、この要件は往路処理に切り分けることができます。

これ以外の要件は無いので、本ハンドラは往路処理のみを実装すればよいことがわかります。
つまり、実装イメージは以下の通りとなります。

public class LoginCheckHandler implements Handler<HttpRequest, Object> {

    public Object handle(HttpRequest request, ExecutionContext context) {

        // ここにチェック処理を記述

        return context.handleNext(request);
    }
}

例外処理が無いのでtry-catch文は不要で、復路処理が無いのでhandleNextの戻り値はそのままreturnすれば大丈夫です。

実装

ハンドラとしてのプログラムの全体像までわかったので、あとはチェック処理のロジックを記述するだけです。
import文やプロパティなども含めた完成形は以下の通りとなります。

package com.nablarch.example.app.web.handler;

import nablarch.common.web.session.SessionUtil;
import nablarch.fw.ExecutionContext;
import nablarch.fw.Handler;
import nablarch.fw.web.HttpErrorResponse;
import nablarch.fw.web.HttpRequest;

import java.util.Objects;

public class LoginCheckHandler implements Handler<HttpRequest, Object> {

	/**
	 * ログイン画面の遷移先URL
	 */
	private String loginPageUrl;

	// ...(GetterとSetterは省略)...

	/**
	 * セッションからユーザ情報を取得するためのキー名
	 */
	private static final String USER_INFO_KEY = "user.id";

	@Override
	public Object handle(HttpRequest request, ExecutionContext context) {
		if (Objects.isNull(SessionUtil.orNull(context, USER_INFO_KEY))) {
			throw new HttpErrorResponse(loginPageUrl);
		}
		return context.handleNext(request);
	}
}

本記事の趣旨から逸れるので軽めにしますが、ロジックの解説もしておきます。

if文の条件式で、リクエストを送信したユーザのユーザIDがセッションストアに存在するかどうかを判定しています。
今回のシステムは、ログイン処理時にユーザのユーザIDをセッションストアに格納し、それが格納されていることをもって「ログイン済み」状態とみなす方式です。
よって、上記のユーザIDの存在判定がログインチェックに相当します。

判定結果は、ユーザIDがセッションストアに存在しない場合にtrueとします。
trueの場合はエラー扱いとしてログイン画面に遷移させます。

ログイン画面の遷移先URLはプロパティに切り出しており、このプロパティ値の定義については次項で解説します。

コンポーネント定義への切り出し

ハンドラのプロパティを頻繁に変更することが想定される場合、プロパティを外部のコンポーネント定義ファイルに切り出すことで、保守性を高められます。

今回はウェブアプリケーションなので、web-component-configuration.xmlがコンポーネント定義ファイルに該当します。
以下の通り、loginPageUrlの値にログイン画面の遷移先URLを指定します。

<component name="loginCheckHandler" class="com.nablarch.example.app.web.handler.LoginCheckHandler">
	<property name="loginPageUrl" value="redirect:///action/login"/>
</component>

ハンドラキューへの挿入

最後に、実装したハンドラを動作させるためにハンドラキューへ挿入します。
ここでは、ハンドラの適切な前後関係を考慮して挿入場所を決定する必要があります。

考えるポイントは、自作したハンドラと他のハンドラの処理結果が互いに影響を及ぼす(一方が他方の処理結果をインプットにする)かどうかです。

ハンドラキューの設定ファイルは先ほどと同じくweb-component-configuration.xmlです。
解説をわかりやすくするために一部省略しますが、挿入前の状態は以下の通りでした。

<component name="webFrontController" class="nablarch.fw.web.servlet.WebFrontController">
	<property name="handlerQueue">
		<list>
			<!-- 中略 -->
			
			<!-- HTTPレスポンスハンドラ -->
			<component-ref name="httpResponseHandler"/>
			
			<!-- 中略 -->
			
			<!-- セッション変数保存ハンドラ -->
			<component-ref name="sessionStoreHandler"/>
			
			<!-- データベース接続管理ハンドラ -->
			<component-ref name="dbConnectionManagementHandler"/>
			
			<!-- トランザクション制御ハンドラ -->
			<component-ref name="transactionManagementHandler"/>
			
			<!-- 中略 -->
		</list>
	</property>
</component>

この4つのハンドラが予め挿入されていたものとして、挿入場所を考えてみましょう。
各ハンドラの詳細な役割や仕様については、それぞれのNablarch解説書をご参照ください。

今回自作したハンドラでは、遷移先URLを指定したHttpErrorResponseを送出しますが、これは復路において「HTTPレスポンスハンドラ」に受け取らせることで実際のクライアントへのレスポンスに変換されます。
よって、「HTTPレスポンスハンドラ」よりは後ろに挿入すべきということがわかります。

さらにセッションストアからのデータ取得も行っていますが、これは実際には、往路において「セッション変数保存ハンドラ」によってロードされたセッション変数を取得することを意味します。
よって、「セッション変数保存ハンドラ」よりも後ろに挿入すべきということがわかります。

以降のハンドラに対しては影響が無いため、「セッション変数保存ハンドラ」より後ろであればどこに挿入しても問題ありません。
せっかくなので、ハンドラキューが全体として効率的に動作する最適な挿入場所を考えてみましょう。

往路において、「データベース接続管理ハンドラ」と「トランザクション制御ハンドラ」はそれぞれ、業務機能の処理のためにデータベース接続を取得し、トランザクションを開始します。
これらの処理は、ユーザが「ログイン済み」であることの確認がとれてから行ったほうが効率的です。
なぜならば、データベース接続取得とトランザクション開始をしてからログインチェックを実施したとすると、ログインエラー発生時にはデータベース接続もトランザクションも無駄になってしまうからです。
よって、「データベース接続管理ハンドラ」と「トランザクション制御ハンドラ」よりは前に挿入すべきということがわかります。

以上をふまえて、今回自作したハンドラの最適な挿入場所は「セッション変数保存ハンドラ」の直後と決定できます。
以下の通りに挿入します。

<component name="webFrontController" class="nablarch.fw.web.servlet.WebFrontController">
	<property name="handlerQueue">
		<list>
			<!-- 中略 -->
			
			<!-- HTTPレスポンスハンドラ -->
			<component-ref name="httpResponseHandler"/>
			
			<!-- 中略 -->
			
			<!-- セッション変数保存ハンドラ -->
			<component-ref name="sessionStoreHandler"/>
			
			<!-- ここに挿入 -->
			<component-ref name="loginCheckHandler"/>
			
			<!-- データベース接続管理ハンドラ -->
			<component-ref name="dbConnectionManagementHandler"/>
			
			<!-- トランザクション制御ハンドラ -->
			<component-ref name="transactionManagementHandler"/>
			
			<!-- 中略 -->
		</list>
	</property>
</component>

まとめ

Nablarchのハンドラの開発事例をご紹介し、そこで必要になった思考プロセスについて重点的にお伝えしました。
本記事が、Nablarchで開発を行う方々の一助となれば幸いです。