1. Tasklet만으로는 beforeStep이나 afterStep을 트리거하지 못한다.
- Tasklet을 구현하고 StepBuilder에서 tasklet() 메소드를 등록해주는 것만으로는 @BeforeStep이나 @AfterStep을 사용할 수 없다.
- Tasklet에서 Step의 생명주기에 관여하고 싶다면 추가적인 작업이 필요하다는 말이다.
@Configuration
public class MemberJobConfig {
@Bean("memberJob")
public Job memberJob(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new JobBuilder("memberJob", jobRepository)
.start(memberStep(jobRepository, transactionManager))
.build();
}
@Bean("memberStep")
public Step memberStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("memberStep", jobRepository)
.tasklet(sendingItemScheduleAlarmTasklet(), transactionManager)
.build();
}
@Bean("memberTasklet")
public Tasklet memberTasklet() {
return new Tasklet() {
@Override
public RepeatStatus execute(
StepContribution contribution, ChunkContext chunkContext) throws Exception {/** 로직 */}
@BeforeStep
public void beforeStep(StepExecution stepExecution) {/** 실행되지 않는다. */}
@AfterStep
public ExitStatus afterStep() {/** 실행되지 않는다. */}
};
}
}
2. Tasklet이 Step의 생명주기에 관여하는 방법
2-1. StepExecutionListener를 구현한다.
- Tasklet과 함께 StepExecutionListener를 구현해주면 된다.
- beforeStep() 메소드와 afterStep() 메소드를 오버라이드를 해서 작성하면 된다.
@Component
public class MemberTasklet implements Tasklet, StepExecutionListener {
@Override
public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext)
throws Exception {
return RepeatStatus.FINISHED;
}
@Override
public void beforeStep(final StepExecution stepExecution) {
// 실행
}
@Override
public ExitStatus afterStep() {
// 실행
}
}
- 아래와 같이 tasklet() 메소드를 사용해 등록만 해줘도 Spring Batch 내부적으로 Tasklet으로 등록한 StepExecutionListener 구현체의 beforeStep(), afterStep() 메소드를 실행해준다.
@Bean("memberStep")
public Step memberStep(
final JobRepository jobRepository, final PlatformTransactionManager transactionManager) {
return new StepBuilder("myStep", jobRepository)
.tasklet(memberTasklet, transactionManager)
.build();
}
- 단, StepExecutionListener을 구현해도 @BeforeStep, @AfterStep이 트리거되지 않는다는 사실은 주의하자.
@Component
public class MemberTasklet implements Tasklet, StepExecutionListener {
@Override
public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext)
throws Exception {/** 로직 */}
@BeforeStep
public void findMember(final StepExecution stepExecution) {/** 실행되지 않는다! */}
@AfterStep
public ExitStatus writeMember() {/** 실행되지 않는다! */}
}
2-2. Tasklet을 StepListener로 등록한다.
- 아래와 같이 StepBuilder의 listener() 메소드를 사용해 Tasklet를 리스너로 등록해주면 @BeforeStep과 @AfterStep이 트리거된다.
@Bean("memberStep")
public Step memberStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("memberStep", jobRepository)
.tasklet(memberTasklet(), transactionManager)
.listener(memberTasklet()) // 리스너로 등록해줘도 트리거된다!!
.build();
}
2-3. 어떻게 동작하길래 리스너로 등록해도 사용이 가능한 걸까?
- 위와 같이 StepBuilder에서 listener() 메소드를 호출하면 AbstractTaskletStepBuilder에서 super.listener(listener)를 호출해 리스너를 추가해준다.
public abstract class AbstractTaskletStepBuilder<B extends AbstractTaskletStepBuilder<B>> extends StepBuilderHelper<B> {
...
public B listener(Object listener) {
super.listener(listener);
Set<Method> chunkListenerMethods = new HashSet<>();
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeChunk.class));
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunk.class));
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunkError.class));
if (chunkListenerMethods.size() > 0) {
StepListenerFactoryBean factory = new StepListenerFactoryBean();
factory.setDelegate(listener);
this.listener((ChunkListener) factory.getObject());
}
return self();
}
}
- StepBuilderHelper에서는 @BeforeStep과 @AfterStep 어노테이션이 달려있는 메소드를 탐색해 StepListenerFactoryBean이라는 객체를 사용해 프록시로 감싸준다.
public abstract class StepBuilderHelper<B extends StepBuilderHelper<B>> {
...
public B listener(Object listener) {
Set<Method> stepExecutionListenerMethods = new HashSet<>();
stepExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeStep.class));
stepExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterStep.class));
if (stepExecutionListenerMethods.size() > 0) {
StepListenerFactoryBean factory = new StepListenerFactoryBean();
factory.setDelegate(listener);
properties.addStepExecutionListener((StepExecutionListener) factory.getObject());
}
return self();
}
...
}
- getObject()를 실행할 때 nvokers에는 현재 리스너를 실행시키고 있는 컨텍스트에 대한 정보와 내가 @BeforeStep나 @AfterStep을 달았던 메소드 이름이 담긴다.
- 나중에 StepListener의 beforeStep이나 afterStep이 실행될 때, 프록시를 통해 @BeforeStep나 @AfterStep가 달려있는 메소드를 실행해준다.
public abstract class AbstractListenerFactoryBean<T> implements FactoryBean<Object>, InitializingBean {
@Override
public Object getObject() {
...
Set<Class<?>> listenerInterfaces = new HashSet<>();
Map<String, Set<MethodInvoker>> invokerMap = new HashMap<>();
boolean synthetic = false;
for (Entry<String, String> entry : metaDataMap.entrySet()) {
...
if (metaData.getAnnotation() != null) {
invoker = getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate, metaData.getParamTypes());
if (invoker != null) {
invokers.add(invoker);
synthetic = true;
}
}
if (!invokers.isEmpty()) {
invokerMap.put(metaData.getMethodName(), invokers);
listenerInterfaces.add(metaData.getListenerInterface());
}
}
...
proxyFactory.setInterfaces(listenerInterfaces.toArray(a));
proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new MethodInvokerMethodInterceptor(invokerMap, ordered)));
return proxyFactory.getProxy();
}
}
- AbstractStep에서 CompositeStepExecutionListener를 사용해 리스너를 등록해준다.
public class CompositeStepExecutionListener implements StepExecutionListener {
private final OrderedComposite<StepExecutionListener> list = new OrderedComposite<>();
public void register(StepExecutionListener stepExecutionListener) {
list.add(stepExecutionListener);
}
protected StepExecutionListener getCompositeListener() {
return stepExecutionListener;
}
...
}
- 이후 Step이 실행될 때, AbstractStep에서 CompositeStepExecutionListener을 가져와 beforeStep과 afterStep을 실행한다.
public abstract class AbstractStep implements Step, InitializingBean, BeanNameAware {
@Override
public final void execute(StepExecution stepExecution)
throws JobInterruptedException, UnexpectedJobExecutionException {
...
getCompositeListener().beforeStep(stepExecution);
...
exitStatus = exitStatus.and(getCompositeListener().afterStep(stepExecution));
...
}
}
- CompositeStepExecutionListener는 Step에 등록된 모든 StepExecutionListener의 beforeStep와 afterStep을 실행하게 된다. 위에서 설명했다시피 프록시 객체를 통해 @BeforeStep과 @AfterStep이 트리거되어 Tasklet에 구현한 메소드가 실행되는 것이다.
ItemReader, ItemWriter, ItemProcessor
- ItemReader, ItemProcessor, ItemWriter는 따로 리스너로 등록하지 않아도 @BeforeStep나 @AfterStep을 사용할 수 있다.
@Bean("memberStep")
public Step memberStep(
JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("memberStep", jobRepository)
.<Member, Member>chunk(chunkSize, transactionManager)
.reader(memberReader())
.writer(memberWriter())
.build();
}
- StepBuilder(정확히는 SimpleStepBuilder)에서 build()를 실행할 때 registerAsStreamsAndListeners() 메소드를 사용해 ItemReader, ItemProcessor, ItemWriter를 리스너로 자동 등록하기 때문이다.
@Override
public TaskletStep build() {
registerStepListenerAsItemListener();
registerAsStreamsAndListeners(reader, processor, writer);
return super.build();
}