« Spring BatchでつくるCSVを複数ファイルに分割する | トップページ

2023年12月 6日 (水)

Spring Bootを最新化した(通算3度目)

0. 重い腰を上げて(ry

弊社で稼働中の受注監視バッチは、2度のバージョンアップを経てSpring Boot 2.7で稼働していたのだが、このほどSpring Boot 2.7/3.0のOSSサポートが切れる、ということで、必要に駆られてSpring Bootの最新化を敢行した。

結論から言うと、今までのバージョンアップで一番きつかったorz

1. 当初「やること」と思っていたこと

2.7からのアップグレードになるので、やること自体はSpring Boot 3.0 Migration Guideをもとに行うのだが、弊社の場合Webモジュールよりもバッチモジュールのほうが主体なので、世の中でよく言われている(と思っている)

  • Spring Securityに由来する諸々の変更
  • Jakarta EE 10への対応
  • Spring MVCにおけるエンドポイントの末尾スラッシュの取り扱い

のほか、Spring Batchの更新が主体だった。
が、これが鬼門だった。

2. Spring Boot 3.0にしてからの作業

Spring Boot 3.xはJava 17が必須なので、これを扱えるGradle 7.5以降にGradle Wrapperを更新したのち、Spring Bootのバージョンを3.0系列の最新版(当時)の3.0.12にひとまず設定。

Spring MVCの話は結論から言うと特に何もすることはなかったので割愛するが、それ以外の対応でここからが長丁場になる。

2-1. Spring Securityに由来する諸々の変更

Spring Securityは6.0が標準となるなるので、その前にある5.8のマイグレーションガイドをもとに、

  • PasswordEncoderの更新
  • Servlet周りの更新

を行っていく。

PasswordEncoderは、デフォルトがDelegatingPasswordEncoderから呼ばれるBCryptだそうなので、PasswordEncoderのBean定義を

  @Bean
  public PasswordEncoder passwordEncoder() {
    //return new BCrypotPasswordEncoder();
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

に書き換えた上で、

  UPDATE APP_USER SET PASSWD = CONCAT('{bcrypt}', PASSWD)
  WHERE PASSWD NOT LIKE '{bcrypt}%';

と、DelegatingPasswordEncoderがBCryptを選択できるようにしておく。
SQL自体は、FlyWayのマイグレーションに組み込んでおけば確実に実行される。

その他Servlet周りの更新については、該当するものが

の3点だったので、それぞれを対応。とはいえ、ほぼマイグレーションガイドの記載のとおりなんだが。

2-2. Jakarta EE 10への対応

データアクセスやServletがもろに影響を受けるjavax.*からjakarta.*へのパッケージ移動だが、弊社の環境ではJAXBとMailSenderも影響下にあった。

メール送信テストでGreenMailという組み込みメールサーバを使っているが、こちらも必要に駆られて更新。

  dependencies {
    def greenMailVer = "2.1.0-alpha-3"
    testImplementation("com.icegreen:greenmail:$greenMailVer")
    testImplementation("com.icegreen:greenmail-junit5:$greenMailVer")
    testRuntimeOnly("org.eclipse.angus:angus-mail:2.0.2")
  }

最後のEclipse Angus MailはGreenMailが内部で利用しているもののようだが、GreeenMailからの呼び出しに失敗していたため、明示的にアーティファクトを追加することで対応。

また後工程のアーティファクトのビルド時に発覚することだが、JAXBもJakarta EE 10対応品の4.0に更新した際、jaxb-api.jar:4.0.3において

> アーティファクトの重複を検知したがどうするかが定義されていない

としてビルドが途中で止まるハプニングがあった。

JAXBはYahoo!ショッピングへのAPIアクセスで使っているため、ここのAPIを使う可能性のあるモジュールに

  bootJar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
  }

を追加することで対応した。

3. そしてSpring Batch

そして今回最大の難関、Spring Batchの更新である。

こちらも例によってSpring Batch 5.0 Migration Guideをもとに移行作業を行っていくわけだが、結論から言うと、メタデータはまるっと捨てた。

3-1. メタデータスキーマの構造変更

まず、BATCH_STEP_EXECUTIONとBATCH_JOB_EXECUTION_PARAMSに、カラムの追加削除が発生する。

DBMSがOracle DatabaseMicrosoft SQL Serverの場合は追加の作業が発生するが、弊社の場合MySQLなので、移行自体は

  ALTER TABLE BATCH_STEP_EXECUTION ADD CREATE_TIME DATETIME(6) NOT NULL DEFAULT '1970-01-01 00:00:00';
  ALTER TABLE BATCH_STEP_EXECUTION MODIFY START_TIME DATETIME(6) NULL;

  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DATE_VAL;
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN LONG_VAL;
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DOUBLE_VAL;

  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS CHANGE COLUMN TYPE_CD PARAMETER_TYPE VARCHAR(100);
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS CHANGE COLUMN KEY_NAME PARAMETER_NAME VARCHAR(100);
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS CHANGE COLUMN STRING_VAL PARAMETER_VALUE VARCHAR(2500);

を実行することで終わる。移行SQL自体はSpring Batchのソースの中、migration-mysql.sqlとして格納されているので、これを実行すればよいことになる。

が、これをそのまま実行すると少々面倒なことになることが後で判明する。

一方BATCH_JOB_EXECUTION.JOB_CONFIGURATION_LOCATIONカラムは使われなくなったのでドロップできる、とのこと。
ドロップする場合は以下の用に実行すればよいそうだ。

  ALTER TABLE BATCH_JOB_EXECUTION DROP COLUMN JOB_CONFIGURATION_LOCATION;

ただ、これ自体は実行していないので、カラムは残ったまま。

3-2. EnableBatchProcessing アノテーションの扱い変更

Spring Boot 3.0 Migration Guideには、

Previously, @EnableBatchProcessing could be used to enable Spring Boot’s auto-configuration of Spring Batch. It is no longer required and should be removed from applications that want to use Boot’s auto-configuration. A bean that is annotated with @EnableBatchProcessing or that extends Batch’s DefaultBatchConfiguration can now be defined to tell the auto-configuration to back off, allowing the application to take complete control of how Batch is configured.

とあり、以前のようにEnableBatchProcessingをつけているとSpring Bootの自動構成が止まるようになっている、とのこと。

早い話、EnableBatchProcessingの中にConigurationアノテーションがつかなくなったということなので、追加でConfigurationをバッチ定義クラスにつける。

この状態でテストを実行。見事にコケる。スタックトレースには、「データソースがない」などと出ている。

いろいろ調べた結果、Spring BootでSpring Batchを使う場合、

  • @EnableBatchProcessingあり → JDBCベースのデータソース構成とTransactionManagerが必要
  • @EnableBatchProcessingなし → Spring Bootが作った構成をそのまま使用

なので、バッチ定義クラスからEnableBatchProcessingを削除した。これ、よく見たら上で書いてるやつだな、うん。

3-3. ExecutionContextSerializerの変更

ステップ間でデータの引き渡しをするのに、ExecutionContextを使うケースがあるかと思う。弊社はまさにこれ。
実際には、引き渡すオブジェクトはJSON文字列としてやり取りされる。

Spring Batch 5から、com.fasterxml.jackson.core:jackson-coreがspring-batch-coreの依存から外れ、オプション扱いになり、これに伴い(というか後述の結果として、と言うべきか)ExecutionContextSerializerのデフォルト実装をJacksonExecutionContextStringSerializerからDefaultExecutionContextSerializerに変えた、とのこと。

このDefaultExecutionContextSerializer、既定でBASE64でシリアライズするので、保存対象のメンバーの型にはjava.io.Serializableの実装が必要だったりする。

したがって、java.nio.file.Pathなどは保存不可になっているのと、それまで使っていたJSONでシリアライズされた情報と互換性がなくなる。

やろうと思えばJSONのままにできるそうだが、EnableBatchProcessingを使ってExecutionContextSerializerへの参照を定義する必要がある。
これはとりもなおさず、Spring Bootがやってくれる自動構成をすべて自力で書く必要があることになるので、保存されているExecutionContextは廃棄することにした。

3-4. その他

その他細かいところで言えば、Taskletなんかで参照可能なステップの実行時刻などがjava.util.Dateからjava.time.LocalDateTimeに変わっていて、実行時間の計測処理に手を入れることになった。

4. そしてSpring Boot 3.2へ

移行作業の開始当初は、最新版が3.1だったのでそこを目標にしていたのだが、やってる最中にSpring Boot 3.2がGAになったので、急遽ターゲットを変更。

Spring Boot 3.1の段階で、Spring Data JPAの技術基盤であるHibernateが6.2になっており、OneToOneユニーク制約がつくように改修されている。
Hibernateプロジェクト曰く、「1対1対応という観点でいうと、ユニークがつかないのは不適」だそうで。

また本来位取り(scale)の概念がないdoubleやfloatといった型にscaleを定義していると怒られるようになったので、以下のいずれかの対応が必要。簡単なのは前者。

CIによるテストもパスしたので、本番デプロイに入ったところ、やはりというか問題発生。ExecutionContextの読み出しでソリッドに例外を吐く。キャストに失敗している模様。

スタックトレースをもとにSpring Batchのソースを追っていくと、該当箇所はこうなっていた。

JdbcJobExecutionDao

  @SuppressWarnings(value = { "unchecked", "rawtypes" })
  protected JobParameters getJobParameters(Long executionId) {
    final Map> map = new HashMap<>();
    RowCallbackHandler handler = rs -> {
      String parameterName = rs.getString("PARAMETER_NAME");

      Class parameterType = null;
      try {
        parameterType = Class.forName(rs.getString("PARAMETER_TYPE"));
      }
      catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
      }
      String stringValue = rs.getString("PARAMETER_VALUE");
      Object typedValue = conversionService.convert(stringValue, parameterType);

      boolean identifying = rs.getString("IDENTIFYING").equalsIgnoreCase("Y");

      JobParameter jobParameter = new JobParameter(typedValue, parameterType, identifying);

      map.put(parameterName, jobParameter);
    };

    getJdbcTemplate().query(getQuery(FIND_PARAMS_FROM_ID), handler, executionId);

    return new JobParameters(map);
  }

上記14行目でResultSetの値からリフレクションをかけようとしていたのがわかったので、すぐさま

  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_TYPE = 'java.lang.Long' WHERE PARAMETER_TYPE = 'LONG';
  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_TYPE = 'java.lang.String' WHERE PARAMETER_TYPE = 'STRING';

を行おうとしたものの、先に LONG_VAL カラムをドロップしてしまっていたため、書き込むべき値がなくなってしまっていた。

このためすべてのテーブルのtruncateを試みたが、BATCH_JOB_EXECUTION、BATCH_STEP_EXECUTIONが外部参照によりtruncateできなかったため、結局のところ以下のような方法でデータを廃棄した。

  DELETE
  FROM BATCH_STEP_EXECUTION_CONTEXT
  WHERE STEP_EXECUTION_ID IN (SELECT BATCH_STEP_EXECUTION.STEP_EXECUTION_ID
                              FROM BATCH_STEP_EXECUTION
                              WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID
                                                         FROM BATCH_JOB_EXECUTION
                                                         WHERE STATUS IN ('COMPLETED', 'FAILED')));
  DELETE
  FROM BATCH_STEP_EXECUTION
  WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM BATCH_JOB_EXECUTION WHERE STATUS IN ('COMPLETED', 'FAILED'));

  DELETE
  FROM BATCH_JOB_EXECUTION_CONTEXT
  WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM BATCH_JOB_EXECUTION WHERE STATUS IN ('COMPLETED', 'FAILED'));

  DELETE
  FROM BATCH_JOB_EXECUTION_PARAMS
  WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM BATCH_JOB_EXECUTION WHERE STATUS IN ('COMPLETED', 'FAILED'));

  DELETE
  FROM BATCH_JOB_EXECUTION
  WHERE STATUS IN ('COMPLETED', 'FAILED');

  DELETE
  FROM BATCH_JOB_INSTANCE
  WHERE JOB_INSTANCE_ID NOT IN (SELECT BATCH_JOB_EXECUTION.JOB_INSTANCE_ID FROM BATCH_JOB_EXECUTION);

  TRUNCATE TABLE BATCH_STEP_EXECUTION_SEQ;
  INSERT INTO BATCH_STEP_EXECUTION_SEQ (ID, UNIQUE_KEY)
  select *
  from (select 0 as ID, '0' as UNIQUE_KEY) as tmp
  where not exists(select * from BATCH_STEP_EXECUTION_SEQ);

  TRUNCATE TABLE BATCH_JOB_EXECUTION_SEQ;
  INSERT INTO BATCH_JOB_EXECUTION_SEQ (ID, UNIQUE_KEY)
  select *
  from (select 0 as ID, '0' as UNIQUE_KEY) as tmp
  where not exists(select * from BATCH_JOB_EXECUTION_SEQ);

  TRUNCATE TABLE BATCH_JOB_SEQ;
  INSERT INTO BATCH_JOB_SEQ (ID, UNIQUE_KEY)
  select *
  from (select 0 as ID, '0' as UNIQUE_KEY) as tmp
  where not exists(select * from BATCH_JOB_SEQ);

