Overview
本節では、ジョブの入力データに対する妥当性のチェック(以降、入力チェックと呼ぶ)について説明する。
本機能は、チャンクモデルとタスクレットモデルとで同じ使い方になる。
一般的に、バッチ処理における入力チェックは、他システム等から受領したデータに対して、
自システムにおいて妥当であることを確認するために実施する事が多い。
反対に、自システム内の信頼できるデータ(たとえば、データベースに格納されたデータ)に対して、
入力チェックを実施することは不要と言える。
入力チェックはMacchinetta Server 1.xの内容と重複するため、Macchinetta Server 1.x 開発ガイドラインの 入力チェックも合わせて参照。 以下に、主な比較について示す。
| 比較対象 | Macchinetta Server 1.x | Macchinetta Batch 2.x | 
|---|---|---|
使用できる入力チェックルール  | 
Macchinetta Server 1.xと同様  | 
|
ルールを付与する対象  | 
  | 
  | 
チェックの実行方法  | 
  | 
  | 
エラーメッセージの設定  | 
Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義と概ね同様だが、 Macchinetta Server 1.xではメッセージキーにフォーム名を含められるのに対し、Macchinetta Batch 2.xではDTO名をメッセージキーに含めることができない。 この差異はチェックの実行方法の違いによるものである。  | 
|
エラーメッセージの出力先  | 
画面  | 
ログ等  | 
なお、本節で説明対象とする入力チェックは、主にステップが処理する入力データを対象とする。
ジョブパラメータのチェックについてはパラメータの妥当性検証を参照。
入力チェックの分類
入力チェックは、単項目チェック、相関項目チェックに分類される。
| 種類 | 説明 | 例 | 実現方法 | 
|---|---|---|---|
単項目チェック  | 
単一のフィールドで完結するチェック  | 
入力必須チェック  | 
Bean Validation(実装ライブラリとしてHibernate Validatorを使用)  | 
相関項目チェック  | 
複数のフィールドを比較するチェック  | 
数値の大小比較  | 
  | 
Springは、Java標準であるBean Validationをサポートしている。
単項目チェックには、このBean Validationを利用する。
相関項目チェックの場合は、Bean ValidationまたはSpringが提供しているorg.springframework.validation.Validatorインタフェースを利用する。
この点は、 Macchinetta Server 1.x 開発ガイドラインの 入力チェックの分類 と同様である。
入力チェックの全体像
チャンクモデル、タスクレットモデルにて入力チェックを行うタイミングは以下のとおりである。
- 
チャンクモデルの場合は
ItemProcessorで行う。 - 
タスクレットモデルの場合は
Tasklet#execute()にて、任意のタイミングで行う。 
チャンクモデル、タスクレットモデルにおいて入力チェックの実装方法は同様となるため、
ここではチャンクモデルのItemProcessorで入力チェックを行う場合について説明する。
まず、入力チェックの全体像を説明する。入力チェックに関連するクラスの関係は以下のとおりである。
- 
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に実装する。 
| 
 データ件数の設定 
  | 
| 
 jakarta.validation.Validatorやorg.springframework.validation.Validatorといったバリデータは直接使用しない。 
 
 一方、  | 
| 
 org.springframework.batch.item.validator.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>
@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;
    }
}
<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が読み込み何件目であるかをあらわす数値をクラスフィールドに保持する。 
 - 
 - 
入力チェックルールを定義する。
- 
Macchinetta Server 1.x 開発ガイドラインの 入力チェック を参照。
 
 - 
 
以下に、入力チェックルールを定義した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
}
| 項番 | 説明 | 
|---|---|
(1)  | 
  | 
(2)  | 
引数で受ける  | 
入力チェックの実施
入力チェックの実施方法について説明する。 入力チェック実施は以下の要領で実装する。
- 
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;
    }
}
| 項番 | 説明 | 
|---|---|
(1)  | 
  | 
(2)  | 
入力チェックエラーをハンドリングする。  | 
(3)  | 
  | 
入力チェックエラーのハンドリング
入力チェックエラーが発生した場合の選択肢は以下の2択となる。
- 
入力チェックエラーが発生した時点で処理を打ち切り、ジョブを異常終了させる。
 - 
