Macchinetta Batch Framework (2.x) Development Guideline - version 2.5.0.RELEASE, 2024-3-28
> INDEX

Overview

本節では、ジョブの入力データに対する妥当性のチェック(以降、入力チェックと呼ぶ)について説明する。

本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。

一般的に、バッチ処理における入力チェックは、他システム等から受領したデータに対して、 自システムにおいて妥当であることを確認するために実施する事が多い。
反対に、自システム内の信頼できるデータ(たとえば、データベースに格納されたデータ)に対して、 入力チェックを実施することは不要と言える。

入力チェックはMacchinetta Server 1.xの内容と重複するため、Macchinetta Server 1.x 開発ガイドラインの 入力チェックも合わせて参照。 以下に、主な比較について示す。

表 1. 主な比較一覧
比較対象 Macchinetta Server 1.x Macchinetta Batch 2.x

使用できる入力チェックルール

Macchinetta Server 1.xと同様

ルールを付与する対象

フォームクラス

DTO

チェックの実行方法

Controllerに@Validatedアノテーションを付与する

ValidatorクラスのAPIをコールする

エラーメッセージの設定

Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義と概ね同様だが、 Macchinetta Server 1.xではメッセージキーにフォーム名を含められるのに対し、Macchinetta Batch 2.xではDTO名をメッセージキーに含めることができない。 この差異はチェックの実行方法の違いによるものである。

エラーメッセージの出力先

画面

ログ等

なお、本節で説明対象とする入力チェックは、主にステップが処理する入力データを対象とする。
ジョブパラメータのチェックについてはパラメータの妥当性検証を参照。

入力チェックの分類

入力チェックは、単項目チェック、相関項目チェックに分類される。

表 2. 設定内容の項目一覧
種類 説明 実現方法

単項目チェック

単一のフィールドで完結するチェック

入力必須チェック
桁チェック
型チェック

Bean Validation(実装ライブラリとしてHibernate Validatorを使用)

相関項目チェック

複数のフィールドを比較するチェック

数値の大小比較
日付の前後比較

org.springframework.validation.Validatorインタフェースを実装したValidationクラス
または Bean Validation

Springは、Java標準であるBean Validationをサポートしている。 単項目チェックには、このBean Validationを利用する。 相関項目チェックの場合は、Bean ValidationまたはSpringが提供しているorg.springframework.validation.Validatorインタフェースを利用する。

この点は、 Macchinetta Server 1.x 開発ガイドラインの 入力チェックの分類 と同様である。

入力チェックの全体像

チャンクモデル、タスクレットモデルにて入力チェックを行うタイミングは以下のとおりである。

  • チャンクモデルの場合はItemProcessorで行う。

  • タスクレットモデルの場合はTasklet#execute()にて、任意のタイミングで行う。

チャンクモデル、タスクレットモデルにおいて入力チェックの実装方法は同様となるため、 ここではチャンクモデルのItemProcessorで入力チェックを行う場合について説明する。

まず、入力チェックの全体像を説明する。入力チェックに関連するクラスの関係は以下のとおりである。

InputValidation architecture
図 1. 入力チェックの関連クラス
  • ItemProcessorに、org.springframework.batch.item.validator.Validatorの実装である org.springframework.batch.item.validator.SpringValidatorをインジェクションしvalidateメソッドを実行する。

    • SpringValidatorは内部にorg.springframework.validation.Validatorを保持し、validateメソッドを実行する。
      いわば、org.springframework.validation.Validatorのラッパーといえる。
      org.springframework.validation.Validatorの実装は、 org.springframework.validation.beanvalidation.LocalValidatorFactoryBeanとなる。 このクラスを通じてHibernate Validatorを使用する。

  • 何件目のデータで入力チェックエラーになったのかを判別するためにorg.springframework.batch.item.ItemCountAwareを入力DTOに実装する。

データ件数の設定

ItemCountAware#setItemCountAbstractItemCountingItemStreamItemReaderによって設定される。 よって、タスクレットモデルでItemReaderを使わない場合、更新されない。 この場合は何件目のデータでエラーになったかはユーザにて設定すること。

jakarta.validation.Validatorやorg.springframework.validation.Validatorといったバリデータは直接使用しない。

jakarta.validation.Validatororg.springframework.validation.Validatorといったバリデータは直接使用せず、 org.springframework.batch.item.validator.SpringValidatorを使用する。

