内容
Spring Batch5で指定フォルダ上に存在する複数ファイルの値を読み込みつつ、
ファイル名を取得するという内容です。
ファイル構成
コード
\demo\src\main\java\com\example\demo\
-
chunk\
- CsvProcessor.java
- CsvReader.java
- CustomFlatFileItemReader.java
-
model\
- User.java
-
BatchConfig.java
-
DemoApplication.java
リソース
\demo\src\main\resources\
- csv\
- female.csv
- male.csv
- application.properties
コード以外のファイル
csv
読み込み対象となるcsvファイルです。
id,name
2,hanako
3,hana
id,name
1,taro
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-batch'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
コード
Model
csvファイルの値に紐づいたフィールド + ファイル名用のフィールドを用意します。
package com.example.demo.model;
import lombok.Data;
@Data
public class User {
private long id;
private String name;
private String fileName;
}
セッター機能を持たせておかないとread処理のマッピング時にエラーになります。
上記だと@Dataによりセッターが自動生成されています。
Caused by: java.lang.IllegalStateException: Method not found: com.example.demo.model.User.()
Reader
package com.example.demo.chunk;
import com.example.demo.model.User;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.MultiResourceItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class CsvReader {
// csvの一覧を取得
private Resource[] getCsvFiles() throws IOException {
return new PathMatchingResourcePatternResolver().getResources("classpath:csv/*.csv");
}
@StepScope
public MultiResourceItemReader<User> read() throws IOException {
// 複数ファイル読み込み
final MultiResourceItemReader<User> multiReader = new MultiResourceItemReader<>();
multiReader.setResources(getCsvFiles());
multiReader.setDelegate(singleFileReader());
return multiReader;
}
private FlatFileItemReader<User> singleFileReader() {
// ファイル読み込み マッピング
final CustomFlatFileItemReader<User> reader = new CustomFlatFileItemReader<>();
reader.setName("csvReader");
reader.setLinesToSkip(1);
reader.setEncoding(StandardCharsets.UTF_8.name());
reader.setLineMapper(new DefaultLineMapper<User>() {
{
setLineTokenizer(new DelimitedLineTokenizer() {{
setNames("id", "name");
}});
setFieldSetMapper(new BeanWrapperFieldSetMapper<User>() {
{
setTargetType(User.class);
}
});
}
});
return reader;
}
}
MultiResourceItemReaderにより複数ファイルを取り込むことができます。
setResourcesにファイル一覧, setDelegateに単体ファイルの処理をセットします。
singleFileReaderにはリソースのセット処理は不要です。
単体ファイルの処理のみの場合は必要ですが、MultiResourceItemReaderのsetResourcesでリソースを渡すことで、Spring Batchがよしなにやってくれるみたいです。
また、singleFileReaderにはFlatFileItemReaderBuilderではなくCustomFlatFileItemReaderを使用しています。ファイルに記載されている値のみをセットするだけであればFlatFileItemReaderBuilderを使用し、ファイル外の値をセットするのであればFlatFileItemReaderBuilderを使いつつ、fieldSetMapperで下記のようにすればOKです。
private FlatFileItemReader<User> singleFileReader() {
final String[] nameArray = new String[]{"id", "name"};
return new FlatFileItemReaderBuilder<User>()
.name("csvReader")
.linesToSkip(1)
.encoding(StandardCharsets.UTF_8.name())
.delimited()
.names(nameArray)
.fieldSetMapper(new CustomFieldSetMapper())
.build();
}
private static class CustomFieldSetMapper implements FieldSetMapper<User> {
@Override
public User mapFieldSet(final FieldSet fieldSet) {
final User user = new User();
user.setId(fieldSet.readLong("id"));
user.setName(fieldSet.readString("name"));
user.setFileName("test"); // ファイルにない項目を追加
return user;
}
}
今回、CustomFlatFileItemReaderを使用している経緯としてはいろいろ苦戦した結果こうなってます。モンハンのプレイ時間削って調査しました。ほんとはモンスター調査のほうがしたかった。
package com.example.demo.chunk;
import com.example.demo.model.User;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.core.io.Resource;
import java.lang.reflect.Field;
import java.util.Objects;
public class CustomFlatFileItemReader<T extends User> extends FlatFileItemReader<User> {
private String fileName;
@Override
public void open(final ExecutionContext executionContext) {
super.open(executionContext);
fileName = Objects.nonNull(getCurrentResource()) ? getCurrentResource().getFilename() : null;
}
@Override
public User read() throws Exception {
final User item = this.doRead();
if (Objects.isNull(item)) return null;
item.setFileName(fileName); // ファイル読み込み値以外をセット
return item;
}
private Resource getCurrentResource() {
try {
final Field resourceField = FlatFileItemReader.class.getDeclaredField("resource");
// privateフィールドにアクセスする
// VMでリフレクションの使用が禁止されている場合、SecurityExceptionを投げる
resourceField.setAccessible(true);
return (Resource) resourceField.get(this);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("リソースへのアクセスに失敗", e);
}
}
}
FlatFileItemReaderを継承したクラスを使用しています。
MultiResourceItemReaderがリソース一覧(ファイル一覧)を渡していましたが、FlatFileItemReader(継承なし)からは何が渡されたのか見れそうにありませんでした。
見た感じリソースはprivateかつセッターのみ。
そのため、渡されたリソースを確認するための機能をCustomFlatFileItemReaderにまとめています。openにより、ファイルが開かれたらgetCurrentResourceが実行されます。
下記処理はFlatFileItemReaderにセットされたリソースを取得するための処理です。
final Field resourceField = FlatFileItemReader.class.getDeclaredField("resource");
resourceField.setAccessible(true);
return (Resource) resourceField.get(this);
リソースはprivateで、ゲッターもありませんでしたね。
上記はスコープを無視して値を無理やり取得するコードです。(リフレクションと呼ばれるもの)処理の切り分けとかスコープとか気にする人ならこれでスタン値溜まりそう。
Processor
読み込んだUser情報を出力するだけです。
package com.example.demo.chunk;
import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.stereotype.Component;
@Component
@StepScope
@Slf4j
public class CsvProcessor implements ItemProcessor<User, User> {
@Override
public User process(final User item) throws Exception {
try {
System.out.println("demo");
System.out.println(item.toString());
} catch (Exception e) {
log.warn(e.getMessage(), e);
return null;
}
return item;
}
}
BatchConfig
Step、Jobを実行するだけです。
package com.example.demo;
import com.example.demo.chunk.CsvProcessor;
import com.example.demo.chunk.CsvReader;
import com.example.demo.model.User;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import java.io.IOException;
import java.io.PrintStream;
@Configuration
public class BatchConfig {
@Autowired
private CsvReader reader;
@Autowired
private CsvProcessor processor;
private Step demoStep(final JobRepository jobRepository, final PlatformTransactionManager transactionManager) throws IOException {
return new StepBuilder("demoStep", jobRepository)
.<User, User>chunk(10, transactionManager)
.reader(reader.read())
.processor(processor)
.writer(items -> {}) // 何もしない。
.build();
}
@Bean
public Job demoJob(final JobRepository jobRepository, final PlatformTransactionManager transactionManager) throws Exception {
System.setOut(new PrintStream(System.out, true, "UTF-8"));
return new JobBuilder("demoJob", jobRepository)
.incrementer(new RunIdIncrementer())
.start(demoStep(jobRepository, transactionManager))
.build();
}
}
Main
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
実行結果
20:02:07: 'bootRun' を実行中...
Starting Gradle Daemon...
Gradle Daemon started in 1 s 818 ms
> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :resolveMainClassName UP-TO-DATE
> Task :bootRun
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.0)
2025-03-02T20:02:30.203+09:00 INFO 24844 --- [demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 23.0.1 with PID 24844 (\demo\build\classes\java\main started by ryuya in \demo)
2025-03-02T20:02:30.205+09:00 INFO 24844 --- [demo] [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2025-03-02T20:02:30.958+09:00 INFO 24844 --- [demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2025-03-02T20:02:31.116+09:00 INFO 24844 --- [demo] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:1ffc8927-57c1-4981-866b-5d128def851a user=SA
2025-03-02T20:02:31.117+09:00 INFO 24844 --- [demo] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2025-03-02T20:02:31.349+09:00 INFO 24844 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.456 seconds (process running for 1.727)
2025-03-02T20:02:31.353+09:00 INFO 24844 --- [demo] [ main] o.s.b.a.b.JobLauncherApplicationRunner : Running default command line with: []
2025-03-02T20:02:31.387+09:00 INFO 24844 --- [demo] [ main] o.s.b.c.l.s.TaskExecutorJobLauncher : Job: [SimpleJob: [name=demoJob]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]
2025-03-02T20:02:31.409+09:00 INFO 24844 --- [demo] [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [demoStep]
demo
User(id=2, name=hanako, fileName=female.csv)
demo
User(id=3, name=hana, fileName=female.csv)
demo
User(id=1, name=taro, fileName=male.csv)
2025-03-02T20:02:31.460+09:00 INFO 24844 --- [demo] [ main] o.s.batch.core.step.AbstractStep : Step: [demoStep] executed in 51ms
2025-03-02T20:02:31.466+09:00 INFO 24844 --- [demo] [ main] o.s.b.c.l.s.TaskExecutorJobLauncher : Job: [SimpleJob: [name=demoJob]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 69ms
2025-03-02T20:02:31.469+09:00 INFO 24844 --- [demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2025-03-02T20:02:31.489+09:00 INFO 24844 --- [demo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 13s
4 actionable tasks: 1 executed, 3 up-to-date
20:02:31: 'bootRun' の実行を完了しました。
ちゃんとファイル名が取得できているのが確認できました。
ファイル順は今回は無視します。
おわり
今作のモンスターはPCスタン攻撃も持ってるのか...
参考
ファイル複数読み込みやファイルの読み込み順について
リフレクションについて