728x90
Spring Batch - Reference Documentation
If a group of Steps share similar configurations, then it may be helpful to define a “parent” Step from which the concrete Steps may inherit properties. Similar to class inheritance in Java, the “child” Step combines its elements and attributes wit
docs.spring.io
초기 세팅
plyers.csv
ID,lastName,firstName,position,birthYear,debutYear
"AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996",
"AbduRa00,Abdullah,Rabih,rb,1975,1999",
"AberWa00,Abercrombie,Walter,rb,1959,1982",
"AbraDa00,Abramowicz,Danny,wr,1945,1967",
"AdamBo00,Adams,Bob,te,1946,1969",
"AdamCh00,Adams,Charlie,wr,1979,2003"
- csv 파일로 ", "를 기준으로 데이터를 구분한다.
- 첫 번째 줄 데이터는 어떤 데이터인지를 구분해 주는 row로 실제 데이터는 아니다.
File 읽어서 Print 하기
Player(dto 생성)
@Data
public class Player {
private String id;
private String lastName;
private String firstName;
private String position;
private int birthYear;
private int debutYear;
}
FileDataReadWriteConfig
/**
* desc: 파일 읽고 쓰기
* run: --spring.batch.job.name=fileReadWriteStep
*/
@Configuration
@RequiredArgsConstructor
public class FileDataReadWriteConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job fileDataReadWriteJob(Step fileReadWriteStep) {
return jobBuilderFactory.get("fileDataReadWriteJob")
.incrementer(new RunIdIncrementer())
.start(fileReadWriteStep)
.build();
}
@Bean
@JobScope
public Step fileReadWriteStep(ItemReader playerItemReader) {
return stepBuilderFactory.get("fileReadWriteStep")
.<Player, Player>chunk(5) //5개의 크기로 읽고 쓰겠다.
.reader(playerItemReader)
.writer(
new ItemWriter() {
@Override
public void write(List items) throws Exception {
items.forEach(System.out::println);
}
}
)
.build();
}
@Bean
@StepScope
public FlatFileItemReader<Player> playerItemReader(){
return new FlatFileItemReaderBuilder<Player>()
.name("playerItemReader")
.resource(new FileSystemResource("resource.Players.csv"))
.lineTokenizer(new DelimitedLineTokenizer())
.fieldSetMapper(new PlayerFieldSetMapper()) //Play - File mapping
.linesToSkip(1) //1번쨰 줄 스킵
.build();
}
}
PlayerFieldSetMapper
public class PlayerFieldSetMapper implements FieldSetMapper<Player> {
@Override
public Player mapFieldSet(FieldSet fieldSet) throws BindException {
Player player = new Player();
player.setId(fieldSet.readString(0));
player.setLastName(fieldSet.readString(1));
player.setFirstName(fieldSet.readString(2));
player.setPosition(fieldSet.readString(3));
player.setBirthYear(fieldSet.readInt(4));
player.setDebutYear(fieldSet.readInt(5));
return player;
}
}
- 옮길 객체를 만들어 준다(player)
- FileDataReadWriteConfig에서 ItemReader를 생성할 때, FlatFileItemReaderBuilder를 통해 생성한다.
- 이름, 리소스 위치(파일 위치) 적어준다.
- lineTokenizer를 통해 줄마다 자르고, DelimitedLineTokenizer를 통해 ", "마다 구분해 준다.
- fieldSetMapper에 변환시켜 줄 Mapper(PlayerFieldSetMapper)를 적어준다.
- 첫 번째 줄은 실제 데이터가 아니므로 lineToSkip()을 통해 생략한다.
- PlayerFieldSetMapper은 FieldSetMapper를 구현하여, mapFieldSet을 구현한다.
- Player객체를 생성하여, 여기에 파일에서 읽은 값을 전달해서 세팅한다.
- ItemWriter에서는 단순하게 Console에 Print 하는 역할만 한다.
File 읽어서 Print 하기
FileDataReadWriteConfig
/**
* desc: 파일 읽고 쓰기 run: --spring.batch.job.name=fileDataReadWriteJob
*/
@Configuration
@RequiredArgsConstructor
public class FileDataReadWriteConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job fileDataReadWriteJob(Step fileReadWriteStep) {
return jobBuilderFactory.get("fileDataReadWriteJob")
.incrementer(new RunIdIncrementer())
.start(fileReadWriteStep)
.build();
}
@Bean
@JobScope
public Step fileReadWriteStep(ItemReader playerItemReader, ItemProcessor playerItemProcessor, ItemWriter playerItemWriter) {
return stepBuilderFactory.get("fileReadWriteStep")
.<Player, PlayerYears>chunk(5) //5개의 크기로 읽고 쓰겠다.
.reader(playerItemReader)
/*
.writer(
new ItemWriter() {
@Override
public void write(List items) throws Exception {
items.forEach(System.out::println);
}
}
)
*/
.processor(playerItemProcessor) //중간 작업
.writer(playerItemWriter)
.build();
}
@Bean
@StepScope
public FlatFileItemReader<Player> playerItemReader() {
return new FlatFileItemReaderBuilder<Player>()
.name("playerItemReader")
.resource(new FileSystemResource("Players.csv"))
.lineTokenizer(new DelimitedLineTokenizer())
.fieldSetMapper(new PlayerFieldSetMapper()) //Play - File mapping
.linesToSkip(1) //1번쨰 줄 스킵
.build();
}
@Bean
@StepScope
public ItemProcessor<Player, PlayerYears> playerItemProcessor() {
return new ItemProcessor<Player, PlayerYears>() {
@Override
public PlayerYears process(Player item) throws Exception {
return new PlayerYears(item);
}
};
}
@Bean
@StepScope
public FlatFileItemWriter<PlayerYears> playerItemWriter() {
//오류 발생: : org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to
//원래라면 ItemWriter는 ItemStream으로 리턴되어도 상관없지만, 위에 @Bean과 @StepScope를 같이 사용해서 Bean이 proxy로 설정되어 FlatFileItemWriter에서 지원하는 open() 메서드가 동작을 안하게 되어 발생되는 문제.
/**
* 해결방안
* 1. csv파일을 write 해주는 객체 FlatFileItemWriter로 리턴해야 하는 경우, 똑같이 반환타입을 맞춘다.
* 2. ItemWriter로 반환하려면 @Bean이나 @StepScope 둘 중 하나를 삭제한다. (proxy 로 지정되는 부분을 삭제)
*/
BeanWrapperFieldExtractor<PlayerYears> fieldExtractor = new BeanWrapperFieldExtractor<>();
fieldExtractor.setNames(new String[]{"id", "lastName", "position", "yearsExperience"}); //새로운 파일 만들 조건
fieldExtractor.afterPropertiesSet();
DelimitedLineAggregator<PlayerYears> lineAggregator = new DelimitedLineAggregator<>(); //구분자 추가
lineAggregator.setDelimiter(",");
lineAggregator.setFieldExtractor(fieldExtractor);
FileSystemResource outputResource = new FileSystemResource("players_output.txt");
return new FlatFileItemWriterBuilder<PlayerYears>()
.name("playerItemWriter")
.resource(outputResource)
.lineAggregator(lineAggregator)
.build();
}
}
PlayerYears
@Data
public class PlayerYears {
private String id;
private String lastName;
private String firstName;
private String position;
private int birthYear;
private int debutYear;
private int yearsExperience;
public PlayerYears(Player player) {
this.id = player.getId();
this.lastName = player.getLastName();
this.firstName = player.getFirstName();
this.position = player.getPosition();
this.birthYear = player.getBirthYear();
this.debutYear = player.getDebutYear();
this.yearsExperience = Year.now().getValue() - player.getDebutYear();
}
}
- 실무에서는 Processor 작업이 반드시 필요하기에 해당 작업을 추가했다.
- 여기서는 Player 객체를 PlayerYear 객체로 변환하는 작업을 한다.
- PlayerYear의 yearsExperience(연차) 필드는 현재 년도에 데뷔년도를 뺀다.
- playerItemWriter에서 실제 파일에 넣는 작업을 진행한다.
- BeanWrapperFieldExtractor를 생성해서, 어떤 필드 값들을 파일에 쓸지 작성한다.
- DelimitedLineAggregator를 사용해서 추가할 구분자를 작성한다.
- FileSystemResource를 생성하여, 어느 파일에 쓸지 작성한다.
- FlatFileItemWriterBuilder를 이용해 ItemWriter를 생성한다.
참조 > https://choisblog.tistory.com/m/83
Spring Batch Error : Writer must be open before it can be written to
에러 메시지 : org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to 원인 : ItemWriter 구현시, csv파일을 write하는 작업에서 발생한 에러. @Bean 과 @StepScope으로 ItemWriter을 구현
choisblog.tistory.com
- 실제 실행하게 되면 오류(org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to)가 발생하는데, ItemWriter 구현시, .txt 파일을 write하는 작업에서 발생한 에러이다.
- ItemWriter는 ItemStream으로 리턴되어도 상관없지만, @Bean과 @StepScope를 같이 사용해서 Bean이 Proxy로 설정이 되어FlatFileItemWriter에서 지원하는 open() 메서드가 동작을 안하게 되어 발생되는 문제이다.
- 해결방법으로 FlatFileItemWriter를 반환하도록 하거나, Bean/StepScioe 어노테이션중 하나를 제거하면 해결된다.
728x90
'예제로 배우는 핵심 Spring Batch' 카테고리의 다른 글
| Ch08. 좋은 코드의 기본 테스트 코드 작성하기 (0) | 2023.06.11 |
|---|---|
| Ch07. 여러개의 step 구동 및 실행 상태에 따른 분기처리 (0) | 2023.06.10 |
| Ch05. DB 데이터 이관 하기 (DB 데이터 읽고 쓰기) (0) | 2023.06.07 |
| Ch04. 배치 작업 실행 전, 후 로그 추가를 위한 리스너 (0) | 2023.06.07 |
| Ch03. 배치 실행 시 파라미터 (파일 이름) 받기 및 (csv)검증 (0) | 2023.06.07 |