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 @@