How to create Spring Beans in a dynamical way. Using Quartz SchedulerFactoryBean

风流意气都作罢 提交于 2020-01-13 22:40:35

问题


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


回答1:


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.




回答2:


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.




回答3:


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.



来源:https://stackoverflow.com/questions/41022276/how-to-create-spring-beans-in-a-dynamical-way-using-quartz-schedulerfactorybean

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!