SpringValidatororg.springframework.validation.Validatorのラッパーである。
SpringValidatorは発生した例外をBindExceptionにラップし、ValidationExceptionとしてスローする。
そのため、ValidationExceptionを通してBindExceptionにアクセスでき、柔軟なハンドリングがしやすくなる。

一方、jakarta.validation.Validatororg.springframework.validation.Validatorといったバリデータを直接使用すると、バリデーションエラーになった情報を処理する際に煩雑なロジックになってしまう。

org.springframework.batch.item.validator.ValidatingItemProcessorは使用しない

org.springframework.validation.Validatorによる入力チェックは、 Spring Batchが提供するValidatingItemProcessorを使用しても実現可能である。

しかし、以下の理由により状況によっては拡張を必要としてしまうため、 実装方法を統一する観点より使用しないこととする。

  • 入力チェックエラーをハンドリングし処理を継続することができない。

  • 入力チェックエラーとなったデータに対して柔軟な対応を行うことができない。

    • 入力チェックエラーとなったデータに対しての処理は、利用者によって多種多様(ログ出力のみ、エラーデータを別ファイルに退避する、など)となると想定される。

How to use

先にも述べたが、入力チェックの実現方法は以下のとおりMacchinetta Server 1.xと同様である。

  • 単項目チェックは、Bean Validationを利用する。

  • 相関項目チェックは、Bean ValidationまたはSpringが提供しているorg.springframework.validation.Validatorインタフェースを利用する。

入力チェックの方法について以下の順序で説明する。

各種設定

入力チェックにはHibernate Validatorを使用する。 ライブラリの依存関係にHibernate Validatorの定義があり、必要なBean定義が存在することを確認する。 これらは、Macchinetta Batch 2.xが提供するブランクプロジェクトではすでに設定済である。

依存ライブラリの設定例
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>
jp.co.ntt.fw.macchinetta.batch.functionaltest.config.LaunchContextConfig.java
@Bean
public SpringValidator<?> validator(Validator beanValidator) {
    final SpringValidator<?> springValidator = new SpringValidator<>();
    springValidator.setValidator(beanValidator);
    return springValidator;
}

@Bean
public Validator beanValidator() {
    try (LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean()) {
        localValidatorFactoryBean.afterPropertiesSet();
        return localValidatorFactoryBean;
    }
}
META-INF/spring/launch-context.xml
<bean id="validator" class="org.springframework.batch.item.validator.SpringValidator"
      p:validator-ref="beanValidator"/>

<bean id="beanValidator"
      class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
エラーメッセージの設定

先にも述べたが、エラーメッセージの設定については、 Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義を参照。

入力チェックルールの定義

入力チェックのルールを実装する対象はItemReaderを通じて取得するDTOである。 ItemReaderを通じて取得するDTOは以下の要領で実装する。

  • 何件目のデータで入力チェックエラーになったのかを判別するために、org.springframework.batch.item.ItemCountAwareを実装する。

    • setItemCountメソッドにて引数で受けた現在処理中のitemが読み込み何件目であるかをあらわす数値をクラスフィールドに保持する。

  • 入力チェックルールを定義する。

以下に、入力チェックルールを定義したDTOの例を示す。

入力チェックルールを定義したDTOの例
public class VerificationSalesPlanDetail implements ItemCountAware {  // (1)

    private int count;

    @NotEmpty
    @Size(min = 1, max = 6)
    private String branchId;

    @NotNull
    @Min(1)
    @Max(9999)
    private int year;

    @NotNull
    @Min(1)
    @Max(12)
    private int month;

    @NotEmpty
    @Size(min = 1, max = 10)
    private String customerId;

    @NotNull
    @DecimalMin("0")
    @DecimalMax("9999999999")
    private BigDecimal amount;

    @Override
    public void setItemCount(int count) {
        this.count = count;  // (2)
    }

    // omitted getter/setter
}
表 3. 設定内容の項目一覧
項番 説明

(1)

ItemCountAwareクラスを実装し、setItemCountメソッドをオーバーライドする。
ItemCountAware#setItemCount()は、ItemReaderが読み込んだデータが何件目であるかを引数に渡される。

(2)

引数で受けるcountをクラスフィールドに保持する。
この値は、何件目のデータで入力チェックエラーになったのかを判別するため使用する。

入力チェックの実施

