diff --git a/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java b/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java index 079e688..99753c2 100644 --- a/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java +++ b/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java @@ -7,7 +7,7 @@ import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; -import com.spring.infra.batch.AbstractBatchJob; +import com.spring.infra.batch.AbstractBatchTask; import com.spring.infra.batch.BatchJobInfo; import lombok.extern.slf4j.Slf4j; @@ -19,7 +19,7 @@ import lombok.extern.slf4j.Slf4j; jobName = "emailSendJob", cronExpression = "*/5 * * * * ?" ) -public class EmailSendBatch extends AbstractBatchJob { +public class EmailSendBatch extends AbstractBatchTask { @Override protected List createSteps() { diff --git a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java index 9c010c2..3cc5ec8 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java @@ -1,7 +1,5 @@ package com.spring.domain.post.batch; -import java.util.List; - import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; @@ -11,7 +9,7 @@ import org.springframework.transaction.PlatformTransactionManager; import com.spring.domain.post.entity.Post; import com.spring.domain.post.mapper.PostMapper; -import com.spring.infra.batch.AbstractBatchJob; +import com.spring.infra.batch.AbstractBatchTask; import com.spring.infra.batch.BatchJobInfo; import com.spring.infra.db.orm.jpa.SecondaryJpaConfig; @@ -23,10 +21,10 @@ import lombok.extern.slf4j.Slf4j; @BatchJobInfo( group = "POST", jobName = "postCreateJob", - cronExpression = "*/5 * * * * ?" + cronExpression = "0/2 * * * * ?" ) @RequiredArgsConstructor -public class PostCreateBatch extends AbstractBatchJob { +public class PostCreateBatch extends AbstractBatchTask { private final PostMapper postMapper; @@ -40,11 +38,9 @@ public class PostCreateBatch extends AbstractBatchJob { @Override protected Tasklet createTasklet() { + log.info(">>>>> PostCreateBatch-createTasklet"); return ((contribution, chunkContext) -> { postMapper.save(Post.builder().title("testTitle").content("testPost").build()); - log.info(">>>>> PostCreateBatchTasklet333333333"); - List list = postMapper.findAll(); - list.forEach(item -> log.info(item.getContent())); return RepeatStatus.FINISHED; }); } diff --git a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java new file mode 100644 index 0000000..3ccf726 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java @@ -0,0 +1,138 @@ +package com.spring.domain.post.batch; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.EntityManagerFactory; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.job.flow.FlowExecutionStatus; +import org.springframework.batch.core.job.flow.JobExecutionDecider; +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.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +import com.spring.domain.post.entity.Post; +import com.spring.domain.post.entity.PostBackUp; +import com.spring.domain.post.repository.PostBackUpRepository; +import com.spring.domain.post.repository.PostRepository; +import com.spring.infra.db.orm.jpa.SecondaryJpaConfig; +import com.spring.infra.quartz.QuartzJob; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +@RequiredArgsConstructor +public class PostCreateBatchChunk { + + private final JobRepository jobRepository; + private final @Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager; + private final @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY) EntityManagerFactory entityManagerFactory; + private final PostRepository postRepository; + private final PostBackUpRepository postBackUpRepository; + + private List list = new ArrayList<>(); + + @QuartzJob(group = "POST", jobName = "testPostJob", cronExpression = "0/5 * * * * ?") + @Bean + Job testPostJob() { + return new JobBuilder("testPostJob") + .repository(jobRepository) + .incrementer(new RunIdIncrementer()) + .start(readListStep()) + .next(decider()) + .from(decider()).on("PROCESS").to(processStep()) + .from(decider()).on("TERMINATE").to(terminateStep()) + .end() + .build(); + } + + @Bean + JobExecutionDecider decider() { + return (JobExecution jobExecution, StepExecution stepExecution) -> + !list.isEmpty() ? new FlowExecutionStatus("PROCESS") : new FlowExecutionStatus("TERMINATE"); + } + + private Step readListStep() { + log.info(">>>>> readListStep"); + return new StepBuilder("readListStep") + .repository(jobRepository) + .transactionManager(transactionManager) + .tasklet(readListTasklet()) + .build(); + } + + private Tasklet readListTasklet() { + log.info(">>>>> readListTasklet"); + return (contribution, chunkContext) -> { + list = postRepository.findAll(); + return RepeatStatus.FINISHED; + }; + } + + private Step processStep() { + log.info(">>>>> processStep"); + return new StepBuilder("processStep") + .repository(jobRepository) + .transactionManager(transactionManager) + .chunk(5) + .reader(testReader()) + .processor(testProcessor()) + .writer(testWriter()) + .build(); + } + + private JpaPagingItemReader testReader() { + log.info(">>>>> JpaPagingItemReader"); + return new JpaPagingItemReaderBuilder() + .name("testReader") + .entityManagerFactory(entityManagerFactory) + .pageSize(5) + .queryString("select p from Post p") + .build(); + } + + private ItemProcessor testProcessor() { + return post -> + PostBackUp.builder().postId(post.getPostId()).content(post.getContent()).title(post.getTitle()).build(); + } + + private ItemWriter testWriter() { + log.info(">>>>> testWriter"); + return postBackUpRepository::saveAll; + } + + private Step terminateStep() { + log.info(">>>>> terminateStep"); + return new StepBuilder("terminateStep") + .repository(jobRepository) + .transactionManager(transactionManager) + .tasklet(terminateTasklet()) + .build(); + } + + private Tasklet terminateTasklet() { + log.info(">>>>> terminateTasklet"); + return (contribution, chunkContext) -> { + log.error("List Read Error : List is null"); + return RepeatStatus.FINISHED; + }; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java b/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java index 719a20a..614a44b 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/entity/Post.java @@ -12,12 +12,13 @@ import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @DatabaseSelector(SecondaryDataSourceConfig.DATABASE) @Entity @Table(name = "APP_POST") @Getter -@Builder +@NoArgsConstructor public class Post { @Id @@ -31,4 +32,11 @@ public class Post { @Column(name = "CONTENT", nullable = false, length = 2000) private String content; + @Builder + public Post(Long postId, String title, String content) { + this.postId = postId; + this.title = title; + this.content = content; + } + } diff --git a/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java b/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java new file mode 100644 index 0000000..989a5b0 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/post/entity/PostBackUp.java @@ -0,0 +1,39 @@ +package com.spring.domain.post.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import com.spring.infra.db.SecondaryDataSourceConfig; +import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +@Entity +@Table(name = "APP_POST_BACKUP") +@Getter +@NoArgsConstructor +public class PostBackUp { + + @Id + @Column(name = "POST_ID", nullable = false) + private Long postId; + + @Column(name = "TITLE", nullable = false, length = 100) + private String title; + + @Column(name = "CONTENT", nullable = false, length = 2000) + private String content; + + @Builder + public PostBackUp(Long postId, String title, String content) { + this.postId = postId; + this.title = title; + this.content = content; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java new file mode 100644 index 0000000..b2189b3 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/post/repository/PostBackUpRepository.java @@ -0,0 +1,12 @@ +package com.spring.domain.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.spring.domain.post.entity.PostBackUp; +import com.spring.infra.db.SecondaryDataSourceConfig; +import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; + +@DatabaseSelector(SecondaryDataSourceConfig.DATABASE) +public interface PostBackUpRepository extends JpaRepository { + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/post/service/PostCreateService.java b/batch-quartz/src/main/java/com/spring/domain/post/service/PostCreateService.java new file mode 100644 index 0000000..fff5dcb --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/post/service/PostCreateService.java @@ -0,0 +1,29 @@ +package com.spring.domain.post.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.spring.domain.post.entity.Post; +import com.spring.domain.post.mapper.PostMapper; +import com.spring.infra.db.orm.jpa.SecondaryJpaConfig; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PostCreateService { + + private final PostMapper postMapper; + + @Transactional(SecondaryJpaConfig.TRANSACTION_MANAGER) + public void save() { + postMapper.save(Post.builder().title("testTitle").content("testPost").build()); + List list = postMapper.findAll(); + list.forEach(item -> log.info(item.getContent())); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchJob.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java similarity index 93% rename from batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchJob.java rename to batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java index 3115651..aacba9a 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchJob.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java @@ -6,6 +6,7 @@ import java.util.List; 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.batch.core.step.tasklet.Tasklet; @@ -31,7 +32,7 @@ import org.springframework.transaction.PlatformTransactionManager; * @version 1.0 */ @Configuration -public abstract class AbstractBatchJob implements ApplicationContextAware { +public abstract class AbstractBatchTask implements ApplicationContextAware { private final BatchJobInfo batchJobInfo; private ApplicationContext applicationContext; @@ -44,7 +45,7 @@ public abstract class AbstractBatchJob implements ApplicationContextAware { * BatchJobInfo 어노테이션을 찾고, 없으면 예외를 발생시킵니다. *

*/ - protected AbstractBatchJob() { + protected AbstractBatchTask() { this.batchJobInfo = AnnotationUtils.findAnnotation(getClass(), BatchJobInfo.class); if (this.batchJobInfo == null) { throw new IllegalStateException("BatchJobInfo annotation is missing"); @@ -104,7 +105,10 @@ public abstract class AbstractBatchJob implements ApplicationContextAware { if (steps.isEmpty()) { throw new IllegalStateException("No steps defined for job: " + jobName()); } - var jobBuilder = new JobBuilder(jobName()).repository(jobRepository).start(steps.get(0)); + var jobBuilder = new JobBuilder(jobName()) + .incrementer(new RunIdIncrementer()) + .repository(jobRepository) + .start(steps.get(0)); for (int i = 1; i < steps.size(); i++) { jobBuilder = jobBuilder.next(steps.get(i)); } diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScanner.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/EntityScanner.java similarity index 98% rename from batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScanner.java rename to batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/EntityScanner.java index ca04a13..0208fa3 100644 --- a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/util/EntityScanner.java +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/EntityScanner.java @@ -1,4 +1,4 @@ -package com.spring.infra.db.orm.jpa.util; +package com.spring.infra.db.orm.jpa; import javax.persistence.Entity; diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java index 7ee587b..d1126cd 100644 --- a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/PrimaryJpaConfig.java @@ -24,7 +24,6 @@ import org.springframework.transaction.PlatformTransactionManager; import com.spring.infra.db.PrimaryDataSourceConfig; import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; -import com.spring.infra.db.orm.jpa.util.EntityScanner; import lombok.RequiredArgsConstructor; @@ -52,8 +51,8 @@ import lombok.RequiredArgsConstructor; public class PrimaryJpaConfig { public static final String TRANSACTION_MANAGER = "primaryTransactionManager"; + public static final String ENTITY_MANAGER_FACTORY = "primaryEntityManagerFactory"; private static final String BASE_PACKAGE = "com.spring.domain"; - private static final String ENTITY_MANAGER_FACTORY = "primaryEntityManagerFactory"; private static final String PERSISTENCE_UNIT = "primaryPersistenceUnit"; private final JpaProperties jpaProperties; diff --git a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java index 9e211d6..1cf7718 100644 --- a/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java +++ b/batch-quartz/src/main/java/com/spring/infra/db/orm/jpa/SecondaryJpaConfig.java @@ -25,7 +25,6 @@ import org.springframework.transaction.PlatformTransactionManager; import com.spring.infra.db.SecondaryDataSourceConfig; import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector; -import com.spring.infra.db.orm.jpa.util.EntityScanner; import lombok.RequiredArgsConstructor; @@ -53,8 +52,8 @@ import lombok.RequiredArgsConstructor; public class SecondaryJpaConfig { public static final String TRANSACTION_MANAGER = "secondaryTransactionManager"; + public static final String ENTITY_MANAGER_FACTORY = "secondaryEntityManagerFactory"; private static final String BASE_PACKAGE = "com.spring.domain"; - private static final String ENTITY_MANAGER_FACTORY = "secondaryEntityManagerFactory"; private static final String PERSISTENCE_UNIT = "secondaryPersistenceUnit"; private final JpaProperties jpaProperties; diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java index 6eb6f2e..aa1d65a 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobLauncher.java @@ -12,6 +12,7 @@ import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * Quartz 작업을 실행하는 Spring Batch Job 실행기 클래스입니다. @@ -31,13 +32,16 @@ import lombok.RequiredArgsConstructor; * @see JobLauncher * @see JobRegistry */ +@Slf4j @Component @RequiredArgsConstructor public class QuartzJobLauncher extends QuartzJobBean { + + private static final String JOB_PARAMETERS_INSTANCE_KEY = "InstanceId"; + private static final String JOB_PARAMETERS_TIMESTAMP_KEY = "timestamp"; private final JobLauncher jobLauncher; private final JobRegistry jobRegistry; - private String jobName; public void setJobName(String jobName) { @@ -61,11 +65,13 @@ public class QuartzJobLauncher extends QuartzJobBean { try { Job job = jobRegistry.getJob(jobName); JobParameters params = new JobParametersBuilder() - .addString("JobID", String.valueOf(System.currentTimeMillis())) + .addString(JOB_PARAMETERS_INSTANCE_KEY, context.getScheduler().getSchedulerInstanceId()) + .addLong(JOB_PARAMETERS_TIMESTAMP_KEY, System.currentTimeMillis()) .toJobParameters(); jobLauncher.run(job, params); } catch (Exception e) { - throw new JobExecutionException(e); + log.error("job execution exception! - {}", e.getCause()); + throw new JobExecutionException(); } } diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java index a02f725..0f35d30 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java @@ -19,7 +19,7 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; -import com.spring.infra.batch.AbstractBatchJob; +import com.spring.infra.batch.AbstractBatchTask; import lombok.RequiredArgsConstructor; @@ -57,7 +57,7 @@ public class QuartzJobRegistrar implements ApplicationListener batchJobs = applicationContext.getBeansOfType(AbstractBatchJob.class); - for (AbstractBatchJob batchJob : batchJobs.values()) { + public void registerAbstractTasks() { + Map batchJobs = applicationContext.getBeansOfType(AbstractBatchTask.class); + for (AbstractBatchTask batchJob : batchJobs.values()) { JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("jobName", batchJob.jobName()); JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) .withIdentity(batchJob.jobName(), batchJob.group()) .setJobData(jobDataMap) - .storeDurably() + .storeDurably(true) .build(); CronTrigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) diff --git a/batch-quartz/src/main/java/lombok.config b/batch-quartz/src/main/java/lombok.config new file mode 100644 index 0000000..eb6db90 --- /dev/null +++ b/batch-quartz/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier \ No newline at end of file diff --git a/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 50472dd..e0e1d8a 100644 --- a/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/batch-quartz/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -143,5 +143,15 @@ "name": "spring.datasource.secondary.hikari.minimum-idle", "type": "java.lang.String", "description": "A description for 'spring.datasource.secondary.hikari.minimum-idle'" + }, + { + "name": "spring.datasource.primary.hikari.idle-timeout", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.primary.hikari.idle-timeout'" + }, + { + "name": "spring.datasource.secondary.hikari.idle-timeout", + "type": "java.lang.String", + "description": "A description for 'spring.datasource.secondary.hikari.idle-timeout'" } ]} \ No newline at end of file diff --git a/batch-quartz/src/main/resources/application.yml b/batch-quartz/src/main/resources/application.yml index 47fc998..702a3fe 100644 --- a/batch-quartz/src/main/resources/application.yml +++ b/batch-quartz/src/main/resources/application.yml @@ -14,6 +14,7 @@ spring: pool-name: HikariPool-1 maximum-pool-size: 10 minimum-idle: 5 + idle-timeout: 60000 secondary: driver-class-name: org.h2.Driver url: 'jdbc:h2:mem:mob' @@ -23,6 +24,7 @@ spring: pool-name: HikariPool-2 maximum-pool-size: 10 minimum-idle: 5 + idle-timeout: 60000 # sql: # init: # mode: always