Quartz Scheduler를 사용한 이유는 Spring의 스케줄링은 정해진 시간에 도는 설정만 가능한데 비해, Quartz Scheduler의 JobStore 기능을 이용하면 사용자가 입력한 정보를 DB 에 저장하고 변경하고 Quartz Scheduler가 이를 확인하여 실행하는 것이 가능했기 때문이다. 이와 관련된 기본적인 설정 및 구현 내용을 정리한다.
build.gradle
ext { quartzSchedulerVersion = '2.2.1' } /** * http://quartz-scheduler.org * Quartz: Job Scheduler */ compile "org.quartz-scheduler:quartz:$quartzSchedulerVersion" compile "org.quartz-scheduler:quartz-jobs:$quartzSchedulerVersion"
-
참조: 'JDBC JobStoreCMT Configuration'
Quartz 에서 등록하는 JOB을 JDBC를 이용해서 저장하기 위한 설정, 보다 자세한 사항은 참조문서를 살펴보시길
org.quartz.scheduler.instanceName = innoquartz-scheduler
org.quartz.threadPool.threadCount = 10
org.quartz.jobStore.isClustered = false
#스케줄을 DB에 저장관리하기 위해서 설정
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
배포판을 내려받아 압축을 풀어보면 docs/dbTables
디렉토리안에 데이터베이스별로 DB 초기화 스크립트가 포함되어 있어 이를 실행해주면 이후에는 Quartz의 SchedulerFactoryBean 을 설정해줄 때 `DataSource`를 주입해주면 알아서 해당 데이터베이스에 정보를 넣고 빼고함
/**
* SchedulerFactoryBean 에 스프링의 빈정보ApplicationContext를 주입하기 위한 목적으로 생성
*
* This JobFactory autowires automatically the created quartz bean with spring
* autowired dependencies.
*
* @author jiheon
* @see https://gist.github.com/ihoneymon/d5c59ea2f4b959d2888d
*/
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
beanFactory = applicationContext.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}
//===========================================================
package com.innotree.innoquartz.server.configuration;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobListener;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import com.innotree.innoquartz.server.common.AutowiringSpringBeanJobFactory;
import com.innotree.innoquartz.server.common.listener.QuartzJobListener;
/**
* Quazrtz ScheudlerFactoryBean을 스프링 빈으로 등록
*
* @author jiheon
* @see https://gist.github.com/jelies/5085593
* @see http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#scheduling-quartz
* @see https://beyondj2ee.wordpress.com/2014/01/02/spring-4-x-quartz-2-x-%EC%97%B0%EB%8F%99-%EB%B0%A9%EB%B2%95-spring-4-x-quartz-2-x-example/
*/
@Slf4j
@Configuration
public class QuartzConfiguration {
@Autowired
private DataSource dataSource;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private ApplicationContext applicationContext;
@Resource
private InnoQuartzProperties innoQuartzProperties;
@PostConstruct
public void init() {
log.debug("QuartzConfig initailized.");
}
@Bean
public SchedulerFactoryBean schedulerFactory() throws SchedulerException {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
schedulerFactoryBean.setJobFactory(jobFactory);
schedulerFactoryBean.setTransactionManager(transactionManager);
schedulerFactoryBean.setDataSource(dataSource);
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setSchedulerName(innoQuartzProperties.getScheduler().getName());
schedulerFactoryBean.setAutoStartup(true);
schedulerFactoryBean.setQuartzProperties(quartzProperties());
return schedulerFactoryBean;
}
@Bean
public Properties quartzProperties() {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("quartz/quartz.properties"));
Properties properties = null;
try {
propertiesFactoryBean.afterPropertiesSet();
properties = propertiesFactoryBean.getObject();
} catch (Exception e) {
log.warn("Cannont load quartz.properties");
}
return properties;
}
@Bean
public JobListener quartzJobListener() {
return new QuartzJob();
}
}
public interface Job {
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Interface.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/**
* <p>
* Called by the <code>{@link Scheduler}</code> when a <code>{@link Trigger}</code>
* fires that is associated with the <code>Job</code>.
* </p>
*
* <p>
* The implementation may wish to set a
* {@link JobExecutionContext#setResult(Object) result} object on the
* {@link JobExecutionContext} before this method exits. The result itself
* is meaningless to Quartz, but may be informative to
* <code>{@link JobListener}s</code> or
* <code>{@link TriggerListener}s</code> that are watching the job's
* execution.
* </p>
*
* @throws JobExecutionException
* if there is an exception while executing the job.
*/
void execute(JobExecutionContext context)
throws JobExecutionException;
}
public class QuartzJob implements Job {
//JobDetail에서 식별을 위한 KEY 생성(Name, Group)
public JobKey getJobKey() {
return JobKey.jobKey(getName(), getGroup());
}
//Trigger 식별을 위해 사한 KEY 생성(Name, KEY)
public TriggerKey getTriggerKey() {
return TriggerKey.triggerKey(getName(), getGroup());
}
/**
* get Job Name
*
* @return {@link Job#getId()}
*/
public String getName() {
return getQueueType().toString();
}
/**
* get Group name
*
* @return {@link Project#getId()}
*/
public String getGroup() {
return getJob().getId().toString();
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
//실행로직 구현
} catch (Exception e) {
log.error("Occur Exception: {}", e);
throw new JobExecutionException(e);
}
}
}
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
public class QuartzJobListner implements JobListener {
@Override
public String getName() {
// TODO Auto-generated method stub
return null;
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
// TODO Auto-generated method stub
//JobDetail이 실행될 떄 스케줄러에의해서 호출되는 메서드
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
// TODO Auto-generated method stub
// JobDetail이 실행횔 때 TriggerListener가 실행
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
// TODO Auto-generated method stub
// Job 실행이 완료된 후 호출되는 메서드, JOB 실행후 처리할 로직을 여기에 구현
}
}
-
QuartzService
import org.quartz.SchedulerException; import com.innotree.innoquartz.server.common.QuartzJob; import com.innotree.innoquartz.server.common.exception.InnoQuartzException; import com.innotree.innoquartz.server.job.Job; import com.innotree.innoquartz.server.job.JobHistory; import com.innotree.innoquartz.server.job.type.JobQueueType; public interface QuartzService { /** * Register JobScheduler QuartzSchedulerFactory빈에 스케줄디테일을 등록하고 그 결과를 * JobHistory로 만들어서 반환한다. * * @param QuartzJob * {@link QuartzJob} * @return {@link JobHistory} * @throws SchedulerException */ public JobHistory register(QuartzJob quartzJob) throws SchedulerException; /** * Delete JobScheduler QuartzSchedulerFactory빈에 스케줄디테일을 삭제하고 그 결과를 * JobHistory로 만들어서 반환한다. 삭제시 trigger도 함께 삭제된다. * * @param jobJob * {@link QuartzJob} * @return {@link JobHistory} * @throws SchedulerException */ public JobHistory delete(QuartzJob quartzJob) throws SchedulerException; /** * Pause jobSchedule * * @param QuartzJob * @return * @throws SchedulerException */ public JobHistory pause(QuartzJob quartzJob) throws SchedulerException; /** * Resume jobSchedule * * @param QuartzJob * @return * @throws SchedulerException */ public JobHistory resume(QuartzJob quartzJob) throws SchedulerException; public boolean checkExistSchedule(QuartzJob quartzJob) throws SchedulerException; /** * Execute immediately Job * * @param QuartzJob * @return * @throws SchedulerException */ public QuartzJob immediatelyExecution(QuartzJob quartzJob) throws SchedulerException, InnoQuartzException; /** * Stop execution job * * @param job * {@link Job} * @param jobQueueType * {@link JobQueueType} * @return */ public void stopExecutionJob(Job job, JobQueueType jobQueueType) throws InnoQuartzException; }
-
QuartzServiceImpl
@Service @Transactional(readOnly = true) public class QuartzServiceImpl implements QuartzService { private static final String KEY_EXECUTOR = "executor"; private static final String KEY_RELATED_JOB = "relatedJob"; private static final String KEY_QUEUE_TYPE = "queueType"; private static final String KEY_JOB = "job"; @Autowired private JobService jobService; @Autowired private SchedulerFactoryBean schedulerFactoryBean; @Autowired private JobHistoryRepository jobHistoryRepository; @Autowired private JobQueueRepository jobQueueRepository; @Autowired private AgentJobService agentJobService; @PostConstruct public void afterPropertiesSet() { log.debug("Cleaning JobQueue."); jobQueueRepository.deleteAll(); jobQueueRepository.flush(); } @PreDestroy public void shutdownQuartzSchedule() { try { log.debug("Shutdown Quartz Scheduler."); schedulerFactoryBean.getScheduler().shutdown(); } catch (SchedulerException e) { log.error("Occur SchedulerException: {}", e); } } @Override @Transactional public JobHistory register(QuartzJob quartzJob) throws SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); JobDetail jobDetail = concreteJobDetail(quartzJob); Trigger trigger = concreteCronTrigger(quartzJob); scheduler.scheduleJob(jobDetail, trigger); return jobHistoryRepository .saveAndFlush(new JobHistory.Builder(quartzJob.getJob(), JobHistoryType.REGISTER).build()); } /** * use identify: {name: job.id, group: project.id} * * @param job * {@link QuartzJob} * @return {@link Trigger} {@link CronTrigger} */ private Trigger concreteCronTrigger(QuartzJob job) { return TriggerBuilder.newTrigger().withIdentity(job.getName(), job.getGroup()).startNow() .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronStatement())).build(); } /** * use identify: {name: job.id, group: project.id} * * QuartzJob에 필요한 데이터를 저장하기 위해서는 getJobDataMap()을 통해 데이터를 등록해야 한다. * * @param quartzJob * {@link QuartzJob} * @return {@link JobDetail} */ private JobDetail concreteJobDetail(QuartzJob quartzJob) { JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class) .withIdentity(quartzJob.getName(), quartzJob.getGroup()).build(); jobDetail.getJobDataMap().put(KEY_JOB, quartzJob.getJob()); jobDetail.getJobDataMap().put(KEY_QUEUE_TYPE, quartzJob.getQueueType()); jobDetail.getJobDataMap().put(KEY_RELATED_JOB, quartzJob.getRelatedJob()); jobDetail.getJobDataMap().put(KEY_EXECUTOR, quartzJob.getExecutor()); return jobDetail; } @Override @Transactional public JobHistory delete(QuartzJob job) throws SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); scheduler.deleteJob(job.getJobKey()); return jobHistoryRepository.saveAndFlush(new JobHistory.Builder(job.getJob(), JobHistoryType.DELETE).build()); } @Override @Transactional public JobHistory pause(QuartzJob quartzJob) throws SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); scheduler.pauseJob(quartzJob.getJobKey()); scheduler.pauseTrigger(quartzJob.getTriggerKey()); return jobHistoryRepository .saveAndFlush(new JobHistory.Builder(quartzJob.getJob(), JobHistoryType.PAUSE).build()); } @Override @Transactional public JobHistory resume(QuartzJob quartzJob) throws SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); scheduler.resumeJob(quartzJob.getJobKey()); scheduler.resumeTrigger(quartzJob.getTriggerKey()); Trigger trigger = concreteCronTrigger(quartzJob); scheduler.rescheduleJob(quartzJob.getTriggerKey(), trigger); return jobHistoryRepository .saveAndFlush(new JobHistory.Builder(quartzJob.getJob(), JobHistoryType.RESUME).build()); } @Override public boolean checkExistSchedule(QuartzJob quartzJob) throws SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); return scheduler.checkExists(quartzJob.getJobKey()); } @Override @Transactional public QuartzJob immediatelyExecution(QuartzJob quartzJob) throws SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); JobDetail jobDetail = concreteJobDetail(quartzJob); Trigger trigger = concreteSimpleTrigger(quartzJob); try { checkJobStatus(quartzJob); stopExecutionJob(quartzJob.getJob(), JobQueueType.IMMEDIATELY); scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { log.error("Occur SchedulerException: {}", e); throw e; } return quartzJob; } private void checkJobStatus(QuartzJob quartzJob) { Job job = jobService.findOne(quartzJob.getJob().getId()); if (!job.isActive()) { throw new InnoQuartzException("job.exception.status.notActive"); } } private Trigger concreteSimpleTrigger(QuartzJob quartzJob) { return TriggerBuilder.newTrigger().withIdentity(quartzJob.getTriggerKey()).startNow().build(); } @Override @Transactional public void stopExecutionJob(Job job, JobQueueType jobQueueType) throws InnoQuartzException { if(job.hasAgentJob()) { agentJobService.stopExecute(job.getAgentJob(), jobQueueType); } else { List<JobQueue> jobQueueList = jobQueueRepository.findByJob(job); interruptAndDelete(job, jobQueueType, jobQueueList); jobQueueRepository.delete(jobQueueList); jobQueueRepository.flush(); } } private void interruptAndDelete(Job job, JobQueueType jobQueueType, List<JobQueue> jobQueueList) { Scheduler scheduler = schedulerFactoryBean.getScheduler(); QuartzJob quartzJob = new QuartzJob.Builder(job).addJobQueueType(jobQueueType).build(); try { if (checkExistSchedule(quartzJob)) { scheduler.interrupt(quartzJob.getJobKey()); scheduler.deleteJob(quartzJob.getJobKey()); } } catch (SchedulerException e) { log.error("Occur SchedulerException: {}", e); } } }
public class JobScheduleDto implements Serializable {
private static final long serialVersionUID = 1L;
private static String PATTERN_LAST = "(L)";
private static String PATTERN_LAST_MINUS_DAY = "(L)(-\\d)";
private static String PATTERN_SINGLE = "(\\*|\\?|LW)";
private static String PATTERN_GENERAL = "(\\d+)(-|/|#)(\\d+)";
private static String PATTERN_WEEK = "(\\d+)(W)";
private static String PATTERN_DIGIT = "(\\d+)";
private boolean useScheduler;
private JobScheduleType scheduleType;
private String minute;
private String hour;
private String dayOfMonth;
private String month;
private String dayOfWeek;
public boolean hasSpecificChar(String fieldName, String separator) throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException {
return getFieldData(fieldName).contains(separator);
}
private String getFieldData(String fieldName) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = this.getClass();
Field field = clazz.getDeclaredField(fieldName);
String fieldData = (String) field.get(this);
return fieldData;
}
public String[] getSplitFieldData(String fieldName) throws NoSuchFieldException, IllegalAccessException {
String originFieldData = getFieldData(fieldName);
if (Pattern.matches(PATTERN_LAST, originFieldData)) {
Matcher matcher = Pattern.compile(PATTERN_LAST).matcher(originFieldData);
return matcher.matches() ? new String[] { matcher.group(1) } : new String[] {};
} else if (Pattern.matches(PATTERN_LAST_MINUS_DAY, originFieldData)) {
Matcher matcher = Pattern.compile(PATTERN_LAST_MINUS_DAY).matcher(originFieldData);
return matcher.matches() ? new String[] { matcher.group(1), matcher.group(2) } : new String[] {};
} else if (Pattern.matches(PATTERN_SINGLE, originFieldData)) {
Matcher matcher = Pattern.compile(PATTERN_SINGLE).matcher(originFieldData);
return matcher.matches() ? new String[] { matcher.group(1) } : new String[] {};
} else if (Pattern.matches(PATTERN_WEEK, originFieldData)) {
Matcher matcher = Pattern.compile(PATTERN_WEEK).matcher(originFieldData);
return matcher.matches() ? new String[] { matcher.group(1), matcher.group(2) } : new String[] {};
} else if (Pattern.matches(PATTERN_DIGIT, originFieldData)) {
Matcher matcher = Pattern.compile(PATTERN_DIGIT).matcher(originFieldData);
return matcher.matches() ? new String[] { matcher.group(1) } : new String[] {};
} else {
Matcher matcher = Pattern.compile(PATTERN_GENERAL).matcher(originFieldData);
return matcher.matches() ? new String[] { matcher.group(1), matcher.group(2), matcher.group(3) }
: new String[] {};
}
}
public boolean isPatternLast(String fieldName) throws NoSuchFieldException, IllegalAccessException {
return Pattern.matches(PATTERN_LAST, getFieldData(fieldName));
}
public boolean isPatternLastMinusDay(String fieldName) throws NoSuchFieldException, IllegalAccessException {
return Pattern.matches(PATTERN_LAST_MINUS_DAY, getFieldData(fieldName));
}
public boolean isPatternSingle(String fieldName) throws NoSuchFieldException, IllegalAccessException {
return Pattern.matches(PATTERN_SINGLE, getFieldData(fieldName));
}
public boolean isPatternGeneral(String fieldName) throws NoSuchFieldException, IllegalAccessException {
return Pattern.matches(PATTERN_GENERAL, getFieldData(fieldName));
}
public boolean isPatternWeek(String fieldName) throws NoSuchFieldException, IllegalAccessException {
return Pattern.matches(PATTERN_WEEK, getFieldData(fieldName));
}
public boolean isPatternDigit(String fieldName) throws NoSuchFieldException, IllegalAccessException {
return Pattern.matches(PATTERN_DIGIT, getFieldData(fieldName));
}
public String getStatement() {
StringBuilder sb = new StringBuilder();
sb.append(getMinute());
sb.append("\t");
sb.append(getHour());
sb.append("\t");
sb.append(getDayOfMonth());
sb.append("\t");
sb.append(getMonth());
sb.append("\t");
sb.append(getDayOfWeek());
return sb.toString();
}
}
-
Quartz 사이트: 'http://quartz-scheduler.org'
-
'2.2.x 문서'
-
-
Quartz Cron Expression: 'http://java.ihoney.pe.kr/353'
-
Quartz Cron Expression unitTest: 'http://java.ihoney.pe.kr/354'