1
votes

We were using Spring Boot 2.1.6.RELEASE. after that we updated spring version to 2.2.2.RELEASE. When we change the version we noticed our quartz jobs not working. We have multiple jobs and we configured them like below. After some reasearch i found some differences between in QuartzAutoConfiguration class. How can i inject my triggers in spring 2.2.2.RELEASE. Is there any easy way? I dont want to write to many triggers and trigger details.

MyConfig

import io.rkpc.commons.util.ApplicationReflectionUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Configuration
@ConfigurationProperties(prefix = "quartz")
@Profile("quartz")
@Data
public class JobConfig {

    private List<Job> jobs;

    @Bean
    public JobDetail[] jobDetail() throws SchedulerConfigException {
        Set<Class<QuartzJobBean>> subClasses = ApplicationReflectionUtil.getSubClasses(QuartzJobBean.class, "io.rkpc");
        List<JobDetail> jobDetails = new ArrayList<>();
        for (Class<QuartzJobBean> quartzJobBeanClass : subClasses) {
            Job job = getJob(quartzJobBeanClass.getSimpleName());
            if (job.isEnabled()) {
                JobDetail jobDetail = JobBuilder.newJob(quartzJobBeanClass)
                        .withIdentity(quartzJobBeanClass.getSimpleName())
                        .storeDurably()
                        .build();
                jobDetails.add(jobDetail);
            }
        }
        return jobDetails.toArray(new JobDetail[0]);
    }

    @Bean
    public Trigger[] jobATrigger(JobDetail[] jobADetails) throws SchedulerConfigException {
        List<Trigger> triggers = new ArrayList<>();
        for (JobDetail jobDetail : jobADetails) {
            Job job = getJob(jobDetail.getKey().getName());
            CronTrigger trigger = TriggerBuilder.newTrigger().forJob(jobDetail)
                    .withIdentity(jobDetail.getKey().getName().concat("Trigger"))
                    .withSchedule(CronScheduleBuilder.cronSchedule(job.getCron()))
                    .build();
            triggers.add(trigger);
        }
        return triggers.toArray(new Trigger[0]);
    }

    private Job getJob(String name) throws SchedulerConfigException {
        List<Job> filteredJobs = jobs.stream().filter(job -> job.getName().equals(name)).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(filteredJobs) || filteredJobs.size() > 1) {
            log.error("{} is not configured", name);
            throw new SchedulerConfigException("Job is not configured");
        }

        return filteredJobs.get(0);
    }

    @Data
    public static class Job {
        private String name;
        private String cron;
        private boolean enabled;
    }
}

QuartzAutoConfiguration.java Spring version 2.1.6 github url ; https://github.com/spring-projects/spring-boot/blob/v2.1.6.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java

QuartzAutoConfiguration.java Spring version 2.2.2 github url https://github.com/spring-projects/spring-boot/blob/v2.2.2.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java

Main difference i notice is ; in 2.1.6 version Quartz AutoConfiguration was "Trigger" array but 2.2.2 doesn't have "Trigger" array.

2
There's no array, but that was an implementation detail of the auto-configuration. Trigger beans should still be configured on the SchedulerFactoryBean: github.com/spring-projects/spring-boot/blob/v2.2.2.RELEASE/… - Andy Wilkinson

2 Answers

1
votes

Spring has always some magic :)

import io.rkpc.commons.util.ApplicationReflectionUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.JobDetailImpl;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import javax.validation.constraints.NotNull;
import java.text.ParseException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Configuration
@ConfigurationProperties(prefix = "quartz")
@Profile("quartz")
@Data
@AutoConfigureBefore({QuartzAutoConfiguration.class})
@RequiredArgsConstructor(onConstructor = @__({@Autowired, @NotNull}))
public class JobConfig {

    private final List<Job> jobs;
    private final DefaultListableBeanFactory beanFactory;

    @PostConstruct
    public void init() throws SchedulerConfigException, ParseException {
        Set<Class<QuartzJobBean>> subClasses = ApplicationReflectionUtil.getSubClasses(QuartzJobBean.class, "io.rkpc");

        for (Class<QuartzJobBean> quartzJobBeanClass : subClasses) {
            Job job = getJob(quartzJobBeanClass.getSimpleName(), jobs);
            if (job.isEnabled()) {
                JobDetailImpl jobDetail = (JobDetailImpl) JobBuilder.newJob(quartzJobBeanClass)
                        .withIdentity(quartzJobBeanClass.getSimpleName())
                        .storeDurably()
                        .build();
                CronTriggerImpl trigger = (CronTriggerImpl) TriggerBuilder.newTrigger().forJob(jobDetail)
                        .withIdentity(jobDetail.getKey().getName().concat("Trigger"))
                        .withSchedule(CronScheduleBuilder.cronSchedule(job.getCron()))
                        .build();

                GenericBeanDefinition jobBeanDefinition = new GenericBeanDefinition();
                jobBeanDefinition.setBeanClass(JobDetailImpl.class);
                jobBeanDefinition.getPropertyValues().addPropertyValue("jobClass", quartzJobBeanClass);
                jobBeanDefinition.getPropertyValues().addPropertyValue("key", jobDetail.getKey());
                jobBeanDefinition.getPropertyValues().addPropertyValue("durability", jobDetail.isDurable());
                beanFactory.registerBeanDefinition(quartzJobBeanClass.getSimpleName(), jobBeanDefinition);

                GenericBeanDefinition triggerBeanDefinition = new GenericBeanDefinition();
                triggerBeanDefinition.setBeanClass(CronTriggerImpl.class);
                triggerBeanDefinition.getPropertyValues().addPropertyValue("jobKey", trigger.getJobKey());
                triggerBeanDefinition.getPropertyValues().addPropertyValue("key", trigger.getKey());
                triggerBeanDefinition.getPropertyValues().addPropertyValue("cronExpression", new CronExpression(trigger.getCronExpression()));
                beanFactory.registerBeanDefinition(trigger.getName(), triggerBeanDefinition);
            }
        }
    }

    public Job getJob(String name, List<Job> jobs) throws SchedulerConfigException {
        List<Job> filteredJobs = jobs.stream().filter(job -> job.getName().equals(name)).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(filteredJobs) || filteredJobs.size() > 1) {
            log.error("{} is not configured", name);
            throw new SchedulerConfigException("Job is not configured");
        }

        return filteredJobs.get(0);
    }

    @Data
    public static class Job {
        private String name;
        private String cron;
        private boolean enabled;
    }
}
1
votes

You are exposing a single Trigger[] bean rather than multiple Trigger beans. You should define one bean per Trigger. You should also do the same for each JobDetail. This was working by accident with Spring Boot 2.1.x as you were relying on the auto-configuration using ObjectProvider<Trigger[]>. The intent of the auto-configuration was to consume all Trigger beans, with the application context turning those beans into an array before injecting them.

If you don't want to define multiple Trigger and JobDetail beans, you may be better configuring the SchedulerFactoryBean yourself rather than relying on the auto-configuration.