入力チェックの実施方法について説明する。 入力チェック実施は以下の要領で実装する。

  • ItemProcessorの実装にて、org.springframework.batch.item.validator.Validator#validate()を実行する。

    • ValidatorにはSpringValidatorのインスタンスをインジェクトして使用する。

  • 入力チェックエラーをハンドリングする。詳細は入力チェックエラーのハンドリングを参照。

入力チェックの実施例を以下に示す。

入力チェックを実施する例
@Component
public class ValidateAndContinueItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
    @Inject  // (1)
    Validator<VerificationSalesPlanDetail> validator;

    @Override
    public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
        try {  // (2)
            validator.validate(item);  // (3)
        } catch (ValidationException e) {
          // omitted exception handling
        }

        SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
        // omitted business logic

        return salesPlanDetail;
    }
}
表 4. 設定内容の項目一覧
項番 説明

(1)

SpringValidatorのインスタンスをインジェクトする。
org.springframework.batch.item.validator.Validatorの型引数には、ItemReaderを通じて取得するDTOを設定する。

(2)

入力チェックエラーをハンドリングする。
例では例外をtry/catchで捕捉する方法で処理している。
詳細は入力チェックエラーのハンドリングを参照。

(3)

ItemReaderを通じて取得するDTOを引数としてValidator#validate()を実行する。

入力チェックエラーのハンドリング

入力チェックエラーが発生した場合の選択肢は以下の2択となる。

  1. 入力チェックエラーが発生した時点で処理を打ち切り、ジョブを異常終了させる。

  2. 入力チェックエラーが発生したことをログ等に残し、後続データの処理は継続する。その後、ジョブ終了時に、ジョブを警告終了させる。

処理を異常終了する場合

例外発生時に処理を異常終了するためには、java.lang.RuntimeExceptionまたはそのサブクラスをスローする。

例外発生時にログ出力等の処理を行う方法は以下の2とおりがある。

  1. 例外をtry/catchで捕捉し、例外をスローする前に行う。

  2. 例外をtry/catchで捕捉せず、ItemProcessListenerを実装しonProcessErrorメソッドにて行う。

    • ItemProcessListener#onProcessError()@OnProcessErrorアノテーションを使用して実装してもよい。 詳細は、リスナーを参照。

例外発生時に、例外情報をログ出力し、処理を異常終了する例を以下に示す。