入力チェックエラーが発生したことをログ等に残し、後続データの処理は継続する。その後、ジョブ終了時に、ジョブを警告終了させる。
 
処理を異常終了する場合
例外発生時に処理を異常終了するためには、java.lang.RuntimeExceptionまたはそのサブクラスをスローする。
例外発生時にログ出力等の処理を行う方法は以下の2とおりがある。
- 
例外をtry/catchで捕捉し、例外をスローする前に行う。
 - 
例外をtry/catchで捕捉せず、
ItemProcessListenerを実装しonProcessErrorメソッドにて行う。- 
ItemProcessListener#onProcessError()は@OnProcessErrorアノテーションを使用して実装してもよい。 詳細は、リスナーを参照。 
 - 
 
例外発生時に、例外情報をログ出力し、処理を異常終了する例を以下に示す。
@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;
    }
}
| 項番 | 説明 | 
|---|---|
(1)  | 
try/catchにて例外を捕捉する。  | 
(2)  | 
入力チェックを実行する。  | 
(3)  | 
例外をスローする前にログ出力処理を行う。  | 
(4)  | 
例外をスローする。  | 
@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());
    }
}
| 項番 | 説明 | 
|---|---|
(1)  | 
入力チェックを実行する。  | 
(2)  | 
  | 
(3)  | 
例外をスローする前にログ出力処理を行う。  | 
| 
 ItemProcessListener#onProcessError()使用時の注意点 
 
  | 
エラーレコードをスキップする場合
入力チェックエラーが発生したレコードの情報をログ出力等を行った後、エラーが発生したレコードをスキップして後続データの処理を継続する場合は以下の要領で実装する。
- 
例外をtry/catchで捕捉する。
 - 
例外発生時のログ出力等を行う。
 - 
ItemProcessor#process()の返り値としてnullを返却する。- 
nullを返却することで入力チェックエラーが発生したレコードは後続の処理対象(ItemWriterによる出力)に含まれなくなる。 
 - 
 
@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;
    }
}
| 項番 | 説明 | 
|---|---|
(1)  | 
try/catchにて例外を捕捉する。  | 
(2)  | 
入力チェックを実行する。  | 
(3)  | 
  | 
(4)  | 
  | 
終了コードの設定
入力チェックエラーが発生した場合、入力チェックエラーが発生しなかった場合とジョブの状態を区別するために必ず正常終了ではない終了コードを設定すること。
入力チェックエラーが発生したデータをスキップした場合、異常終了した場合においても終了コードの設定は必須である。
終了コードの設定方法については、ジョブの管理を参照。
エラーメッセージの出力
入力チェックエラーが発生した場合にMessageSourceを使用することで、任意のエラーメッセージを出力することができる。 エラーメッセージの設定については、Macchinetta Server 1.x 開発ガイドラインの エラーメッセージの定義を参照。 エラーメッセージを出力する場合は以下の要領で実装する。
エラーメッセージを出力する方法としては、以下の2とおりがある。
- 
レコード内の各項目についてエラーメッセージを出力
 - 
エラーメッセージをまとめて出力
 
レコード内の各項目についてエラーメッセージを出力する場合の要領と実装例を以下に示す。
- 
入力チェックでエラーが発生した項目に対して、
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);
    }
}
| 項番 | 説明 | 
|---|---|
(1)  | 
  | 
(2)  | 
try/catchにて例外を捕捉する。  | 
(3)  | 
入力チェックを実行する。  | 
(4)  | 
  | 
(5)  | 
  | 
(6)  | 
取得した  | 
(7)  | 
  | 
エラーメッセージをまとめて出力する場合の要領と実装例を以下に示す。
- 
StepExecutionContextを利用し、入力チェックでエラーが発生した項目のエラーメッセージをリストに格納しておく。 - 
AfterStepで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);
        }
    }
}
| 項番 | 説明 | 
|---|---|
(1)  | 
クラスに  | 
(2)  | 
  | 
(3)  | 
  | 
(4)  | 
  | 
(5)  | 
エラーメッセージを格納するためのリストを定義する。  | 
(6)  | 
  | 
(7)  | 
  | 
(8)  | 
  | 
(9)  | 
  | 
(10)  | 
  | 
(11)  | 
  | 
(12)  | 
エラーメッセージを繰り返しログ出力する。  |