diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 2126c5c248..1b7893a057 100755 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -656,6 +656,11 @@ narayana-jts-integration true + + org.quartz-scheduler + quartz + true + org.springframework.boot diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/AutowireCapableBeanJobFactory.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/AutowireCapableBeanJobFactory.java new file mode 100644 index 0000000000..36828976da --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/AutowireCapableBeanJobFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import org.quartz.spi.TriggerFiredBundle; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.scheduling.quartz.SpringBeanJobFactory; +import org.springframework.util.Assert; + +/** + * Subclass of {@link SpringBeanJobFactory} that supports auto-wiring job beans. + * + * @author Vedran Pavic + * @since 2.0.0 + * @see Inject application + * context dependencies in Quartz job beans + */ +class AutowireCapableBeanJobFactory extends SpringBeanJobFactory { + + private final AutowireCapableBeanFactory beanFactory; + + AutowireCapableBeanJobFactory(AutowireCapableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "Bean factory must not be null"); + this.beanFactory = beanFactory; + } + + @Override + protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { + Object jobInstance = super.createJobInstance(bundle); + this.beanFactory.autowireBean(jobInstance); + this.beanFactory.initializeBean(jobInstance, null); + return jobInstance; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/JobStoreType.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/JobStoreType.java new file mode 100644 index 0000000000..e7673d4288 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/JobStoreType.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +/** + * Define the supported Quartz {@code JobStore}. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +public enum JobStoreType { + + /** + * Store jobs in memory. + */ + MEMORY, + + /** + * Store jobs in the database. + */ + JDBC + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java new file mode 100644 index 0000000000..284db53cf8 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import javax.sql.DataSource; + +import org.quartz.Calendar; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.Trigger; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Quartz Scheduler. + * + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.0.0 + */ +@Configuration +@ConditionalOnClass({ Scheduler.class, SchedulerFactoryBean.class, + PlatformTransactionManager.class }) +@EnableConfigurationProperties(QuartzProperties.class) +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class }) +public class QuartzAutoConfiguration { + + private final QuartzProperties properties; + + private final List customizers; + + private final Executor taskExecutor; + + private final JobDetail[] jobDetails; + + private final Map calendars; + + private final Trigger[] triggers; + + private final ApplicationContext applicationContext; + + public QuartzAutoConfiguration(QuartzProperties properties, + ObjectProvider> customizers, + ObjectProvider taskExecutor, ObjectProvider jobDetails, + ObjectProvider> calendars, + ObjectProvider triggers, + ApplicationContext applicationContext) { + this.properties = properties; + this.customizers = customizers.getIfAvailable(); + this.taskExecutor = taskExecutor.getIfAvailable(); + this.jobDetails = jobDetails.getIfAvailable(); + this.calendars = calendars.getIfAvailable(); + this.triggers = triggers.getIfAvailable(); + this.applicationContext = applicationContext; + } + + @Bean + @ConditionalOnSingleCandidate(DataSource.class) + @ConditionalOnMissingBean + public QuartzDatabaseInitializer quartzDatabaseInitializer(DataSource dataSource, + ResourceLoader resourceLoader) { + return new QuartzDatabaseInitializer(dataSource, resourceLoader, this.properties); + } + + @Bean + @ConditionalOnMissingBean + public SchedulerFactoryBean quartzScheduler() { + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + schedulerFactoryBean.setJobFactory(new AutowireCapableBeanJobFactory( + this.applicationContext.getAutowireCapableBeanFactory())); + if (!this.properties.getProperties().isEmpty()) { + schedulerFactoryBean + .setQuartzProperties(asProperties(this.properties.getProperties())); + } + if (this.taskExecutor != null) { + schedulerFactoryBean.setTaskExecutor(this.taskExecutor); + } + if (this.jobDetails != null && this.jobDetails.length > 0) { + schedulerFactoryBean.setJobDetails(this.jobDetails); + } + if (this.calendars != null && !this.calendars.isEmpty()) { + schedulerFactoryBean.setCalendars(this.calendars); + } + if (this.triggers != null && this.triggers.length > 0) { + schedulerFactoryBean.setTriggers(this.triggers); + } + customize(schedulerFactoryBean); + return schedulerFactoryBean; + } + + private Properties asProperties(Map source) { + Properties properties = new Properties(); + properties.putAll(source); + return properties; + } + + private void customize(SchedulerFactoryBean schedulerFactoryBean) { + if (this.customizers != null) { + for (SchedulerFactoryBeanCustomizer customizer : this.customizers) { + customizer.customize(schedulerFactoryBean); + } + } + } + + @Configuration + @ConditionalOnSingleCandidate(DataSource.class) + protected static class JdbcStoreTypeConfiguration { + + @Bean + public SchedulerFactoryBeanCustomizer dataSourceCustomizer( + QuartzProperties properties, DataSource dataSource, + ObjectProvider transactionManager) { + return schedulerFactoryBean -> { + if (properties.getJobStoreType() == JobStoreType.JDBC) { + schedulerFactoryBean.setDataSource(dataSource); + PlatformTransactionManager txManager = + transactionManager.getIfUnique(); + if (txManager != null) { + schedulerFactoryBean.setTransactionManager(txManager); + } + } + }; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDatabaseInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDatabaseInitializer.java new file mode 100644 index 0000000000..63c3d52ab4 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDatabaseInitializer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.AbstractDatabaseInitializer; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; + +/** + * Initializer for Quartz Scheduler schema. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +public class QuartzDatabaseInitializer extends AbstractDatabaseInitializer { + + private final QuartzProperties properties; + + public QuartzDatabaseInitializer(DataSource dataSource, ResourceLoader resourceLoader, + QuartzProperties properties) { + super(dataSource, resourceLoader); + Assert.notNull(properties, "QuartzProperties must not be null"); + this.properties = properties; + } + + @Override + protected boolean isEnabled() { + return this.properties.getJdbc().isInitializeSchema(); + } + + @Override + protected String getSchemaLocation() { + return this.properties.getJdbc().getSchema(); + } + + @Override + protected String getDatabaseName() { + String databaseName = super.getDatabaseName(); + if ("db2".equals(databaseName)) { + return "db2_v95"; + } + if ("mysql".equals(databaseName)) { + return "mysql_innodb"; + } + if ("postgresql".equals(databaseName)) { + return "postgres"; + } + if ("sqlserver".equals(databaseName)) { + return "sqlServer"; + } + return databaseName; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java new file mode 100644 index 0000000000..7f601444d3 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for the Quartz Scheduler integration. + * + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.0.0 + */ +@ConfigurationProperties("spring.quartz") +public class QuartzProperties { + + /** + * Quartz job store type. + */ + private JobStoreType jobStoreType = JobStoreType.MEMORY; + + /** + * Additional Quartz Scheduler properties. + */ + private final Map properties = new HashMap<>(); + + private final Jdbc jdbc = new Jdbc(); + + public JobStoreType getJobStoreType() { + return this.jobStoreType; + } + + public void setJobStoreType(JobStoreType jobStoreType) { + this.jobStoreType = jobStoreType; + } + + public Map getProperties() { + return this.properties; + } + + public Jdbc getJdbc() { + return this.jdbc; + } + + public static class Jdbc { + + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/quartz/impl/" + + "jdbcjobstore/tables_@@platform@@.sql"; + + /** + * Path to the SQL file to use to initialize the database schema. + */ + private String schema = DEFAULT_SCHEMA_LOCATION; + + /** + * Create the required Quartz Scheduler tables on startup. + */ + private boolean initializeSchema; + + public String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public boolean isInitializeSchema() { + return this.initializeSchema; + } + + public void setInitializeSchema(boolean initializeSchema) { + this.initializeSchema = initializeSchema; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer.java new file mode 100644 index 0000000000..d1d7492e7f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +/** + * Callback interface that can be implemented by beans wishing to customize the Quartz + * {@link SchedulerFactoryBean} before it is fully initialized, in particular to tune its + * configuration. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +public interface SchedulerFactoryBeanCustomizer { + + /** + * Customize the {@link SchedulerFactoryBean}. + * @param schedulerFactoryBean the scheduler to customize + */ + void customize(SchedulerFactoryBean schedulerFactoryBean); + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/package-info.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/package-info.java new file mode 100644 index 0000000000..ddfdec5897 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Quartz Scheduler. + */ +package org.springframework.boot.autoconfigure.quartz; diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 42d7e3c3e2..356299c4b0 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -357,6 +357,10 @@ "name": "spring.mvc.locale-resolver", "defaultValue": "accept-header" }, + { + "name": "spring.quartz.job-store-type", + "defaultValue": "memory" + }, { "name": "spring.rabbitmq.cache.connection.mode", "defaultValue": "channel" diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 14474fb5bd..8d3ba7d5a8 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -92,6 +92,7 @@ org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ +org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\ org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java new file mode 100644 index 0000000000..bbfe90ba70 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java @@ -0,0 +1,296 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.quartz.Calendar; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.calendar.MonthlyCalendar; +import org.quartz.impl.calendar.WeeklyCalendar; +import org.quartz.simpl.RAMJobStore; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scheduling.quartz.LocalDataSourceJobStore; +import org.springframework.scheduling.quartz.LocalTaskExecutorThreadPool; +import org.springframework.scheduling.quartz.QuartzJobBean; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.containsString; + +/** + * Tests for {@link QuartzAutoConfiguration}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +public class QuartzAutoConfigurationTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public OutputCapture output = new OutputCapture(); + + private ConfigurableApplicationContext context; + + @After + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void withNoDataSource() throws Exception { + load(); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getMetaData().getJobStoreClass()) + .isAssignableFrom(RAMJobStore.class); + } + + @Test + public void withDataSourceUseMemoryByDefault() throws Exception { + load(new Class[] { EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class }); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getMetaData().getJobStoreClass()) + .isAssignableFrom(RAMJobStore.class); + } + + @Test + public void withDataSource() throws Exception { + load(new Class[] { QuartzJobsConfiguration.class, + EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class }, + "spring.quartz.job-store-type=jdbc", + "spring.quartz.jdbc.initialize-schema=true"); + testWithDataSource(); + } + + @Test + public void withDataSourceNoTransactionManager() throws Exception { + load(new Class[] { QuartzJobsConfiguration.class, + EmbeddedDataSourceConfiguration.class }, + "spring.quartz.job-store-type=jdbc", + "spring.quartz.jdbc.initialize-schema=true"); + testWithDataSource(); + } + + private void testWithDataSource() throws SchedulerException { + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getMetaData().getJobStoreClass()) + .isAssignableFrom(LocalDataSourceJobStore.class); + JdbcTemplate jdbcTemplate = new JdbcTemplate( + this.context.getBean(DataSource.class)); + assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM QRTZ_JOB_DETAILS", + Integer.class)).isEqualTo(2); + assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM QRTZ_SIMPLE_TRIGGERS", + Integer.class)).isEqualTo(0); + } + + @Test + public void withTaskExecutor() throws Exception { + load(QuartzExecutorConfiguration.class); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getMetaData().getThreadPoolClass()) + .isEqualTo(LocalTaskExecutorThreadPool.class); + } + + @Test + public void withConfiguredJobAndTrigger() throws Exception { + load(QuartzFullConfiguration.class, "test-name=withConfiguredJobAndTrigger"); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getJobDetail(JobKey.jobKey("fooJob"))).isNotNull(); + assertThat(scheduler.getTrigger(TriggerKey.triggerKey("fooTrigger"))).isNotNull(); + Thread.sleep(1000L); + this.output.expect(containsString("withConfiguredJobAndTrigger")); + this.output.expect(containsString("jobDataValue")); + } + + @Test + public void withConfiguredCalendars() throws Exception { + load(QuartzCalendarsConfiguration.class); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getCalendar("weekly")).isNotNull(); + assertThat(scheduler.getCalendar("monthly")).isNotNull(); + } + + @Test + public void withQuartzProperties() throws Exception { + load("spring.quartz.properties.org.quartz.scheduler.instanceId=FOO"); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getSchedulerInstanceId()).isEqualTo("FOO"); + } + + @Test + public void withCustomizer() throws Exception { + load(QuartzCustomConfig.class); + assertThat(this.context.getBeansOfType(Scheduler.class)).hasSize(1); + Scheduler scheduler = this.context.getBean(Scheduler.class); + assertThat(scheduler.getSchedulerName()).isEqualTo("fooScheduler"); + } + + private void load(String... environment) { + load(new Class[0], environment); + } + + private void load(Class config, String... environment) { + load(new Class[] { config }, environment); + } + + private void load(Class[] configs, String... environment) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + TestPropertyValues.of(environment).applyTo(ctx); + if (!ObjectUtils.isEmpty(configs)) { + ctx.register(configs); + } + ctx.register(QuartzAutoConfiguration.class); + ctx.refresh(); + this.context = ctx; + } + + @Configuration + protected static class QuartzJobsConfiguration { + + @Bean + public JobDetail fooJob() { + return JobBuilder.newJob().ofType(FooJob.class).withIdentity("fooJob") + .storeDurably().build(); + } + + @Bean + public JobDetail barJob() { + return JobBuilder.newJob().ofType(FooJob.class).withIdentity("barJob") + .storeDurably().build(); + } + + } + + @Configuration + protected static class QuartzFullConfiguration { + + @Bean + public JobDetail fooJob() { + return JobBuilder.newJob().ofType(FooJob.class).withIdentity("fooJob") + .usingJobData("jobDataKey", "jobDataValue") + .storeDurably().build(); + } + + @Bean + public Trigger fooTrigger() { + SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(10).repeatForever(); + + return TriggerBuilder.newTrigger().forJob(fooJob()).withIdentity("fooTrigger") + .withSchedule(scheduleBuilder).build(); + } + + } + + @Configuration + protected static class QuartzCalendarsConfiguration { + + @Bean + public Calendar weekly() { + return new WeeklyCalendar(); + } + + @Bean + public Calendar monthly() { + return new MonthlyCalendar(); + } + + } + + @Configuration + protected static class QuartzExecutorConfiguration { + + @Bean + public Executor executor() { + return Executors.newSingleThreadExecutor(); + } + + } + + @Configuration + protected static class QuartzCustomConfig { + + @Bean + public SchedulerFactoryBeanCustomizer customizer() { + return schedulerFactoryBean -> schedulerFactoryBean + .setSchedulerName("fooScheduler"); + } + + } + + public static class FooJob extends QuartzJobBean { + + @Autowired + private Environment env; + + private String jobDataKey; + + @Override + protected void executeInternal(JobExecutionContext context) + throws JobExecutionException { + System.out.println(this.env.getProperty("test-name", "unknown") + " - " + + this.jobDataKey); + } + + public void setJobDataKey(String jobDataKey) { + this.jobDataKey = jobDataKey; + } + + } + +} diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 574208707a..8636fa1651 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -138,6 +138,7 @@ 3.0.0-M01 4.1.11.Final 42.1.1 + 2.3.0 4.1.4 Bismuth-M1 1.3.0 @@ -491,6 +492,11 @@ spring-boot-starter-reactor-netty 2.0.0.BUILD-SNAPSHOT + + org.springframework.boot + spring-boot-starter-quartz + 2.0.0.BUILD-SNAPSHOT + org.springframework.boot spring-boot-starter-security @@ -1970,6 +1976,11 @@ lombok ${lombok.version} + + org.quartz-scheduler + quartz + ${quartz.version} + org.seleniumhq.selenium htmlunit-driver diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index be9b741429..67367e74c6 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -129,6 +129,12 @@ content into your application; rather pick only the properties that you need. spring.profiles.active= # Comma-separated list (or list if using YAML) of <>. spring.profiles.include= # Unconditionally activate the specified comma separated profiles (or list of profiles if using YAML). + # QUARTZ SCHEDULER ({sc-spring-boot-autoconfigure}/quartz/QuartzProperties.{sc-ext}[QuartzProperties]) + spring.quartz.job-store-type=memory # Quartz job store type. + spring.quartz.properties.*= # Additional Quartz Scheduler properties. + spring.quartz.jdbc.initialize-schema=false # Create the required Quartz Scheduler tables on startup. + spring.quartz.jdbc.schema=classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql # Path to the SQL file to use to initialize the database schema. + # Reactor spring.reactor.stacktrace-mode.enabled=false # Set whether Reactor should collect stacktrace information at runtime. diff --git a/spring-boot-docs/src/main/asciidoc/index.adoc b/spring-boot-docs/src/main/asciidoc/index.adoc index 21a401598b..2672eda029 100644 --- a/spring-boot-docs/src/main/asciidoc/index.adoc +++ b/spring-boot-docs/src/main/asciidoc/index.adoc @@ -1,5 +1,5 @@ = Spring Boot Reference Guide -Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson; Marcel Overdijk; Christian Dupuis; Sébastien Deleuze; Michael Simons +Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson; Marcel Overdijk; Christian Dupuis; Sébastien Deleuze; Michael Simons; Vedran Pavić :doctype: book :toc: :toclevels: 4 diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 76974087b4..6b3b9580cf 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -5151,6 +5151,71 @@ caching is enabled. +[[boot-features-quartz]] +== Quartz Scheduler +Spring Boot offers several conveniences for working with the Quartz scheduler, including +the `spring-boot-starter-quartz` '`Starter`'. If Quartz is available, a `Scheduler` will +be auto-configured (via the `SchedulerFactoryBean` abstraction). + +Beans of the following types will be automatically picked up and associated with the +the `Scheduler`: + +* `JobDetail`: defines a particular Job. `JobDetail` instance can easily be built with +the `JobBuilder` API +* `Calendar` +* `Trigger`: defines when a particular job is triggered + +By default, an in-memory `JobStore` will be used. However, it is possible to configure +a JDBC-based store if a `DataSource` bean is available in your application and if the +`spring.quartz.job-store-type` property is configured accordingly: + +[source,properties,indent=0] +---- + spring.quartz.job-store-type=jdbc +---- + +When the jdbc store is used, the schema can be initialized on startup: + +[source,properties,indent=0] +---- + spring.quartz.jdbc.initialize-schema=true +---- + +NOTE: The database is detected by default and initialized using the standard scripts +provided with the Quartz library. It is also possible to provide a custom script using the +`spring.quartz.jdbc.schema` property. + +Quartz Scheduler configuration can be customized using Quartz configuration properties (see +`spring.quartz.properties.*`) and `SchedulerFactoryBeanCustomizer` beans which allows +programmatic `SchedulerFactoryBean` customization. + +Job can define setters to inject data map properties. Regular beans can also be injected +in a similar manner: + +[source,java,indent=0] +---- + public class SampleJob extends QuartzJobBean { + + private MyService myService; + private String name; + + // Inject "MyService" bean + public void setMyService(MyService myService) { ... } + + // Inject the "name" job data property + public void setName(String name) { ... } + + @Override + protected void executeInternal(JobExecutionContext context) + throws JobExecutionException { + ... + } + + } +---- + + + [[boot-features-integration]] == Spring Integration Spring Boot offers several conveniences for working with Spring Integration, including diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 7a263b0994..8fa1bc4280 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -71,6 +71,7 @@ spring-boot-sample-parent-context spring-boot-sample-profile spring-boot-sample-property-validation + spring-boot-sample-quartz spring-boot-sample-secure spring-boot-sample-secure-oauth2 spring-boot-sample-secure-oauth2-actuator diff --git a/spring-boot-samples/spring-boot-sample-quartz/README.adoc b/spring-boot-samples/spring-boot-sample-quartz/README.adoc new file mode 100644 index 0000000000..e90a709db9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-quartz/README.adoc @@ -0,0 +1,11 @@ +== Spring Boot Quartz Sample + +This sample demonstrates the Quartz auto-configuration support. + +The sample uses Maven. It can be built and run from the command line: + +---- +$ mvn spring-boot:run +---- + +Console log will now show "Hello World!" from `SampleJob` every 2 seconds. diff --git a/spring-boot-samples/spring-boot-sample-quartz/pom.xml b/spring-boot-samples/spring-boot-sample-quartz/pom.xml new file mode 100644 index 0000000000..a161afa0be --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-quartz/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 2.0.0.BUILD-SNAPSHOT + + spring-boot-sample-quartz + Spring Boot Quartz Sample + Spring Boot Quartz Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-quartz/src/main/java/sample/quartz/SampleJob.java b/spring-boot-samples/spring-boot-sample-quartz/src/main/java/sample/quartz/SampleJob.java new file mode 100644 index 0000000000..b1d3db23ca --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-quartz/src/main/java/sample/quartz/SampleJob.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.quartz; + +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import org.springframework.scheduling.quartz.QuartzJobBean; + +public class SampleJob extends QuartzJobBean { + + private String name; + + // Invoked if a Job data map entry with that name + public void setName(String name) { + this.name = name; + } + + @Override + protected void executeInternal(JobExecutionContext context) + throws JobExecutionException { + System.out.println(String.format("Hello %s!", this.name)); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-quartz/src/main/java/sample/quartz/SampleQuartzApplication.java b/spring-boot-samples/spring-boot-sample-quartz/src/main/java/sample/quartz/SampleQuartzApplication.java new file mode 100644 index 0000000000..0836fa78a4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-quartz/src/main/java/sample/quartz/SampleQuartzApplication.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.quartz; + +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SampleQuartzApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleQuartzApplication.class, args); + } + + @Bean + public JobDetail sampleJobDetail() { + return JobBuilder.newJob().ofType(SampleJob.class).withIdentity("sampleJob") + .usingJobData("name", "World") + .storeDurably().build(); + } + + @Bean + public Trigger sampleJobTrigger() { + SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(2).repeatForever(); + + return TriggerBuilder.newTrigger().forJob(sampleJobDetail()) + .withIdentity("sampleTrigger").withSchedule(scheduleBuilder).build(); + } + +} diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index eaff2e49ab..ddf430ce25 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -59,6 +59,7 @@ spring-boot-starter-mustache spring-boot-starter-actuator spring-boot-starter-parent + spring-boot-starter-quartz spring-boot-starter-reactor-netty spring-boot-starter-security spring-boot-starter-social-facebook diff --git a/spring-boot-starters/spring-boot-starter-quartz/pom.xml b/spring-boot-starters/spring-boot-starter-quartz/pom.xml new file mode 100644 index 0000000000..a2f0c7489f --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-quartz/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starters + 2.0.0.BUILD-SNAPSHOT + + spring-boot-starter-quartz + Spring Boot Quartz Starter + Spring Boot Quartz Starter + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter + + + org.springframework + spring-context-support + + + org.springframework + spring-tx + + + org.quartz-scheduler + quartz + + + diff --git a/spring-boot-starters/spring-boot-starter-quartz/src/main/resources/META-INF/spring.provides b/spring-boot-starters/spring-boot-starter-quartz/src/main/resources/META-INF/spring.provides new file mode 100644 index 0000000000..55020b99f9 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-quartz/src/main/resources/META-INF/spring.provides @@ -0,0 +1 @@ +provides: spring-context-support,spring-tx,quartz