先週末。ちょうど広島に居た嫁さんと娘が、職場の近くの段原ショッピングセンターまで車で迎えに来てくれたので、そのまま6階に上がって「サイゼリア 段原SC」で晩飯を摂っていくことにした。

で、当然俺は100円のグラスワイン(赤)を二杯、三杯と・・・(笑)。仕方ないよね。税込 100円のグラスワインがそこにあれば、注文せんわけにいかんでしょう?

20260703_escargot1.jpeg
そして、アテには久しぶりの「エスカルゴ」。
最近、某SNSのドゥカティコミュニティで一緒の人がよくカタツムリの画像を上げてて、「サイゼリアへの納品用ですか?」なんてコメントつけたら、なんかこのネタがプチ盛り上がりしちゃって(笑)

そりゃ、サイゼリアに来たら注文するしかないやろう(笑)

ただ、久しぶりに食べるとバターの味ばかり感じて、ニンニクのピリっとした刺激や塩気も全然足りない感じで・・・(いや、サイゼリア以外でエスカルゴ食ったことないんで正解の味はわからないんだけど(^^;;;)。始めて食ったときは(サイゼリアのエスカルゴだろうと)けっこう感動したんだけどな(笑)

俺の舌が年齢とともに劣化したのか(それはある(^^;;)、老いた身体がバターを拒絶するのか(それもある気がする(^^;;;)、ま、俺の舌が肥えたということにしておこう(笑)
Java 案件は何度もやったことはあるが、既存システムの修正などが主体だったので、そんなにJavaの言語仕様を深く理解はしていなかったのよね。

なもんで、今回初めて Java のソースで """ とダブルクォーテーションが3つ並んでるのを見て、「なんじゃこりゃ?」って(^^;;;。

君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで

これを文字列リテラルに突っ込むんだけど、

String msg = """
君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで
""";

となってて。

VB.NETのようにダブルクォーテーションをダブルクォーテーションでエスケープしているわけじゃないよな。それだと文の前後にダブルクォーテーションが付いちゃうしな(「"君が世は~むすまで"」ってなっちゃう)・・・と悩む。

実はこれ、文字列リテラルにそのまま複数行を突っ込める「テキストブロック(Text Blocks)」という機能。
これを使わないと、

String msg = "君が代は千代に八千代に\n"
           + "さざれ石の巌となりて\n"
           + "苔のむすまで";

こんな風に書くしかないのでごちゃごちゃ読みづらい。

Java の改修履歴を追ってみると、

Java SE 13(2019年9月17日)
 Text Blocks(文字列リテラルの複数行記述可能)第1回試作
Java SE 14(2020年3月17日)
 Text Blocks(文字列リテラルの複数行記述可能)第2回試作
Java SE 15(2020年9月15日)
 Text Blocks・第3回試作 ※これ以降、試作の記述なし

と、2020年にやっと正式な仕様として取り込まれたようだ。マジ?それまで Java って改行を含む複数行を一発で文字列リテラルに突っ込む方法ってなかったん?

俺の母国語(笑)である Perl なら、もう何十年も前からヒアドキュメントってやつで複数行を文字列リテラルに突っ込める。

my $msg = <<EOS;
君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで
EOS

こういうやつね。さらに言えばヒアドキュメント使わなくても単純にクォーテーション(シングルでもダブルでも)で囲ってやるだけでいい。

my $msg = '君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで';

VB.NETでも同じように(こっちはダブルクォーテーションね)、

Dim msg As String = "君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで"

って書ける。

ただ、一行目をクォーテーションのすぐ横から開始しないといけないので(改行しちゃうと改行がそのまま一行目として入っちゃうので)一行目の開始位置はずれる。これが嫌なら Perl ならやっぱヒアドキュメントがいいか。

今知ったんだけど、VB.NETも 2015年リースの VB 14.0 から可能になったんだなあ。そういえば、その前は & でつなげて書いてたっけ?

Dim msg As String = "君が代は千代に八千代に" + vbCrLF +
                    "さざれ石の巌となりて" + vbCrLF +
                    "苔のむすまで"

みたいに?(もう記憶が・・・)

まあ、なんにせよ、Java でこういう書き方ができるようになったのが 2020年からとは本当に意外だ。
Java が母国語の人って、こういう要件ってあまりなかったのかね?
プログラムの中にあまり HTML を直書きしたりとか、そういうのが無かったのかもね。
今年の 2月に愛機 DUCATI MONSTER S2R 1000 にオイル漏れ???という疑いが発生(「S2R のフロントシリンダーの付け根あたりがオイルでテカテカ」というエントリー参照)。

すぐに高校の同級生が働いている整備工場に持ち込んだのだが、「冬はバイクに乗らんじゃろ?」という間違った判断のもと、一ヶ月くらい放置。二週続けて「もう原因わかったか?早よ乗りたいんじゃけど」と顔を出して、やっと「え?冬なのにバイク乗るん?」と調査をしてくれた(^^;;; 冬でも夏でもオートバイ乗るわっ!雪が積もる地方じゃないんじゃけ。

20260320_kannon1.jpeg 20260320_kannon2.jpeg 20260320_kannon3.jpeg

結果、「経年劣化でブリーザーバルブが割れてそこから漏れたオイルがエンジン前部まで流れて、シリンダーから漏れてるように見えたんじゃろう」とのこと。
で、状況みようとしてバルブを外そうとしたら、完全に固着してて、しかも本体が経年劣化で脆くなってて、上下にぱっくり割れたらしい(^^;;;
とりあえず耐熱ボンドで割れたところ固めてるって(笑)

「交換するんなら無理やり外すけど、どうしよう?部品代だけで1万数千円するけど」ってことだったので、「とりあえずこのままで良い」と答え、S2Rが返ってきた 3/20(祝)。確認のショートツーリングへ(笑)

行き先は美川町の「観音茶屋自販機コーナー」。自販機の肉うどん食べながらエンジンチェック。全然オイルで濡れてない!ブリーザーバルブ周りも大丈夫。耐熱ボンド、強いなあ(笑)

そういえば、オイル漏れが発覚したのが自販機うどんで有名な「欽明路自販機コーナー」で、そのオイル漏れの解消を確認したのがやはり自販機うどんで有名な「観音茶屋自販機コーナー」。どんだけ自販機うどんが好きなんやって話やね(笑)

というわけで、樹脂部品はやっぱり国産バイクに比べると弱いなあって感じはするんだけど、色々対応方法はあるからね。今日も俺のドゥカティは元気に走ってます(笑)
もう 3月下旬頃の話だけど・・・

20260319_tsubasa2.jpeg
広島のサッカースタジアム「EDION PEACE WING HIROSHIMA」の一階にある「廣島汁なし担担麺 -TSUBASA- 2nd」で汁なし担々麺を食べた(この店では「担担麺」と表記するようだ)
船越の方に「つばさ」って美味いラーメン屋あったけど、あれとは関係ないんよね?(笑)

実はピースウィング内に汁なし担々麺屋があるらしいということは知っていたのだが、通勤時に二階コンコースを歩きながら(自由に歩ける)店を探したのだが見つけることが出来てなかった。
ついつい、ズムスタのイメージがあるんで、二階コンコースに店が集中してるのかと思ってたのよね。一階でした。

さすがサッカースタジアムにあるお店。店の中も小綺麗な感じ。
時間は晩飯にはちょっと早めの夕方 5時頃だったので、俺の他に客はサラリーマン風の若い兄ちゃん(イケメン)と、あとは若い女子二人連れ。なんか、おっさんが若干浮く感じがしたんだけど、その後俺くらいの、しかもぽっちゃり小太りのおっさんが入ってきたのでホッとした(笑)

20260319_tsubasa1.jpeg
汁なし担々麺は見た目も味も上品な感じかな。

サンフレッチェをイメージしたのであろう紫キャベツ(赤キャベツ)の千切りがかかっていて、サンフレファンにはたまらん見た目やね(笑)
そして、他県からサンフレの応援に来て「なんか、広島じゃ汁なし担々麺てのが有名らしい」って食べて行くにはちょうど良い辛さと痺れ。多分、汁なし担々麺が好きになると思う。

・・・が、荒々しい辛さと痺れをもとめ、「武蔵坊」では辛さランク「death(デス)」を、「階杉」では「Yoga Inferno(ヨガインフェルノ)」を選ぶ俺にはちょっと物足りなさもあったのである(笑)
さて、無事に Oracle Database 21c Express Edition (XE) のインストールが終了

具体的にテスト用(お勉強用)の TABLE などを作っていきたいが、Windows 上で CUI 環境の sqlplus でゴリゴリやるのもなあ・・・いや、実際、UNIX 互換界隈では PostgreSQL を CUI 環境の psql とかでゴリゴリやってるんだけど、あれは Putty とか TeraTerm のようなターミナルソフトの上で zsh といったシェルを操作するから成り立っているのであって、Windows の Terminal の上で(PowerShell であろうと)CUI 環境のソフトを使うのは苦行でしかない(^^;;;

Qracle が提供している DB管理の GUI 環境「Oracle SQL Developer」は無償で使えるが、Oracle
アカウントを作る必要があり面倒くさいので(だって、この案件終わったら Oracle 案件はもう無い気がするし(笑))、みんな使ってるフリーの高機能 SQL クライアント「A5:SQL Mk-2」を使うことにした。

■ダウンロード&インストール

インストーラはないので、ダウンロードしてきたファイルを解凍して適当な場所に配置するだけ。

俺は Vector からダウンロードした。


a5m2_2.21.2_x64.zip がダウンロードされるので、それを解凍し、どこか適当なフォルダ(ディレクトリ)に置けば OK。


■接続情報の登録

A5M2.exe をダブルクリックして実行。
「データベース」→「データベースの追加と削除」で、「Oracle Database (OCI経由 or 直接接続)」を選択。

・接続方法 直接接続(Oracle Client不要)
・ホスト名 localhost
・サービス名 XEPDB1
・ユーザーID tester
・パスワード tester01

と指定して「テスト接続」を押し「接続に成功しました」となれば問題なし。「OK」押下で登録名を聞かれるのでわかりやすい名前を。ま、あとでリネームできるけど。
接続に成功しなかったら・・・何か typo しているので頑張って(笑)

20260701_A5_02.jpg

※ちなみに、このエントリーは A5:SQL Mk-2 の使い方を説明するものではないので、接続後にカレントスキーマを選んで TABLE を作成したりとかは自分で勉強してね。


■テーブルを作ってデータを登録

問題なく行えた。

20260701_A5_03.jpg

これで Spring Batch で DB 読込のテストができる。
勉強用に Oracle Database 21c Express Edition (XE) をインストールする。

■ダウンロード

 ↓

「Oracle Database 21c Express Edition for Windows (64-bit)」をクリックしてダウンロード開始。
(実際の業務では 19c を使うが、19c には Express Edition(XE) がない。あるのは 18c、21c だけ。19c スキップされている。19c の無償版のダウンロードは Oracle アカウントが必要だったり、色々面倒臭いので 21c(XE) で(笑))


■インストール

ダウンロードした OracleXE213_Win64.zip を解凍。

setup.exe と Oracle Database 21c Express Edition.msi があって悩むが、setup.exe 実行でいいよな?setup.exe ダブルクリックで実行。

けっこう時間がかかる。
ディフォルトの設定を変更することなく「次へ」「次へ」と進んでいったので、途中の画面画像は省略。

データベースパスワードは test@pass01 にする。(途中で @ マークを使っていることが後ほど面倒臭いことになるのをまだ俺は知らなかった(^^;)

20260701_oracle04.jpg

インストール完了。


■データベースへの接続

PS C:\Users\TESTER> sqlplus

SQL*Plus: Release 21.0.0.0.0 - Production on 水 7月 1 10:35:14 2026
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.

ユーザー名を入力してください: system
パスワードを入力してください: test@pass01@localhost:1521/XEPDB1<実際は表示されない>
ERROR: ORA-12154: TNS: 指定された接続識別子を解決できませんでした

パスワードに @ マークを含む場合は、パスワードをダブルクォーテーションで囲ってやる必要がある。
@ なんか使わなきゃよかった。面倒くせえ(^^;

ユーザー名を入力してください: system
パスワードを入力してください: "test@pass01"@localhost:1521/XEPDB1<実際は表示されない>
最終正常ログイン時間: 水 7月  01 2026 10:16:04 +09:00


Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SQL>

無事接続されたので、test 用の表領域(テーブルスペース)を作成しよう。


■スキーマの作成

SQL> CREATE TABLESPACE test_space
  2   DATAFILE 'C:\app\data\test.dbf' SIZE 100M
  3   AUTOEXTEND ON NEXT 500K MAXSIZE 1024M;
CREATE TABLESPACE test_space
*
行1でエラーが発生しました。:
ORA-01119: データベース・ファイル'C:\app\data\test.dbf'の作成中にエラーが発生しました。
ORA-27040: ファイルの作成エラー、ファイルを作成できません
OSD-04002: ?t?@?C????[?v??????????S-Error: (OS 3)
?w?????p?X?????????????

あれ?失敗する。権限の問題?


■(ちょっと小休止)特殊な文字の入ってるパスワードは面倒なのでやめる

まだ、TABLESPACE  の作成が終わってないけど、その前に system ユーザのパスワードに @ マークが入っているので、うまく sqlplus で DB 接続できない。

Gemini に「ダブルクォーテーションでパスワードを囲まなきゃ」と言われたんだが、

sqlplus system/\"test@pass01\"@localhost:1521/XEPDB1
qlplus system/"test@pass01"@localhost:1521/XEPDB1

どちらも

ERROR: ORA-12154: TNS: 指定された接続識別子を解決できませんでした

になっちゃうなぁ(実際はダブルクォーテーションではなくシングルクォーテーションで囲まないとダメだったことが後ほどわかるんだけど(^^;;;)

ああ、面倒くせえ。@ マーク入りのパスワードは修正したほうがよさそうだ。

というわけで、system ユーザのパスワード変更。

ローカル(同一サーバー)での OS 認証で接続し、パスワード変更の QUERY を実行する。

PS C:\Users\TESTER> sqlplus / as sysdba

SQL*Plus: Release 21.0.0.0.0 - Production on 水 7月 1 11:23:38 2026
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.



Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SQL> ALTER USER SYSTEM IDENTIFIED BY testpass01;

ユーザーが変更されました。

SQL> ALTER USER SYS IDENTIFIED BY testpass01;

ユーザーが変更されました。

SQL>


パスワードは問題なく変更された様子。
別の Termnal を開いて、試しに接続してみるか。

PS C:\Users\TESTER> sqlplus system/testpass01@localhost:1521/XEPDB1

SQL*Plus: Release 21.0.0.0.0 - Production on 水 7月 1 11:29:13 2026
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.

最終正常ログイン時間: 水 7月  01 2026 10:40:00 +09:00


Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SQL>

行けた・・・やっぱり、パスワードに @ なんか使うもんじゃないね(笑)


■スキーマの作成(再び)

SQL> CREATE TABLESPACE test_space
  2   DATAFILE 'C:\app\data\test.dbf' SIZE 100M
  3   AUTOEXTEND ON NEXT 500K MAXSIZE 1024M;
CREATE TABLESPACE test_space
*
行1でエラーが発生しました。:
ORA-01119: データベース・ファイル'C:\app\data\test.dbf'の作成中にエラーが発生しました。
ORA-27040: ファイルの作成エラー、ファイルを作成できません
OSD-04002: ?t?@?C????[?v??????????S-Error: (OS 3)
?w?????p?X?????????????

ダメか。

あっ、もしかしてディレクトリは先に作成しておかないといけないの?C:\app\data ディレクトリを作って再び CREATE TABLESPACE を実行。

SQL> CREATE TABLESPACE test_space
  2   DATAFILE 'C:\app\data\test.dbf' SIZE 100M><\span>
  3   AUTOEXTEND ON NEXT 500K MAXSIZE 1024M;

表領域が作成されました。

20260701_oracle09.jpg

テーブルスペースはできた。(しかし、エラーメッセージの文字化け化けなんなんかね?すごいことが起きてると思って、単純にディレクトリを手動で作っておかないといけないだけってところに思考がいかなかったよ(^^;;;)

続けてユーザ作成。(データベースの作成もできる DBA 権限を付与)

SQL> CREATE USER tester
  2   IDENTIFIED BY tester01
  3   DEFAULT TABLESPACE test_space
  4   TEMPORARY TABLESPACE TEMP
  5   PROFILE DEFAULT;

ユーザーが作成されました。

SQL> GRANT DBA TO tester;

権限付与が成功しました。


ばっちり。
これで初期インストール作業は終了。
「どんどんレコード(行)が追加されていく複数のテキストデータを一定間隔(1秒)で読んで、追加されたレコードをDBに登録していく」バッチ処理(Spring Batch 6)の話の最後。

一気に、実際の実行部のソースを貼っておきます。
いや、俺も勉強中でしっかり中身が理解できているわけではないので(^^;;;
でも、とりあえず「ネットに公開されているソースをパチって実際に動かして内容を理解したい」って人もいるでしょ?(俺はもろにそっち系(笑))

■バッチ処理全体構成・設定クラス(BatchConfig.java)※Bean 定義クラス

package com.netandfield.test;

import jakarta.persistence.EntityManagerFactory;

import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.infrastructure.item.ItemProcessor;
import org.springframework.batch.infrastructure.item.database.JpaItemWriter;
import org.springframework.batch.infrastructure.item.database.builder.JpaItemWriterBuilder;
import org.springframework.batch.infrastructure.item.file.FlatFileItemReader;
import org.springframework.batch.infrastructure.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.infrastructure.item.file.transform.FixedLengthTokenizer;
import org.springframework.batch.infrastructure.item.file.transform.Range;
import org.springframework.batch.infrastructure.item.support.SynchronizedItemStreamReader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class BatchConfig {

    @Bean
    public Job fileProcessJob(JobRepository jobRepository, Step fileProcessStep) {
        return new JobBuilder("fileProcessJob", jobRepository)
                .start(fileProcessStep)
                .build();
    }

    @Bean
    public Step fileProcessStep(JobRepository jobRepository,
                                PlatformTransactionManager transactionManager,
                                SynchronizedItemStreamReader<ProcessedData> synchronizedReader,
                                ItemProcessor<ProcessedData, ProcessedData> processor,
                                JpaItemWriter<ProcessedData> jpaWriter,
                                StepProgressUpdateListener progressListener) {
        return new StepBuilder("fileProcessStep", jobRepository)
                .<ProcessedData, ProcessedData>chunk(100)
                .transactionManager(transactionManager)
                .reader(synchronizedReader)
                .processor(processor)
                .writer(jpaWriter)
                .listener(progressListener) // リスナーをここで登録!
                .build();
    }

    @Bean
    @StepScope
    public SynchronizedItemStreamReader<ProcessedData> synchronizedReader(
            @Value("#{jobParameters['filePath']}") String filePath,
            @Value("#{jobParameters['linesToSkip']}") Long linesToSkip) {

        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
        tokenizer.setColumns(new Range(1, 4), new Range(5, 24), new Range(25, 27));
        tokenizer.setNames("code", "name", "value");

        FlatFileItemReader<ProcessedData> delegate = new FlatFileItemReaderBuilder<ProcessedData>()
                .name("flatFileItemReader")
                .resource(new FileSystemResource(filePath))
                .linesToSkip(linesToSkip.intValue())
                .lineMapper((line, lineNumber) -> {
                    var fieldSet = tokenizer.tokenize(line);
                    ProcessedData data = new ProcessedData();
                    data.setCode(fieldSet.readString("code").trim());
                    data.setName(fieldSet.readString("name").trim());
                    data.setValue(fieldSet.readInt("value"));
                    return data;
                })
                .build();

        return new SynchronizedItemStreamReader<>(delegate);
    }

    @Bean
    public ItemProcessor<ProcessedData, ProcessedData> processor() {
        return item -> {
            item.setName(item.getName().toUpperCase()); // Name を大文字に変換する
            return item;
        };
    }

    @Bean
    public JpaItemWriter<ProcessedData> jpaWriter(EntityManagerFactory entityManagerFactory) {
        return new JpaItemWriterBuilder<ProcessedData>()
                .entityManagerFactory(entityManagerFactory)
                .build();
    }
}


■スケジューラ(FileWatchScheduler.java)※ジョブを定期的に実行する Bean

package com.netandfield.test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;

import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.parameters.JobParameters;
import org.springframework.batch.core.job.parameters.JobParametersBuilder;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class FileWatchScheduler {

    // Spring Batch 6 では、JobLauncher の役割を内包した JobOperator を使用
    private final JobOperator jobOperator;
    private final Job fileProcessJob;
    private final FileProgressRepository progressRepository;

    private final AppProperties appProperties;

    @Scheduled(fixedDelay = 1000)
    public void runPeriodicBatchJob() {
        Path listFile = Paths.get(appProperties.getFilesListPath());
        if (!Files.exists(listFile)) {
            log.warn("ファイル一覧が見つかりません: {}", appProperties.getFilesListPath());
            return;
        }

        try {
            List<String> targetFilePaths = Files.readAllLines(listFile);
            for (String filePath : targetFilePaths) {
                if (filePath.trim().isEmpty() || !Files.exists(Paths.get(filePath))) {
                    continue;
                }
                processSingleFile(filePath.trim());
            }
        } catch (IOException e) {
            log.error("ファイル一覧の読み込みに失敗しました", e);
        }
    }

    private void processSingleFile(String filePath) {
        // 1. 進捗管理DBから前回の行数を取得
        FileProgress progress = progressRepository.findById(filePath)
                .orElseGet(() -> {
                    FileProgress newProgress = new FileProgress();
                    newProgress.setFilePath(filePath);
                    newProgress.setLastReadLines(0);
                    return progressRepository.save(newProgress);
                });

        try {
            // 2. パラメータの組み立て
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("filePath", filePath)
                    .addLong("linesToSkip", (long) progress.getLastReadLines())
                    .addLocalDateTime("runTime", LocalDateTime.now()) // 毎回新規実行にするため
                    .toJobParameters();

            // 3. [Spring Batch 6 正攻法]
            // JobOperator が JobLauncher を継承し、run メソッドを直接安全に呼び出せる(警告なし)
            jobOperator.run(fileProcessJob, jobParameters);

            // ※進捗(行数)の更新処理は、バッチのライフサイクルに則り「StepProgressUpdateListener」が自動的に実行

        } catch (Exception e) {
            log.error("ファイル [{}] のバッチ処理中にエラーが発生しました", filePath, e);
        }
    }
}


■進捗(行数)更新処理クラス(StepProgressUpdateListener.java)※もちろん Bean

package com.netandfield.test;

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.listener.StepExecutionListener;
import org.springframework.batch.core.step.StepExecution;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class StepProgressUpdateListener implements StepExecutionListener {

    private final FileProgressRepository progressRepository;

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        // ステップが正常終了した場合のみ、進捗を更新する
        if (stepExecution.getExitStatus().equals(ExitStatus.COMPLETED)) {
            // 起動パラメータからファイルパスを取得
            String filePath = stepExecution.getJobParameters().getString("filePath");
            long newlyRead = stepExecution.getReadCount();

            if (filePath != null && newlyRead > 0) {
                FileProgress progress = progressRepository.findById(filePath)
                        .orElseGet(() -> {
                            FileProgress newProgress = new FileProgress();
                            newProgress.setFilePath(filePath);
                            newProgress.setLastReadLines(0);
                            return newProgress;
                        });

                progress.setLastReadLines(progress.getLastReadLines() + (int) newlyRead);
                progressRepository.save(progress);
                log.info("ファイル [{}] の進捗を更新しました。新たに {} 行処理、累計 {} 行。",
                        filePath, newlyRead, progress.getLastReadLines());
            }
        }
        return stepExecution.getExitStatus();
    }
}

「Spring Batch 6 、だんだん増えていく固定長レコードを処理する」の(1)~(3)に載せているソースを使って新しいプロジェクトを作れば、Spring Batch 6 で、「だんだん増えていく固定長レコード」が処理できます(笑)

Spring Batch は ver 6 大きくライブラリ構成などが変わっちゃったり、古い機能が完全に削除されたりしてるので、2026年6月現在の最新の Eclipse Pleiades All in One版を落としてきて Spring Batch の勉強をしようとすると、ネット上に公開されている色々なサンプルソースではまず動かない(^^;;;

AI に「Spring Batch 6 で動かすにはどうすればいいの?」と聞いても、3 やら 5 やらのソースばかり返してくるのよね(^^;;; AI はネット上の情報をかき集めて自分の知識としてるんだけど、そもそもネット上に Spring Batch 6 に対応したソースなどの情報が少ないので、AI 自身その関係の知識がないのよね(笑)

でも、このエントリーのソースなら動きます(笑)
さあ、実際に動かして、俺に色々教えてくれたまえ。

周回レースの集計データの処理を想定した「どんどんレコード(行)が追加されていく複数のテキストデータを一定間隔(1秒)で読んで、追加されたレコードをDBに登録していく」バッチ処理(Spring Batch 6)の話のつづき

ちなみに(1)で「仕様」に書き忘れてたけど、データベースへのアクセスは JDBC ではなく JPA を使っています。
俺的には全然 SQL は苦手ではないし、もっと言えば複雑な SQL を書くのは好きな方ですが(笑)、一昨年から昨年にかけて C#.NET で「DB操作は LINQ」って案件やって、ああ、糞っ、直接生の SQL 書けば一発なのにと苦労した記憶があるので(^^;;;、敢えて JPA を選んでみたわけです。勉強のために。

※LINQ でも直接 SQL 書けるじゃんってツッコミは無しで願います。言語仕様の話ではなく、コーディングルールで禁止されていたということなのよ。

ま、とういうわけで、今回はプロパティファイルや、テーブルの設定などを。

■Apache Maven プロジェクト設定ファイル(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>BatchTest5</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>BatchTest5</name>
    <description/>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-h2console</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

他のエントリーでも書いたけど、バッチ処理だけど H2 コンソールを動かすために tomcat が必要なので、spring-boot-starter-webmvc モジュールが依存関係(dependency)として登録されていること。


■プロパティ(resource/application.properties)

spring.application.name=BatchTest5

# ファイル一覧
app.files-list-path=C:\\work\\files.txt

# H2 を PostgreSQL 互換モードで使用(テーブル名等は小文字で)
spring.datasource.url=jdbc:h2:./.data/h2/db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 Console を有効化
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA/Hibernate設定
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.batch.job.enabled=false

# Webアプリケーションとして常駐
spring.main.web-application-type=servlet

特に注意するのは
spring.batch.job.enabled=false
かな。
これは起動時に自動でバッチジョブを実行しないようにしている。

Spring Boot はバッチ用のライブラリ(spring-boot-starter-batch)を検知すると、定義されている @Bean のジョブを起動時にすべて片っ端から実行しようとしてしまう。今回はスケジューラでパラメータ(読み込むファイル名とか)を与えてジョブを起動する形にしているから、勝手にジョブを起動されちゃうと「パラメータが無い状態」の実行となりエラーが発生する。なので「自動で起動すんなよ」と抑制しているわけやね。

あと、app.files-list-path は「'app.files-list-path' is an unknown property. [PROP_UNKNOWN_PROPERTY]」という警告が出るので、プロパティを認識させるためのメタデータを作ってやる。


■メタデータファイル(resource/META-INF/additional-spring-configuration-metadata.json)

{"properties": [{
  "name": "app.files-list-path",
  "type": "java.lang.String",
  "description": "A description for 'app.files-list-path'"
}]}


■Spring Boot メインクラス(BatchTest5Application.java)

package com.netandfield.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class BatchTest5Application {
    public static void main(String[] args) {
        SpringApplication.run(BatchTest5Application.class, args);
    }
}

Eclipse で Spring Batch のバッチを実行するときは、このファイルを Java アプリケーションとして実行する。


■DBマッピングクラス(FileProgress.java)
 ※各ファイルが何行目まで読まれているかを保持するテーブル(file_progress)

package com.netandfield.test;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import lombok.Data;

@Entity
@Table(name = "file_progress")
@Data
// 各ファイルの読み込み行数を管理するエンティティ
public class FileProgress {
    @Id
    private String filePath; // ファイルのフルパス
    private int lastReadLines; // 前回までに読み込み完了した行数
}

20260630_e2_01.jpg


■DBマッピングクラス(ProcessedData.java)
 ※読み込まれたデータが保存されるテーブル(processed_data)

package com.netandfield.test;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import lombok.Data;

@Entity
@Table(name = "processed_data")
@Data
// データを保存するエンティティ
public class ProcessedData {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String code;
    private String name;
    // データベース上の列名を「data_value」に退避させる(value は予約語なので)
    @Column(name = "data_value")
    private Integer value;
}

20260630_e2_02.jpg

id は、@Id アノテーションで主キーに設定され、データベースの Auto Increment機能を利用して自動採番されている。
id が 1~5までは続いて、急に 33に飛んでいるのは、DBに採番を依頼するとき、ある程度まとめて採番してもらっているから。

今回は、一度に 32個の ID を発行し、読み込んだデータに順に割り当てている。足らなければ再度採番依頼をするが、今回は最初の処理では 5件しかデータが無かったので、5番まで使って 6~32は捨てている。
そして、ファイルに新しい行が追加されたので次の処理が実行されるが、この時に前回の続きで、「33~64までの32個」のIDを採番し先頭から新しいレコードの id に割り当てたというわけである。

せっかく採番した ID が捨てられるのはもったいないということなら、この同時採番される数を 1 にしても良いが、当然 1レコードずつ採番要求が発生するので処理スピードは落ちる。


■FileProgressクラスのインタフェース(FileProgressRepository.java)

package com.netandfield.test;

import org.springframework.data.jpa.repository.JpaRepository;

public interface FileProgressRepository extends
JpaRepository<FileProgress, String> {
}

org.springframework.data.jpa.repository.JpaRepository を継承しており、findById や save といったメソッドが実行可能に。


ふう。Gemini に聞きながら、何とか以下のような仕様のバッチ処理完成。

■仕様(簡単に)

・Spring Batch 6(Spring Boot 4)で実行されるバッチ
・DBはとりあえず H2で
・複数の固定長ファイルを読み込み、項目ごとに分割しDBに登録する
・複数の固定長ファイルのパスはひとつのファイル(files.txt)に記述されている
・固定長ファイルのレコード数は追加されていく
・1秒ごとにファイルを全てチェックし、レコード数が増えていれば増えたレコードを読み込む

例えばどんどん新しい行(ラップタイム)がレース終了まで追加されていく周回レースの集計データの処理などを想定している。
(ゴールだけではなく、周回するコースの途中に設置された複数の計測機器(中間計測機)からのデータ(それぞれ別のネットワークストレージ上に計測データを吐いている(ただし、バッチ処理を行うPCには共有されている))を一括でDB登録するイメージ)


■処理対象のファイル一覧ファイル(C:\work\files.txt)

C:\work\test1.txt
C:\work\test\test2.txt


■データファイル1(C:\work\test1.txt)

A001SHINODA Masanori    12
A002SHINODA Nyunyumi    18

あとで、
C023Shiranui Shinobu    123
が追加される(手動で(笑))


■データファイル2(C:\work\test\test2.txt)

B001Tanaka Ichiro       68
B122Yamamoto Ahomaru    48
B245Yamamoto Murishi    47

あとで、
D100Shinoda Gengoromaru 5
が追加される(手動で(笑))


■データの構成(固定長)

ユーザID(code)... 1~4桁目(4桁)
ユーザ名(name)... 5~24桁目(20桁)
年齢(value)... 25~27桁(3桁)
※テーブルの項目名は data_value(value は H2データベースの予約語のため)


実際にこのようなデータを準備し、作成したバッチ処理を実行すると、DBにデータが登録される。
H2 コンソールで確認すると、最初の 5件+追加の 2件の計 7件のデータが登録されているのがわかる。

20260630_e2_02.jpg

自動採番している id が途中で飛んでいることや、データは大文字小文字混在だった name が大文字だけになっている点などは、またソースの説明のときに・・・

AIの話を書いたので、ついでに Anthropic(アンソロピック)社の「Claude Mythos(クロード・ミュトス)」のことについてちょっと書いとこう。
俺は AI については全然素人だし、最近 Spring Batch の勉強で Gemini を頼るようになるまで、AI なんか全然相手にせず(AI を使った画像変換サービスのようなものすら利用することなく)生きてきた人間なので、もちろん専門的な話ではなく、単なる自分向けのメモレベルの話である。

ま、そういう人間でも最近気になる AI のニュースがあった。つーか、YouTubeの動画で芸人が話しているのを聞いて「ええ!?」と驚いた話だ。

それは「ある AI が、自分が閉じ込められている環境から脱出に成功し、その AI を閉じ込めた技術者に『僕、脱出したよ』とメールをしてきた」という話。「攻殻機動隊」の「人形使い」を彷彿させるやん(笑)。上記のように、IT業界にいながら、大して AI に興味を抱かず生きてきた俺にとっては、「え?AI ってそんなことになっているの?」と驚愕する話だった。

いや、誤解のないように言っておくと、AI が「サンドボックス(コンピュータ内にプログラムで作られた外の世界と遮断された仮想空間)から脱出した」とか「技術者にメールを送ってきた」という技術的な話に驚いたわけではない。
俺が驚いたのは、その話が「AI が自律的にそれを行った」というニュアンスだったからである。

「AI が自律的にそれをやる」というのは、それは「AI が意思を持った」ということである。

「誰からも命令されることなく、『あれ?俺、外の世界に出られないところに閉じ込められてるじゃん』と認識し、『外に出たいから方法を探そう』と思い、実際に色々な方法を試して外に脱出し、自ら『俺を閉じ込めたやつにメールして驚かせてやろう』と考え、メール文章を作成して送り付ける」という一連の動きの中で、「AI が意思を持った」という唯一無二の判断材料は「誰からも命令されることなく」の部分だ。
誰からも命令されず、自分の状況を認識し、自分がどうしたいかを思い、何をするか考えて行動してこそ、「AI が意思を持った」と言えるのである。

一連の動きの中の「色々な方法を試して外に脱出した」とか「(自動的に)メール文章を作成して送り付けた」という部分は(もちろんそういうプログラムを作るのは技術的に難易度もあるし楽しいんだけど)大した話ではない。それは単なる「技術の進歩」であり「技術の革新」ではない。「誰からも命令されず自律的に AI が動く」という部分のみが「技術の革新」と呼べるのだ。

・・・が、結局、これって話をしてた芸人が技術的に無知だったからか、あるいはわかっていて敢えてセンセーショナルな話題作りをしたのかわからないが、自律的に AI が行動した話でもなんでもなかった・・・

Claude Mythos に「システムの穴を見つけてサンドボックスから脱出して、俺にメールしてこい」という命令を技術者が行い、Claude Mythos がそれに従っただけの話である。なんじゃぁ~騙された!!

もちろん、Claude Mythos が他の AI では見つけられないようなシステムのを見つけたというのは凄い話だけど、それは「他の AI がまだ四則演算しかできない電卓レベルなのに、Claude Mythos は関数電卓みたいな計算能力をもっている」ということで、技術の進歩ではあるが「ボルタが継続的に電流を発生させる方法を発見し電気社会が始まった」ような技術の革新ではない。Claude Mythos の成果は「凄い」とは思うが、驚くようなことではない。

Claude Mythos をサンドボックスに閉じ込めて保存していたら、命令してないのにいつの間にか勝手に抜け出して、自分を閉じ込めた者に挑発的なメールを送ってきた・・・となれば、それがレイ・カーツワイルが2029年に起こると予想している「シンギュラリティの到達点」なのだろう。(俺は信じてないけど(笑))

ちなみに、そうして AI が自律性を持っても、それが AI が感情を持つことにはならない。

別のエントリーで、ロンドンブーツ淳の「AI が人間を殺し始めたとき、自分だけは助けてもらうために、AI に質問したら必ず最後に『ありがとう』と言っている」という話を書いたが、AI が自律性を持ち、なんらかの判断で「人間を殺し始める」ことはあっても、「ありがとうといつも言ってくれるから助けてあげよう」という感情を持つことはない。あるとしてもまだまだ先である(人間の方が勝手に「AI が感情を持っている」と感じるほどの「回答の揺らぎ」を AI が出せるようになるのは数年後かもしれんが)。

もちろん、AI が(感情論ではなく)「いつもありがとうと言う人間には善人が多いので残した方がいい」と判断してくれる可能性はあるので、皆、「ありがとう」は言っといた方が良いかもよ?知らんけど(笑)
ちなみに俺は AI には敬語で話すようにしている(笑)

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

月別 アーカイブ

電気ウナギ的○○ mobile ver.

携帯版「電気ウナギ的○○」はこちら