try/catchによるハンドリング例
@Component
public class ValidateAndAbortByTryCatchItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
    /**
     * Logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ValidateAndAbortByTryCatchItemProcessor.class);

    @Inject
    Validator<VerificationSalesPlanDetail> validator;

    @Override
    public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
        try {  // (1)
            validator.validate(item);  // (2)
            return convert(item);
        } catch (ValidationException e) {
            // (3)
            logger.error("Exception occurred in input validation at the {} th item. [message:{}]",
                    item.getCount(), e.getMessage());
            throw e;  // (4)
        }
    }
    private SalesPlanDetail convert(VerificationSalesPlanDetail item) {

        SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
        // omitted business logic

        return salesPlanDetail;
    }
}
表 5. 設定内容の項目一覧
項番 説明

(1)

try/catchにて例外を捕捉する。

(2)

入力チェックを実行する。

(3)

例外をスローする前にログ出力処理を行う。

(4)

例外をスローする。
org.springframework.batch.item.validator.ValidationExceptionRuntimeExceptionのサブクラスであるため、 そのままスローしなおしてよい。

ItemProcessListener#OnProcessErrorによるハンドリング例
@Component
public class ValidateAndAbortItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {

    /**
     * Logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ValidateAndAbortItemProcessor.class);

    @Inject
    Validator<VerificationSalesPlanDetail> validator;

    @Override
    public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
        validator.validate(item);  // (1)

        SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
        // omitted business logic

        return salesPlanDetail;
    }

    @OnProcessError  // (2)
    void onProcessError(VerificationSalesPlanDetail item, Exception e) {
        // (3)
        logger.error("Exception occurred in input validation at the {} th item. [message:{}]", item.getCount() ,e.getMessage());
    }
}
表 6. 設定内容の項目一覧
項番 説明

(1)

入力チェックを実行する。

(2)

ItemProcessListener#onProcessError()@OnProcessErrorアノテーションを使用して実装する。

(3)

例外をスローする前にログ出力処理を行う。

ItemProcessListener#onProcessError()使用時の注意点

onProcessErrorメソッドの利用は業務処理と例外ハンドリングを切り離すことができるためソースコードの可読性、保守性の向上等に有用である。
しかし、上記の例でハンドリング処理を行っているValidationException以外の例外が発生した場合も同じメソッドが実行されるため注意が必要である。

ItemProcessor#process()におけるログ出力を例外によって出力し分ける場合は、 onProcessErrorメソッドにて発生した例外の種類を判定して例外処理を行う必要がある。 これが煩雑である場合は、try/catchによるハンドリングにて入力チェックエラーのみを処理し、それ以外はリスナーに移譲するように責務を分担するとよい。

エラーレコードをスキップする場合

入力チェックエラーが発生したレコードの情報をログ出力等を行った後、エラーが発生したレコードをスキップして後続データの処理を継続する場合は以下の要領で実装する。

  • 例外をtry/catchで捕捉する。

  • 例外発生時のログ出力等を行う。

  • ItemProcessor#process()の返り値としてnullを返却する。

    • nullを返却することで入力チェックエラーが発生したレコードは後続の処理対象(ItemWriterによる出力)に含まれなくなる。

ItemProcessorによるスキップ例
@Component
public class ValidateAndContinueItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
    /**
     * Logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ValidateAndContinueItemProcessor.class);

    @Inject
    Validator<VerificationSalesPlanDetail> validator;

    @Override
    public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
        try {  // (1)
            validator.validate(item);  // (2)
        } catch (ValidationException e) {
            // (3)
            logger.warn("Skipping item because exception occurred in input validation at the {} th item. [message:{}]",
                    item.getCount(), e.getMessage());
            // (4)
            return null;  // skipping item
        }

        SalesPlanDetail salesPlanDetail = new SalesPlanDetail();
        // omitted business logic

        return salesPlanDetail;
    }
}
表 7. 設定内容の項目一覧
項番 説明

(1)

try/catchにて例外を捕捉する。

(2)

入力チェックを実行する。

(3)

nullを返却する前にログ出力処理を行う。

(4)

nullを返却することで当該データをスキップし次のデータ処理へ移る。

終了コードの設定

入力チェックエラーが発生した場合、入力チェックエラーが発生しなかった場合とジョブの状態を区別するために必ず正常終了ではない終了コードを設定すること。
入力チェックエラーが発生したデータをスキップした場合、異常終了した場合においても終了コードの設定は必須である。

終了コードの設定方法については、ジョブの管理を参照。

エラーメッセージの出力

入力チェックエラーが発生した場合にMessageSourceを使用することで、任意のエラーメッセージを出力することができる。 エラーメッセージの設定については、Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義を参照。 エラーメッセージを出力する場合は以下の要領で実装する。

エラーメッセージを出力する方法としては、以下の2とおりがある。

  1. レコード内の各項目についてエラーメッセージを出力

  2. エラーメッセージをまとめて出力

レコード内の各項目についてエラーメッセージを出力する場合の要領と実装例を以下に示す。

  • 入力チェックでエラーが発生した項目に対して、MessageSourceを用いてエラーメッセージのログ出力を行う。

MessageSourceによるエラーメッセージ出力例
@Component
public class ValidateAndMessageItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {
    /**
     * Logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ValidateAndMessageItemProcessor.class);

    @Inject
    Validator<VerificationSalesPlanDetail> validator;

    @Inject
    MessageSource messageSource;  // (1)

    @Override
    public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
        try {  // (2)
            validator.validate(item);  // (3)
        } catch (ValidationException e) {
            // (4)
            BindException errors = (BindException) e.getCause();

            // (5)
            for (FieldError fieldError : errors.getFieldErrors()) {
                // (6)
                logger.warn(messageSource.getMessage(fieldError, null) +
                                "Skipping item because exception occurred in input validation at the {} th item. [message:{}]",
                                    item.getCount(), e.getMessage());
            // (7)
            return null;  // skipping item
        }

        return convert(item);
    }
}
表 8. 設定内容の項目一覧
項番 説明

(1)

ResourceBundleMessageSourceのインスタンスをインジェクトする。
MassageSorceのBean定義はメッセージ管理を参照。

(2)

try/catchにて例外を捕捉する。

(3)

入力チェックを実行する。

(4)

getCause()org.springframework.validation.BindExceptionを取得する。

(5)

getFieldErrors()で1件分のFiledErrorを取得する。

(6)

取得したFieldErrorを引数にして、messageSourceでエラーメッセージの出力処理を行う。
1レコード内に3項目のエラーがある場合、3件のエラーメッセージを繰り返し出力する。

(7)

nullを返却することで当該データをスキップし次のデータ処理へ移る。

エラーメッセージをまとめて出力する場合の要領と実装例を以下に示す。

  • StepExecutionContextを利用し、入力チェックでエラーが発生した項目のエラーメッセージをリストに格納しておく。

  • AfterStepStepExecutionContextからリストを取得し、まとめてエラーメッセージのログ出力を行う。

StepExecutionContextを利用したエラーメッセージの一括出力例
@Component
@Scope("step")  // (1)
public class ValidateAndBulkMessageItemProcessor implements ItemProcessor<VerificationSalesPlanDetail, SalesPlanDetail> {

    /**
     * Logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ValidateAndBulkMessageItemProcessor.class);

    private StepExecution stepExecution;  // (2)

    @Inject
    Validator<VerificationSalesPlanDetail> validator;

    @Inject
    MessageSource messageSource;

    @BeforeStep  // (3)
    public void beforeStep(StepExecution stepExecution) {
        this.stepExecution = stepExecution;  // (4)
    }

    @Override
    public SalesPlanDetail process(VerificationSalesPlanDetail item) throws Exception {
        try {
            validator.validate(item);
        } catch (ValidationException e) {

            BindException errors = (BindException) e.getCause();

            List<String> errorMessageList = new ArrayList<>();  // (5)
            // (6)
            if (stepExecution.getExecutionContext().containsKey("errorMessageList")) {
                errorMessageList = (List<String>) stepExecution.getExecutionContext().get("errorMessageList");
            }

            // (7)
            for (FieldError fieldError : errors.getFieldErrors()) {
                String itemNumber = item.getCount() + " th item";
                String errorMessage = messageSource.getMessage(fieldError, null);
                String detailErrorMessage = e.getMessage();

                String message = MessageFormat
                        .format("{0} Skipping item because exception occurred in input validation at the {1}. [message:{2}]",
                                errorMessage, itemNumber, detailErrorMessage);

                errorMessageList.add(message);
            }

            stepExecution.getExecutionContext().put("errorMessageList", errorMessageList);  // (8)

            return null; // skipping item
        }

        return convert(item);
    }

    @AfterStep  // (9)
    public void afterStep(StepExecution stepExecution) {
        ExecutionContext executionContext = stepExecution.getExecutionContext();  // (10)

        List<String> errorMessageList = (List<String>) executionContext.get("errorMessageList");  // (11)
        //  (12)
        for (String errorMessage : errorMessageList) {
            logger.warn(errorMessage);
        }
    }
}
表 9. 設定内容の項目一覧
項番 説明

(1)

クラスに@Scopeアノテーションを付与してスコープを指定する。
スコープは本クラス内で利用するStepExecution に合わせてstepとする。

(2)

StepExecutionを保持するためのフィールドを定義する。

(3)

beforeStepメソッドを実装し、@BeforeStepアノテーションを付与する。
シグネチャはvoid beforeStep(StepExecution stepExecution)とする。
StepExecutionListenerクラスを実装し、beforeStepメソッドをオーバーライドする方法でもよい。

(4)

StepExecutionを取得してクラスフィールドに保持する。

(5)

エラーメッセージを格納するためのリストを定義する。

(6)

StepExecutionからstepExecutionContextを取得し、その中にerrorMessageListというキーが存在するかチェックする。
stepExecutionContextからerrorMessageListをキーとする値を取得し、(5)で定義したリストに代入する。
その後、stepExecutionContextから削除する。

(7)

getFieldErrors()で1レコード中で発生したエラーを取得する。
ログ出力用エラーメッセージを生成し、errorMessageListに追加する。

(8)

StepExecutionからstepExecutionContextを取得し、errorMessageListというキーを指定してstepExecutionContextにエラーメッセージを格納したリストを登録する。

(9)

afterStepメソッドを実装し、@AfterStepアノテーションを付与する。
シグネチャはvoid afterStep(StepExecution stepExecution)とする。
StepExecutionListenerクラスを実装し、afterStepメソッドをオーバーライドする方法でもよい。

(10)

StepExecutionからstepExecutionContextを取得する。

(11)

errorMessageListというキーを指定してstepExecutionContextからエラーメッセージを格納したリストを取得する。

(12)

エラーメッセージを繰り返しログ出力する。

Macchinetta Batch Framework (2.x) Development Guideline - version 2.5.0.RELEASE, 2024-3-28