この結果を踏まえると、前述のマイグレーションSQLは以下のようにしたほうがよかった、といえるかもしれない。
ただ試せてないので実環境での実行は参考程度で。

  ALTER TABLE BATCH_STEP_EXECUTION ADD CREATE_TIME DATETIME(6) NOT NULL DEFAULT '1970-01-01 00:00:00';
  ALTER TABLE BATCH_STEP_EXECUTION MODIFY START_TIME DATETIME(6) NULL;

  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS CHANGE COLUMN TYPE_CD PARAMETER_TYPE VARCHAR(100);
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS CHANGE COLUMN KEY_NAME PARAMETER_NAME VARCHAR(100);
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS CHANGE COLUMN STRING_VAL PARAMETER_VALUE VARCHAR(2500);

  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_VALUE = CAST(DATE_VAL AS VARCHAR) WHERE PARAMETER_TYPE = 'DATE';
  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_VALUE = CAST(DOUBLE_VAL AS VARCHAR) WHERE PARAMETER_TYPE = 'DOUBLE';
  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_VALUE = CAST(LONG_VAL AS VARCHAR) WHERE PARAMETER_TYPE = 'LONG';

  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_TYPE = 'java.lang.Long' WHERE PARAMETER_TYPE = 'LONG';
  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_TYPE = 'java.lang.String' WHERE PARAMETER_TYPE = 'STRING';
  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_TYPE = 'java.lang.Double' WHERE PARAMETER_TYPE = 'DOUBLE';
  UPDATE BATCH_JOB_EXECUTION_PARAMS SET PARAMETER_TYPE = 'java.util.Date' WHERE PARAMETER_TYPE = 'DATE';

  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DATE_VAL;
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN LONG_VAL;
  ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DOUBLE_VAL;

5. その後

デプロイ後の監視を行っていると、バッチモジュールの起動直後にBean生成で警告が上がっていた。
いずれもSpring BatchにまつわるBeanの模様。

  WARN  [main : o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari' of type [org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected into a currently created BeanPostProcessor [jobRegistryBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies.
  WARN  [main : o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] - Bean 'spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties' of type [org.springframework.boot.autoconfigure.jdbc.DataSourceProperties] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected into a currently created BeanPostProcessor [jobRegistryBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies.

ググってみるとSpring BatchのGitHubにもたびたび報告が寄せられているようで、どうやらSpring Batch 5で行われた「JDBCベースのJobRegistryへの移行」に起因している模様

なので、Spring Batchのプロジェクトがどう対応するかに依存するようなので、ひとまず様子見するしかなさそうだ。

« Spring BatchでつくるCSVを複数ファイルに分割する | トップページ

コメント

コメントを書く

(ウェブ上には掲載しません)

« Spring BatchでつくるCSVを複数ファイルに分割する | トップページ

2023年12月
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

最近のトラックバック

無料ブログはココログ