Skip to content

Instantly share code, notes, and snippets.

@ihoneymon
Created November 4, 2015 06:35
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ihoneymon/8e039a7d63e82f209826 to your computer and use it in GitHub Desktop.
Save ihoneymon/8e039a7d63e82f209826 to your computer and use it in GitHub Desktop.

Quartz Scheduler 적용 정리

Quartz Scheduler를 사용한 이유는 Spring의 스케줄링은 정해진 시간에 도는 설정만 가능한데 비해, Quartz Scheduler의 JobStore 기능을 이용하면 사용자가 입력한 정보를 DB 에 저장하고 변경하고 Quartz Scheduler가 이를 확인하여 실행하는 것이 가능했기 때문이다. 이와 관련된 기본적인 설정 및 구현 내용을 정리한다.


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"

quartz.properties 설정하여 클래스패스(src/main/resource/quartz) 상에 위치

  • 참조: '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

Quartz JOBStore를 위해서는 Database에 QuartzJOB 관련된 스키마와 테이블이 생성되어 있어야 한다.

배포판을 내려받아 압축을 풀어보면 docs/dbTables 디렉토리안에 데이터베이스별로 DB 초기화 스크립트가 포함되어 있어 이를 실행해주면 이후에는 Quartz의 SchedulerFactoryBean 을 설정해줄 때 `DataSource`를 주입해주면 알아서 해당 데이터베이스에 정보를 넣고 빼고함

Quartz Scheduler 관련 설정

/**
 * 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();
    }
}

Job 구현

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);
       }
   }
}

JobListner 구현

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 실행후 처리할 로직을 여기에 구현
    }

}

QuartzJob을 실행하기 위한 QuartzServiceQuartzServiceImpl 구현

  • 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);
            }
        }
    }

JobSchedulerDto

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();
    }
}

참고

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment