2

I have a QuartzJobConfig class where I register my Spring-Quartz-Beans.

I followed the instruction of the SchedulerFactoryBean, JobDetailFactoryBean and CronTriggerFactoryBean.

My Jobs are configured in a yaml file outside the application. Means I have to create the Beans dynamically when the application starts.

My Config:

channelPartnerConfiguration:
  channelPartners:
  - code: Job1
    jobConfigs:
    - schedule: 0 * * ? * MON-FRI
      name: Job1 daily
      hotel: false
      allotment: true
      enabled: true
    - schedule: 30 * * ? * MON-FRI
      name: Job2 weekly
      hotel: true
      allotment: false
      enabled: true
    ...

My Config Class:

@Configuration
public class QuartzJobConfig implements IJobClass{

    @Autowired 
    ChannelPartnerProperties channelPartnerProperties;

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public SchedulerFactoryBean quartzScheduler() {
        SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean();

        quartzScheduler.setOverwriteExistingJobs(true);
        quartzScheduler.setSchedulerName("-scheduler");

        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        quartzScheduler.setJobFactory(jobFactory);

        // point 1
        List<Trigger> triggers = new ArrayList<>();
        for(ChannelPartner ch : channelPartnerProperties.getChannelPartners()){
            for(JobConfig jobConfig : ch.getJobConfigs()){
                triggers.add(jobTrigger(ch, jobConfig).getObject());
            }
        }
        quartzScheduler.setTriggers(triggers.stream().toArray(Trigger[]::new));

        return quartzScheduler;
    }

    @Bean
    public JobDetailFactoryBean jobBean(ChannelPartner ch, JobConfig jobConfig) {
        JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
        jobDetailFactoryBean.setJobClass(findJobByConfig(jobConfig));
        jobDetailFactoryBean.setGroup("mainGroup");
        jobDetailFactoryBean.setName(jobConfig.getName());
        jobDetailFactoryBean.setBeanName(jobConfig.getName());
        jobDetailFactoryBean.getJobDataMap().put("channelPartner", ch);
        return jobDetailFactoryBean;
    }

    @Bean
    public CronTriggerFactoryBean jobTrigger(ChannelPartner ch, JobConfig jobConfig) {
        CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
        cronTriggerFactoryBean.setJobDetail(jobBean(ch, jobConfig).getObject());
        cronTriggerFactoryBean.setCronExpression(jobConfig.getSchedule());
        cronTriggerFactoryBean.setGroup("mainGroup");
        return cronTriggerFactoryBean;
    }

    @Override
    public Class<? extends Job> findJobByConfig(JobConfig jobConfig) {
        if(isAllotmentJob(jobConfig) && isHotelJob(jobConfig)){
            return HotelAndAllotmentJob.class;
        }
        if(isAllotmentJob(jobConfig)){
            return AllotmentJob.class;
        }
        if(isHotelJob(jobConfig)){
            return HotelJob.class;
        }
        return HotelAndAllotmentJob.class;
    }

    private boolean isAllotmentJob(JobConfig jobConfig){
        return jobConfig.isAllotment();
    }

    private boolean isHotelJob(JobConfig jobConfig) {
        return jobConfig.isHotel();
    }

}

My problem is that the creation of the Beans inside the iteration (Point 1) is just done one time. After the first iteration its not going inside the jobTrigger(ch, jobConfig) method anymore. (More or less clear because of the bean name if I am right)

What I was thinking, because I use the Quartz factories of Spring the jobDetailFactoryBean.setBeanName() method is used to create more beans with different names.

Not sure how I can solve this problem. The Code is working and the first created job is executing right. But I need more jobs.

How can I create the different jobs in a dynamically way?


Edit:

My full configuration classes:

@Configuration
@ConfigurationProperties(prefix = "channelPartnerConfiguration", locations = "classpath:customer/channelPartnerConfiguration.yml")
public class ChannelPartnerProperties {

    @Autowired
    private List<ChannelPartner> channelPartners;

    public List<ChannelPartner> getChannelPartners() {
        return channelPartners;
    }

    public void setChannelPartners(List<ChannelPartner> channelPartners) {
        this.channelPartners = channelPartners;
    }
}

@Configuration
public class ChannelPartner {

    private String code;
    private String contracts;
    private Boolean includeSpecialContracts;
    private String touroperatorCode = "EUTO";

    @Autowired
    private PublishConfig publishConfig;

    @Autowired
    private BackupConfig backupConfig;

    @Autowired
    private List<JobConfig> jobConfigs;
    //getter/setter

@Configuration
public class JobConfig {

    private String schedule;
    private boolean hotelEDF;
    private boolean allotmentEDF;
    private boolean enabled;
    private String name;
    //getter/setter

Added project to github for better understanding of the problem

Patrick
  • 12,336
  • 15
  • 73
  • 115
  • You have your methods marked `@Bean` which means all of them are singletons... If you just want to use it as a factory method mark it `@Bean(scope="prototype")`. – M. Deinum Dec 09 '16 at 07:43
  • @M.Deinum I tried to use prototype scope. But its just initialising the same job. Should my `List` be prototype? – Patrick Dec 09 '16 at 08:32
  • Every bean you want to have multiple instances of (your job, triggers) need to be prototype scoped in this scenario else it will not work. So both your `jobTrigger` as well as the `jobBean` have to be prototype scoped... Else it will fail. – M. Deinum Dec 09 '16 at 08:43
  • @M.Deinum exactly. The workflow you described was exactly happened like that. But only my first jobconfig in the list was registered multiple times. What did I miss here? – Patrick Dec 09 '16 at 09:00
  • If you don't have prototype scoped beans they are singletons and will only be created once... – M. Deinum Dec 09 '16 at 09:12
  • you are saying that it only goes once inside the jobTrigger() method. So does that mean your jobConfigs you are iterating over is a single element? Have you inspected it? – dimitrisli Dec 11 '16 at 02:38
  • @dimitrisli jobConfigs is a list and has for one `channelPartner` multiple `jobConfigs`. The iteration works, means it wents multiple times inside the loop but only the first time inside `jobTrigger()`. (singleton / prototype issue?) I added my full config. – Patrick Dec 12 '16 at 07:38
  • can you check with your debugger the for loop happens multiple times and step-in to see it getting into jobTrigger(). There is no Spring scope issue here, being prototype or singleton is irrelevant because you are not injecting it - you are invoking the method directly. – dimitrisli Dec 12 '16 at 09:58
  • @dimitrisli I checked it with the debugger. The loop iterates correct. The first time it wents to jobtrigger() and after the first time the debug point in jobtrigger is never stop again. And it adds the same first object to the list. (Means for every iteration the same object.) I will add a sample code to github in some minutes. – Patrick Dec 12 '16 at 10:06
  • @dimitrisli added code on github. Should work without any additional config. Maybe it helps to solve the problem. [Github](https://github.com/Pkurz/Quartz) – Patrick Dec 12 '16 at 12:52

3 Answers3

2

The reason why your list will contain null values is because the getObject method you are calling, should return the CronTrigger which is only initiated in afterPropertiesSet method called by spring when done initiating the spring context. You can call this method yourself manually on your CronTriggerFactoryBean, this will allow you to have it as a private method.

    // Just to clarify, no annotations here
    private CronTriggerFactoryBean jobTrigger(ChannelPartner ch, JobConfig jobConfig) throws ParseException {
        CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
        cronTriggerFactoryBean.setJobDetail(jobBean(ch, jobConfig).getObject());
        cronTriggerFactoryBean.setCronExpression(jobConfig.getSchedule());
        cronTriggerFactoryBean.setGroup("mainGroup");
        cronTriggerFactoryBean.setBeanName(jobConfig.getName() + "Trigger");
        cronTriggerFactoryBean.afterPropertiesSet();
        return cronTriggerFactoryBean;
    }

I'm sure there are many other ways of doing this as well, as you mentioned yourself you did a work-around for it, if this however is not what you want or need I can check some more if I can find a better way.

nesohc
  • 487
  • 1
  • 5
  • 14
  • why `CronTriggerFactoryBean` should not have any annotation? – Patrick Dec 16 '16 at 08:09
  • Both your jobBean and jobTrigger methods are called (by chain) from quartzScheduler method. And as that method creates a SchedulerFactoryBean that contains these and itself is a bean (meaning it's a part of the spring context). I think the only important thing is that your SchedulerFactoryBean is a spring bean, I could be wrong here but I think as long as you take care of them yourself, there is no need for them to actually be in the spring context. So if you can answer, why should JobDetailFactoryBean and CronTriggerFactoryBean be in the spring context? – nesohc Dec 16 '16 at 08:31
  • I followed this instruction. [quartz and spring](https://gist.github.com/jelies/5085593) – Patrick Dec 16 '16 at 08:51
  • Okey, so the way I understand this is, that the reason they are added as spring beans is to ensure that some methods are being called after creating the context. afterPropertiresSet being the only one I can find, and that you can manually call. The downside however of not having them as beans would be if in future versions other methods are being added that needs to be called after creation that spring takes care of. So basically what you would do here is taking care of the creation yourself, take responsibility of it and not leaving it to spring. – nesohc Dec 16 '16 at 09:35
1

Your jobTrigger() and jobBean() methods are not actual beans but factory methods you are using given some inputs to construct CronTriggers and JobDetails to register in your loop found in your quartzScheduler bean by invoking triggers.add(..).

Remove the @Bean and @Scope annotations from the jobTrigger() and jobBean() methods (ideally reduce their visibility too (package private if not private) and you should be good to go.

dimitrisli
  • 20,895
  • 12
  • 59
  • 63
  • Thanks for your answer. I tried to remove the `@Bean` and `@Scope` annotations but the list gets filled with null values. Any suggestion? – Patrick Dec 12 '16 at 15:14
1

After many different tries to get this code working, I found a working solution. Its just a workaround but gives maybe some hints to find the right - not workaround - solution.

What I did:

  1. I changed all my @Configuration classes to @Component except ChannelPartnerProperties and QuartzJobConfig.
  2. I put @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE) to my jobBean() and jobTrigger() method.
  3. I deleted the method parameter of both.
  4. I dont have any other @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE) anywhere else in my code.
  5. I created three counter for counting through my channelPartners, jobConfigs and one for the TriggerGroups name.
  6. I dont use the local Objects in my loops anymore. But use the counters to get the right Objects from my @Autowired channelPartnerProperties which holds all the entries of my yaml file.

After that my QuartzJobConfig class looks like that:

@Configuration
public class QuartzJobConfig implements IJobClass {

    private static int channelPartnerCount = 0;
    private static int jobCount = 0;
    private static int groupCounter = 0;

    @Autowired
    ChannelPartnerProperties channelPartnerProperties;

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public SchedulerFactoryBean quartzScheduler() {
        SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean();

        quartzScheduler.setOverwriteExistingJobs(true);
        quartzScheduler.setSchedulerName("-scheduler");

        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        quartzScheduler.setJobFactory(jobFactory);

        List<CronTrigger> triggers = new ArrayList<>();
        for (ChannelPartner ch : channelPartnerProperties.getChannelPartners()) {
            for (JobConfig jobConfig : ch.getJobConfigs()) {
                triggers.add(jobTrigger().getObject());
                jobCount++;
                groupCounter++;
            }
            channelPartnerCount++;
            jobCount = 0;
        }
        quartzScheduler.setTriggers(triggers.stream().toArray(Trigger[]::new));

        return quartzScheduler;
    }

    @Bean
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public JobDetailFactoryBean jobBean() {
        JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
        jobDetailFactoryBean.setJobClass(findJobByConfig(
                channelPartnerProperties.getChannelPartners().get(channelPartnerCount).getJobConfigs().get(jobCount)));
        jobDetailFactoryBean.setGroup("mainGroup" + groupCounter);
        jobDetailFactoryBean.setName(channelPartnerProperties.getChannelPartners().get(channelPartnerCount)
                .getJobConfigs().get(jobCount).getName());
        jobDetailFactoryBean.setBeanName(channelPartnerProperties.getChannelPartners().get(channelPartnerCount)
                .getJobConfigs().get(jobCount).getName());
        jobDetailFactoryBean.getJobDataMap().put("channelPartner",
                channelPartnerProperties.getChannelPartners().get(channelPartnerCount));
        return jobDetailFactoryBean;
    }

    @Bean
    @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public CronTriggerFactoryBean jobTrigger() {
        CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
        cronTriggerFactoryBean.setJobDetail(jobBean().getObject());
        cronTriggerFactoryBean.setCronExpression(channelPartnerProperties.getChannelPartners().get(channelPartnerCount)
                .getJobConfigs().get(jobCount).getSchedule());
        cronTriggerFactoryBean.setGroup("mainGroup" + groupCounter);
        cronTriggerFactoryBean.setBeanName(channelPartnerProperties.getChannelPartners().get(channelPartnerCount)
                .getJobConfigs().get(jobCount).getName() + "Trigger" + groupCounter);
        return cronTriggerFactoryBean;
    }

    @Override
    public Class<? extends Job> findJobByConfig(JobConfig jobConfig) {
        if (isAllotmentJob(jobConfig) && isHotelJob(jobConfig)) {
            return HotelAndAllotmentEdfJob.class;
        }
        if (isAllotmentJob(jobConfig)) {
            return AllotmentEdfJob.class;
        }
        if (isHotelJob(jobConfig)) {
            return HotelEdfJob.class;
        }
        return HotelAndAllotmentEdfJob.class;
    }

    private boolean isAllotmentJob(JobConfig jobConfig) {
        return jobConfig.isAllotmentEDF();
    }

    private boolean isHotelJob(JobConfig jobConfig) {
        return jobConfig.isHotelEDF();
    }

All the defined jobs in my yaml configuration gets initialized and executed like they defined.

Its a working solution but a workaround. Maybe we find a better one.

Patrick
  • 12,336
  • 15
  • 73
  • 115