diff --git a/batch-quartz/pom.xml b/batch-quartz/pom.xml index 6feebbd..64f715a 100644 --- a/batch-quartz/pom.xml +++ b/batch-quartz/pom.xml @@ -20,12 +20,28 @@ 11 + + + + org.springframework.cloud + spring-cloud-dependencies + 2021.0.7 + pom + import + + + + org.projectlombok lombok true + + org.springframework.cloud + spring-cloud-starter + org.springframework.boot spring-boot-configuration-processor 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 964f3ad..4af4bc8 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,14 +7,13 @@ import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; +import com.spring.domain.post.repository.PostRepository; import com.spring.infra.batch.AbstractBatchTask; import com.spring.infra.batch.BatchJobInfo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.spring.domain.post.repository.PostRepository; - @Slf4j @Component @BatchJobInfo( 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 0472fa5..cdf209d 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 @@ -31,9 +31,7 @@ public class PostCreateBatch extends AbstractBatchTask { @Autowired @Override - public void setTransactionManager( - @Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager - ) { + public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) { super.setTransactionManager(transactionManager); } 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 index b610c68..57f4435 100644 --- 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 @@ -13,7 +13,6 @@ 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; @@ -21,15 +20,16 @@ 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.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; 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.batch.AbstractBatchChunk; import com.spring.infra.batch.BatchJobInfo; import com.spring.infra.db.orm.jpa.SecondaryJpaConfig; @@ -37,14 +37,21 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j -@Configuration +@Component +@BatchJobInfo( + group = "${batch-info.post-create-batch.group}", + jobName = "${batch-info.post-create-batch.job-name}", + cronExpression = "${batch-info.post-create-batch.cron-expression}", + description = "${batch-info.post-create-batch.description}" +) @RequiredArgsConstructor -public class PostCreateBatchChunk { +public class PostCreateBatchChunk extends AbstractBatchChunk { - private final JobRepository jobRepository; - - @Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) - private final PlatformTransactionManager transactionManager; + @Autowired + @Override + public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) { + super.setTransactionManager(transactionManager); + } @Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY) private final EntityManagerFactory entityManagerFactory; @@ -54,15 +61,9 @@ public class PostCreateBatchChunk { private List list = new ArrayList<>(); - @Bean - @BatchJobInfo( - group = "${batch-info.post-create-batch.group}", - jobName = "${batch-info.post-create-batch.job-name}", - cronExpression = "${batch-info.post-create-batch.cron-expression}", - description = "${batch-info.post-create-batch.description}" - ) - Job testPostJob() { - return new JobBuilder("testPostJob") + @Override + public Job createJob() { + return new JobBuilder(batchJobInfoData.getJobName()) .repository(jobRepository) .incrementer(new RunIdIncrementer()) .start(readListStep()) @@ -136,5 +137,5 @@ public class PostCreateBatchChunk { return RepeatStatus.FINISHED; }; } - + } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatch.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatch.java new file mode 100644 index 0000000..df0ed7b --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatch.java @@ -0,0 +1,17 @@ +package com.spring.infra.batch; + +import org.springframework.batch.core.Job; + +public interface AbstractBatch { + + void initializeBatchJobInfo(); + + void registerJobBean(); + + Job createJob(); + + default String removeScopedTargetPrefix(String beanName) { + return beanName.startsWith("scopedTarget.") ? beanName.substring("scopedTarget.".length()) : beanName; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchChunk.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchChunk.java new file mode 100644 index 0000000..db2f2b3 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchChunk.java @@ -0,0 +1,96 @@ +package com.spring.infra.batch; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNull; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +public abstract class AbstractBatchChunk implements AbstractBatch, ApplicationContextAware, InitializingBean { + + private final BatchJobInfo batchJobInfo; + private BatchJobInfoService batchJobInfoService; + private ApplicationContext applicationContext; + protected BatchJobInfoData batchJobInfoData; + protected JobRepository jobRepository; + protected PlatformTransactionManager transactionManager; + + /** + * 기본 생성자입니다. + *

+ * BatchJobInfo 어노테이션을 찾고, 없으면 예외를 발생시킵니다. + *

+ */ + protected AbstractBatchChunk() { + this.batchJobInfo = AnnotationUtils.findAnnotation(getClass(), BatchJobInfo.class); + if (this.batchJobInfo == null) { + throw new IllegalStateException("BatchJobInfo annotation is missing"); + } + } + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.batchJobInfoService = applicationContext.getBean(BatchJobInfoService.class); + initializeBatchJobInfo(); + registerJobBean(); + } + + @Override + public void initializeBatchJobInfo() { + String beanName = applicationContext.getBeanNamesForType(this.getClass())[0]; + this.batchJobInfoData = batchJobInfoService.getBatchJobInfo(removeScopedTargetPrefix(beanName)); + } + + /** + * 배치 작업을 Spring의 Bean으로 등록합니다. + */ + @Override + public void registerJobBean() { + var beanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); + var registry = (BeanDefinitionRegistry) beanFactory; + String jobBeanName = batchJobInfoData.getJobName(); + if (!registry.containsBeanDefinition(jobBeanName)) { + var beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Job.class, this::createJob); + registry.registerBeanDefinition(jobBeanName, beanDefinitionBuilder.getBeanDefinition()); + } + } + + /** + * JobRepository를 설정합니다. + * + * @param jobRepository 설정할 JobRepository + */ + @Autowired + public void setJobRepository(JobRepository jobRepository) { + this.jobRepository = jobRepository; + } + + /** + * PlatformTransactionManager를 설정합니다. + * + * @param transactionManager 설정할 PlatformTransactionManager + */ + @Autowired + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public abstract Job createJob(); + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java index bcf59b3..b7d574d 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java @@ -23,8 +23,6 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.NonNull; import org.springframework.transaction.PlatformTransactionManager; -import com.spring.infra.batch.BatchJobInfoBeanPostProcessor.BatchJobInfoData; - /** * 배치 작업을 정의하는 추상 클래스입니다. *

@@ -35,10 +33,10 @@ import com.spring.infra.batch.BatchJobInfoBeanPostProcessor.BatchJobInfoData; * @version 1.0 */ @Configuration -public abstract class AbstractBatchTask implements ApplicationContextAware, InitializingBean { +public abstract class AbstractBatchTask implements AbstractBatch, ApplicationContextAware, InitializingBean { private final BatchJobInfo batchJobInfo; - private BatchJobInfoBeanPostProcessor batchJobInfoProcessor; + private BatchJobInfoService batchJobInfoService; private BatchJobInfoData batchJobInfoData; private ApplicationContext applicationContext; private JobRepository jobRepository; @@ -60,28 +58,33 @@ public abstract class AbstractBatchTask implements ApplicationContextAware, Init @Override public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; + this.batchJobInfoService = applicationContext.getBean(BatchJobInfoService.class); } @Override public void afterPropertiesSet() throws Exception { - this.batchJobInfoProcessor = applicationContext.getBean(BatchJobInfoBeanPostProcessor.class); initializeBatchJobInfo(); registerJobBean(); } - private void initializeBatchJobInfo() { + @Override + public void initializeBatchJobInfo() { String beanName = applicationContext.getBeanNamesForType(this.getClass())[0]; - this.batchJobInfoData = batchJobInfoProcessor.getBatchJobInfo(beanName); + this.batchJobInfoData = batchJobInfoService.getBatchJobInfo(removeScopedTargetPrefix(beanName)); } /** * 배치 작업을 Spring의 Bean으로 등록합니다. */ - private void registerJobBean() { + @Override + public void registerJobBean() { var beanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); var registry = (BeanDefinitionRegistry) beanFactory; - var beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Job.class, this::createJob); - registry.registerBeanDefinition(batchJobInfoData.getJobName(), beanDefinitionBuilder.getBeanDefinition()); + String jobBeanName = batchJobInfoData.getJobName(); + if (!registry.containsBeanDefinition(jobBeanName)) { + var beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Job.class, this::createJob); + registry.registerBeanDefinition(jobBeanName, beanDefinitionBuilder.getBeanDefinition()); + } } /** @@ -110,7 +113,8 @@ public abstract class AbstractBatchTask implements ApplicationContextAware, Init * @return 생성된 Job 객체 * @throws IllegalStateException STEP이 정의되지 않은 경우 예외 발생 */ - private Job createJob() { + @Override + public Job createJob() { List steps = createSteps(); if (steps.isEmpty()) { throw new IllegalStateException("No steps defined for job: " + batchJobInfoData.getJobName()); diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchConfig.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchConfig.java index cbc9958..1b134c3 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/BatchConfig.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchConfig.java @@ -32,9 +32,9 @@ public class BatchConfig { * @param jobRegistry JobRegistry 객체 * @return 설정된 JobRegistryBeanPostProcessor 객체 */ - @Bean + @Bean JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) { - final var processor = new JobRegistryBeanPostProcessor(); + var processor = new JobRegistryBeanPostProcessor(); processor.setJobRegistry(jobRegistry); return processor; } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java index 08c45ca..7dd927d 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java @@ -5,13 +5,16 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.cloud.context.config.annotation.RefreshScope; + /** * 배치 작업에 대한 메타데이터를 정의하는 어노테이션입니다. *

* 이 어노테이션은 배치 작업의 그룹, 이름 및 cron 표현식을 설정하는 데 사용됩니다. *

*/ -@Target({ElementType.TYPE, ElementType.METHOD}) +@RefreshScope +@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface BatchJobInfo { /** diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java index 77358e6..e3a8ded 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java @@ -1,79 +1,60 @@ package com.spring.infra.batch; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - +import org.springframework.batch.core.Job; +import org.springframework.batch.core.configuration.DuplicateJobException; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.configuration.support.ReferenceJobFactory; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; -import org.springframework.core.env.Environment; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; -import org.springframework.util.ReflectionUtils; -import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Component @RequiredArgsConstructor -public class BatchJobInfoBeanPostProcessor implements InstantiationAwareBeanPostProcessor { +public class BatchJobInfoBeanPostProcessor implements BeanPostProcessor { - private final Environment environment; - private final Map batchJobInfoMap = new ConcurrentHashMap<>(); - - private static final String BASE_PACKAGE = "com.spring"; + private final BatchJobInfoService batchJobInfoService; + private final JobRegistry jobRegistry; @Override @Nullable - public Object postProcessBeforeInstantiation(@NonNull Class beanClass, @NonNull String beanName) throws BeansException { - if (beanClass.getPackage().getName().startsWith(BASE_PACKAGE)) { - processAnnotations(beanClass, beanName); - } - return null; + public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + processBatchJobInfoAnnotations(bean, beanName); + return bean; } - private void processAnnotations(Class beanClass, String beanName) { - if (beanClass.isAnnotationPresent(BatchJobInfo.class)) { - BatchJobInfo batchJobInfo = beanClass.getAnnotation(BatchJobInfo.class); - setBatchJobInfoData(batchJobInfo, beanName); - } - - ReflectionUtils.doWithMethods(beanClass, method -> { - if (method.isAnnotationPresent(BatchJobInfo.class)) { - BatchJobInfo batchJobInfo = method.getAnnotation(BatchJobInfo.class); - String jobBeanName = method.getName(); - setBatchJobInfoData(batchJobInfo, jobBeanName); + @Override + @Nullable + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + if (bean instanceof AbstractBatch) { + AbstractBatch abstractBatch = (AbstractBatch) bean; + Job job = abstractBatch.createJob(); + String jobName = job.getName(); + if (!jobRegistry.getJobNames().contains(jobName)) { + try { + jobRegistry.register(new ReferenceJobFactory(job)); + } catch (DuplicateJobException e) { + log.warn("Unexpected DuplicateJobException for job: {}", jobName, e); + } } - }); + } + return bean; } - private void setBatchJobInfoData(BatchJobInfo batchJobInfo, String beanName) { - batchJobInfoMap.put(beanName, processBatchJobInfo(batchJobInfo)); + private void processBatchJobInfoAnnotations(Object bean, String beanName) { + if (bean.getClass().isAnnotationPresent(BatchJobInfo.class)) { + BatchJobInfo batchJobInfo = bean.getClass().getAnnotation(BatchJobInfo.class); + batchJobInfoService.setBatchJobInfoData(removeScopedTargetPrefix(beanName), batchJobInfo); + } } - private BatchJobInfoData processBatchJobInfo(BatchJobInfo batchJobInfo) { - return new BatchJobInfoData( - resolvePlaceHolder(batchJobInfo.group()), - resolvePlaceHolder(batchJobInfo.jobName()), - resolvePlaceHolder(batchJobInfo.cronExpression()), - resolvePlaceHolder(batchJobInfo.description()) - ); + private String removeScopedTargetPrefix(String beanName) { + return beanName.startsWith("scopedTarget.") ? beanName.substring("scopedTarget.".length()) : beanName; } - private String resolvePlaceHolder(String value) { - return environment.resolvePlaceholders(value); - } - - public BatchJobInfoData getBatchJobInfo(String beanName) { - return batchJobInfoMap.get(beanName); - } - - @Getter - @RequiredArgsConstructor - public static class BatchJobInfoData { - private final String jobGroup; - private final String jobName; - private final String cronExpression; - private final String description; - } } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoData.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoData.java new file mode 100644 index 0000000..fece66e --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoData.java @@ -0,0 +1,13 @@ +package com.spring.infra.batch; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class BatchJobInfoData { + private final String jobGroup; + private final String jobName; + private final String cronExpression; + private final String description; +} diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoService.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoService.java new file mode 100644 index 0000000..e455378 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoService.java @@ -0,0 +1,39 @@ +package com.spring.infra.batch; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class BatchJobInfoService { + + private final Environment environment; + private final Map batchJobInfoMap = new ConcurrentHashMap<>(); + + public void setBatchJobInfoData(String beanName, BatchJobInfo batchJobInfo) { + batchJobInfoMap.put(beanName, processBatchJobInfo(batchJobInfo)); + } + + public BatchJobInfoData getBatchJobInfo(String beanName) { + return batchJobInfoMap.get(beanName); + } + + private BatchJobInfoData processBatchJobInfo(BatchJobInfo batchJobInfo) { + return new BatchJobInfoData( + resolvePlaceHolder(batchJobInfo.group()), + resolvePlaceHolder(batchJobInfo.jobName()), + resolvePlaceHolder(batchJobInfo.cronExpression()), + resolvePlaceHolder(batchJobInfo.description()) + ); + } + + private String resolvePlaceHolder(String value) { + return environment.resolvePlaceholders(value); + } + +} 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 a475d80..b4c8021 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 @@ -10,7 +10,6 @@ import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.lang.NonNull; import org.springframework.scheduling.quartz.QuartzJobBean; -import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,7 +33,6 @@ import lombok.extern.slf4j.Slf4j; * @see JobRegistry */ @Slf4j -@Component @RequiredArgsConstructor public class QuartzJobLauncher extends QuartzJobBean { 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 aee3192..3d642c7 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 @@ -2,25 +2,16 @@ package com.spring.infra.quartz; import java.util.Map; -import org.quartz.CronScheduleBuilder; -import org.quartz.CronTrigger; -import org.quartz.JobBuilder; -import org.quartz.JobDataMap; -import org.quartz.JobDetail; -import org.quartz.Scheduler; -import org.quartz.SchedulerException; -import org.quartz.TriggerBuilder; +import org.springframework.aop.support.AopUtils; +import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; -import org.springframework.util.ReflectionUtils; -import com.spring.infra.batch.AbstractBatchTask; -import com.spring.infra.batch.BatchJobInfo; -import com.spring.infra.batch.BatchJobInfoBeanPostProcessor; -import com.spring.infra.batch.BatchJobInfoBeanPostProcessor.BatchJobInfoData; +import com.spring.infra.batch.AbstractBatch; import lombok.RequiredArgsConstructor; @@ -46,9 +37,8 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class QuartzJobRegistrar implements ApplicationListener { - private final Scheduler scheduler; private final ApplicationContext applicationContext; - private final BatchJobInfoBeanPostProcessor batchJobInfoProcessor; + private final QuartzScheduleService quartzScheduleService; /** * 애플리케이션 컨텍스트가 리프레시될 때 호출되는 메소드입니다. @@ -58,96 +48,34 @@ public class QuartzJobRegistrar implements ApplicationListener beanClass = bean.getClass(); - - ReflectionUtils.doWithMethods(beanClass, method -> { - if (method.isAnnotationPresent(BatchJobInfo.class)) { - BatchJobInfoData batchJobInfoData = batchJobInfoProcessor.getBatchJobInfo(beanName); - if (batchJobInfoData != null) { - scheduleQuartzJob(batchJobInfoData); - } - } - }); - } - } - - private void scheduleQuartzJob(BatchJobInfoData batchJobInfoData) { - JobDataMap jobDataMap = new JobDataMap(); - jobDataMap.put("jobName", batchJobInfoData.getJobName()); - - JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) - .withIdentity(batchJobInfoData.getJobName(), batchJobInfoData.getJobGroup()) - .setJobData(jobDataMap) - .storeDurably(true) - .withDescription(batchJobInfoData.getDescription()) - .build(); - - CronTrigger trigger = TriggerBuilder.newTrigger() - .forJob(jobDetail) - .withIdentity(batchJobInfoData.getJobName() + "Trigger", batchJobInfoData.getJobGroup()) - .withSchedule(CronScheduleBuilder.cronSchedule(batchJobInfoData.getCronExpression())) - .withDescription(batchJobInfoData.getDescription()) - .build(); - - try { - scheduler.scheduleJob(jobDetail, trigger); - } catch (SchedulerException e) { - throw new IllegalStateException("Error scheduling Quartz job: " + batchJobInfoData.getJobName(), e); - } } /** * AbstractBatchJob을 상속받은 모든 클래스를 찾아 Quartz 스케줄러에 등록합니다. */ - private void registerAbstractBatchJobs() { - Map batchJobs = applicationContext.getBeansOfType(AbstractBatchTask.class); - for (String beanName : batchJobs.keySet()) { - scheduleJob(batchJobInfoProcessor.getBatchJobInfo(beanName)); + private void registerBatchJobs() { + Map batchJobs = applicationContext.getBeansOfType(AbstractBatch.class); + for (Map.Entry entry : batchJobs.entrySet()) { + AbstractBatch batch = entry.getValue(); + if (!AopUtils.isAopProxy(batch)) { + quartzScheduleService.createJobTrigger(removeScopedTargetPrefix(entry.getKey())); + } } } - private void scheduleJob(BatchJobInfoData batchJobInfoData) { - JobDataMap jobDataMap = new JobDataMap(); - jobDataMap.put("jobName", batchJobInfoData.getJobName()); - - JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) - .withIdentity(batchJobInfoData.getJobName(), batchJobInfoData.getJobGroup()) - .setJobData(jobDataMap) - .storeDurably(true) - .withDescription(batchJobInfoData.getDescription()) - .build(); - - CronTrigger trigger = TriggerBuilder.newTrigger() - .forJob(jobDetail) - .withIdentity(batchJobInfoData.getJobName() + "Trigger", batchJobInfoData.getJobGroup()) - .withSchedule(CronScheduleBuilder.cronSchedule(batchJobInfoData.getCronExpression())) - .withDescription(batchJobInfoData.getDescription()) - .build(); - - try { - scheduler.scheduleJob(jobDetail, trigger); - } catch (SchedulerException e) { - throw new IllegalStateException("Error scheduling AbstractBatchJob: " + batchJobInfoData.getJobName(), e); - } + private String removeScopedTargetPrefix(String beanName) { + return beanName.startsWith("scopedTarget.") ? beanName.substring("scopedTarget.".length()) : beanName; } } diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzScheduleService.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzScheduleService.java new file mode 100644 index 0000000..05929f6 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzScheduleService.java @@ -0,0 +1,60 @@ +package com.spring.infra.quartz; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.springframework.stereotype.Service; + +import com.spring.infra.batch.BatchJobInfoData; +import com.spring.infra.batch.BatchJobInfoService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class QuartzScheduleService { + + private final Scheduler scheduler; + private final BatchJobInfoService batchJobInfoService; + + public void clearJobs() { + try { + scheduler.clear(); + } catch (SchedulerException e) { + throw new IllegalStateException(); + } + } + + public void createJobTrigger(String beanName) { + BatchJobInfoData batchJobInfoData = batchJobInfoService.getBatchJobInfo(beanName); + + JobDataMap jobDataMap = new JobDataMap(); + jobDataMap.put("jobName", batchJobInfoData.getJobName()); + + JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) + .withIdentity(batchJobInfoData.getJobName(), batchJobInfoData.getJobGroup()) + .setJobData(jobDataMap) + .storeDurably(true) + .withDescription(batchJobInfoData.getDescription()) + .build(); + + CronTrigger trigger = TriggerBuilder.newTrigger() + .forJob(jobDetail) + .withIdentity(batchJobInfoData.getJobName() + "Trigger", batchJobInfoData.getJobGroup()) + .withSchedule(CronScheduleBuilder.cronSchedule(batchJobInfoData.getCronExpression())) + .withDescription(batchJobInfoData.getDescription()) + .build(); + + try { + scheduler.scheduleJob(jobDetail, trigger); + } catch (SchedulerException e) { + throw new IllegalStateException("Error scheduling Quartz job: " + batchJobInfoData.getJobName(), e); + } + } + +} 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 3290422..b60146f 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 @@ -163,5 +163,65 @@ "name": "front.base.timeout", "type": "java.lang.String", "description": "A description for 'front.base.timeout'" + }, + { + "name": "batch-info.email-send-batch.group", + "type": "java.lang.String", + "description": "A description for 'batch-info.email-send-batch.group'" + }, + { + "name": "batch-info.email-send-batch.job-name", + "type": "java.lang.String", + "description": "A description for 'batch-info.email-send-batch.job-name'" + }, + { + "name": "batch-info.email-send-batch.cron-expression", + "type": "java.lang.String", + "description": "A description for 'batch-info.email-send-batch.cron-expression'" + }, + { + "name": "batch-info.email-send-batch.description", + "type": "java.lang.String", + "description": "A description for 'batch-info.email-send-batch.description'" + }, + { + "name": "batch-info.post-batch.group", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-batch.group'" + }, + { + "name": "batch-info.post-batch.job-name", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-batch.job-name'" + }, + { + "name": "batch-info.post-batch.cron-expression", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-batch.cron-expression'" + }, + { + "name": "batch-info.post-batch.description", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-batch.description'" + }, + { + "name": "batch-info.post-create-batch.group", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-create-batch.group'" + }, + { + "name": "batch-info.post-create-batch.job-name", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-create-batch.job-name'" + }, + { + "name": "batch-info.post-create-batch.cron-expression", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-create-batch.cron-expression'" + }, + { + "name": "batch-info.post-create-batch.description", + "type": "java.lang.String", + "description": "A description for 'batch-info.post-create-batch.description'" } ]} \ 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 920f8dd..5ea7ac5 100644 --- a/batch-quartz/src/main/resources/application.yml +++ b/batch-quartz/src/main/resources/application.yml @@ -2,6 +2,13 @@ server: port: 8081 spring: + cloud: + refresh: + enabled: true + devtools: + restart: + enabled: false + add-properties: false application: name: spring-batch-quartz datasource: @@ -95,7 +102,7 @@ batch-info: email-send-batch: group: "EMAIL" job-name: "emailSendJob" - cron-expression: "*/10 * * * * ?" + cron-expression: "*/20 * * * * ?" description: "이메일배치작업" post-batch: group: "POST" @@ -105,7 +112,7 @@ batch-info: post-create-batch: group: "POST" job-name: "postCreateJob" - cron-expression: "0/50 * * * * ?" + cron-expression: "0/30 * * * * ?" description: "테스트배치작업" jwt: @@ -116,6 +123,12 @@ jwt: secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc= expiration: 10080 +management: + endpoints: + web: + exposure: + include: refresh + logging: level: org: diff --git a/batch-quartz/src/main/resources/static/js/apis/schedule-api.js b/batch-quartz/src/main/resources/static/js/apis/schedule-api.js index b671c3e..586e6cf 100644 --- a/batch-quartz/src/main/resources/static/js/apis/schedule-api.js +++ b/batch-quartz/src/main/resources/static/js/apis/schedule-api.js @@ -34,6 +34,11 @@ const scheduleService = { cronExpression }); return response.data.data; + }, + + refreshJob: async () => { + await apiClient.post('/actuator/refresh'); + return true; } }; diff --git a/batch-quartz/src/main/resources/static/js/common/axios-instance.js b/batch-quartz/src/main/resources/static/js/common/axios-instance.js index 01e8d36..323235d 100644 --- a/batch-quartz/src/main/resources/static/js/common/axios-instance.js +++ b/batch-quartz/src/main/resources/static/js/common/axios-instance.js @@ -29,6 +29,11 @@ apiClient.interceptors.request.use( apiClient.interceptors.response.use( (response) => { + const authHeader = response.headers['authorization']; + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.substring(7); + saveAccessToken(token); + } return response; }, async (error) => { @@ -49,6 +54,7 @@ apiClient.interceptors.response.use( } if (status === 401) { + removeTokens(); location.href = "/"; } return new Promise(() => {}); diff --git a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js index f1a76a8..d726231 100644 --- a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js +++ b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js @@ -9,6 +9,11 @@ document.addEventListener('DOMContentLoaded', () => { e.preventDefault(); fetchDataAndRender(); }); + + const refreshJobBtn = document.getElementById('refreshJobBtn'); + refreshJobBtn.addEventListener('click', refreshJobs); + + manageTooltips.init(); }); const fetchDataAndRender = async () => { @@ -19,6 +24,15 @@ const fetchDataAndRender = async () => { updateTable(response); }; +const refreshJobs = async () => { + const result = await scheduleService.refreshJob(); + if (result) { + alert('스케줄이 재적용 되었습니다.'); + manageTooltips.hideAll(); + fetchDataAndRender(); + } +}; + const updateTable = (jobs) => { const tableBody = document.querySelector('tbody'); tableBody.innerHTML = jobs.map(job => ` @@ -64,10 +78,10 @@ const showJobDetail = async (e) => { updateJobControlButtons(jobDetail.status); document.getElementById('pauseJobBtn').onclick = () => { - pauseJob(group, name).then(() => updateJobStatus(group, name, 'PAUSED')); + scheduleService.pauseJob(group, name).then(() => updateJobStatus(group, name, 'PAUSED')); }; document.getElementById('resumeJobBtn').onclick = () => { - resumeJob(group, name).then(() => updateJobStatus(group, name, 'NORMAL')); + scheduleService.resumeJob(group, name).then(() => updateJobStatus(group, name, 'NORMAL')); }; document.getElementById('updateCronBtn').onclick = () => { updateCronExpression(group, name); @@ -167,4 +181,15 @@ const updateTableJobStatus = (group, name, newStatus) => { break; } } +}; + +const manageTooltips = { + init: () => { + const tooltipElements = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipElements.map(el => new bootstrap.Tooltip(el)); + }, + hideAll: () => { + const tooltip = bootstrap.Tooltip.getInstance('#refreshJobBtn'); + setTimeout(() => { tooltip.hide(); }, 100); + } }; \ No newline at end of file diff --git a/batch-quartz/src/main/resources/templates/fragments/header.html b/batch-quartz/src/main/resources/templates/fragments/header.html index 0aeab69..ca19264 100644 --- a/batch-quartz/src/main/resources/templates/fragments/header.html +++ b/batch-quartz/src/main/resources/templates/fragments/header.html @@ -3,7 +3,7 @@