From 48bc29c77ac6de48bf1cf3c8706aef24e18a876d Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Mon, 10 Apr 2017 00:59:23 +0200 Subject: [PATCH] Add database initializer for Spring Integration See gh-8881 --- spring-boot-autoconfigure/pom.xml | 5 + .../IntegrationAutoConfiguration.java | 57 ++++++++++- .../IntegrationDatabaseInitializer.java | 52 ++++++++++ .../integration/IntegrationProperties.java | 79 +++++++++++++++ .../IntegrationAutoConfigurationTests.java | 96 +++++++++++++++++-- .../appendix-application-properties.adoc | 4 + .../main/asciidoc/spring-boot-features.adoc | 15 ++- 7 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDatabaseInitializer.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index dd0f773bf8..84eb9a2e64 100755 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -329,6 +329,11 @@ spring-integration-core true + + org.springframework.integration + spring-integration-jdbc + true + org.springframework.integration spring-integration-jmx diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java index 0241676b84..57bdb365de 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -17,25 +17,35 @@ package org.springframework.boot.autoconfigure.integration; import javax.management.MBeanServer; +import javax.sql.DataSource; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.EnableIntegrationManagement; import org.springframework.integration.gateway.GatewayProxyFactoryBean; +import org.springframework.integration.jdbc.lock.DefaultLockRepository; +import org.springframework.integration.jdbc.store.JdbcChannelMessageStore; +import org.springframework.integration.jdbc.store.JdbcMessageStore; import org.springframework.integration.jmx.config.EnableIntegrationMBeanExport; import org.springframework.integration.monitor.IntegrationMBeanExporter; import org.springframework.integration.support.management.IntegrationManagementConfigurer; @@ -48,10 +58,12 @@ import org.springframework.util.StringUtils; * @author Artem Bilan * @author Dave Syer * @author Stephane Nicoll + * @author Vedran Pavic * @since 1.1.0 */ @Configuration @ConditionalOnClass(EnableIntegration.class) +@EnableConfigurationProperties(IntegrationProperties.class) @AutoConfigureAfter(JmxAutoConfiguration.class) public class IntegrationAutoConfiguration { @@ -131,4 +143,47 @@ public class IntegrationAutoConfiguration { } + /** + * Integration JDBC configuration. + */ + @Configuration + @ConditionalOnClass(JdbcMessageStore.class) + @ConditionalOnSingleCandidate(DataSource.class) + protected static class IntegrationJdbcConfiguration { + + @Bean + @ConditionalOnMissingBean + @Conditional(IntegrationSchemaCondition.class) + public IntegrationDatabaseInitializer integrationDatabaseInitializer( + DataSource dataSource, ResourceLoader resourceLoader, + IntegrationProperties properties) { + return new IntegrationDatabaseInitializer(dataSource, resourceLoader, + properties); + } + + } + + static class IntegrationSchemaCondition extends AnyNestedCondition { + + IntegrationSchemaCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(JdbcMessageStore.class) + static class JdbcMessageStoreUsed { + + } + + @ConditionalOnBean(JdbcChannelMessageStore.class) + static class JdbcChannelMessageStoreUsed { + + } + + @ConditionalOnBean(DefaultLockRepository.class) + static class DefaultLockRepositoryUsed { + + } + + } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDatabaseInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDatabaseInitializer.java new file mode 100644 index 0000000000..29d4d8f306 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDatabaseInitializer.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 org.springframework.boot.autoconfigure.integration; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.AbstractDatabaseInitializer; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; + +/** + * Initializer for Spring Integration schema. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +public class IntegrationDatabaseInitializer extends AbstractDatabaseInitializer { + + private final IntegrationProperties.Jdbc properties; + + public IntegrationDatabaseInitializer(DataSource dataSource, + ResourceLoader resourceLoader, IntegrationProperties properties) { + super(dataSource, resourceLoader); + Assert.notNull(properties, "IntegrationProperties must not be null"); + this.properties = properties.getJdbc(); + } + + @Override + protected boolean isEnabled() { + return this.properties.getInitializer().isEnabled(); + } + + @Override + protected String getSchemaLocation() { + return this.properties.getSchema(); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java new file mode 100644 index 0000000000..e0706ef29d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java @@ -0,0 +1,79 @@ +/* + * 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.integration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Spring Integration. + * + * @author Vedran Pavic + * @since 2.0.0 + */ +@ConfigurationProperties(prefix = "spring.integration") +public class IntegrationProperties { + + private final Jdbc jdbc = new Jdbc(); + + public Jdbc getJdbc() { + return this.jdbc; + } + + public static class Jdbc { + + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" + + "integration/jdbc/schema-@@platform@@.sql"; + + /** + * Path to the SQL file to use to initialize the database schema. + */ + private String schema = DEFAULT_SCHEMA_LOCATION; + + private final Initializer initializer = new Initializer(); + + public String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public Initializer getInitializer() { + return this.initializer; + } + + public class Initializer { + + /** + * Create the required integration tables on startup if necessary. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java index 381b945f8e..9ee3f1c08c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -20,12 +20,19 @@ import java.util.Arrays; import java.util.List; import javax.management.MBeanServer; +import javax.sql.DataSource; import org.junit.After; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration.IntegrationComponentScanAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -34,8 +41,11 @@ import org.springframework.context.annotation.Primary; import org.springframework.integration.annotation.IntegrationComponentScan; import org.springframework.integration.annotation.MessagingGateway; import org.springframework.integration.gateway.RequestReplyExchanger; +import org.springframework.integration.jdbc.store.JdbcMessageStore; import org.springframework.integration.support.channel.HeaderChannelRegistry; import org.springframework.integration.support.management.IntegrationManagementConfigurer; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jmx.export.MBeanExporter; import org.springframework.test.context.support.TestPropertySourceUtils; @@ -47,9 +57,13 @@ import static org.mockito.Mockito.mock; * * @author Artem Bilan * @author Stephane Nicoll + * @author Vedran Pavic */ public class IntegrationAutoConfigurationTests { + @Rule + public ExpectedException thrown = ExpectedException.none(); + private AnnotationConfigApplicationContext context; @After @@ -126,12 +140,72 @@ public class IntegrationAutoConfigurationTests { @Test public void primaryExporterIsAllowed() { - load(CustomMBeanExporter.class); + load(new Class[] { CustomMBeanExporter.class }); assertThat(this.context.getBeansOfType(MBeanExporter.class)).hasSize(2); assertThat(this.context.getBean(MBeanExporter.class)) .isSameAs(this.context.getBean("myMBeanExporter")); } + @Test + public void integrationJdbcDatabaseInitializerEnabledWithRequiredBeans() { + load(new Class[] { EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + JdbcTemplateAutoConfiguration.class, + IntergrationJdbcConfiguration.class }); + assertThat(this.context.getBean(IntegrationProperties.class).getJdbc() + .getInitializer().isEnabled()).isTrue(); + JdbcOperations jdbcOperations = this.context.getBean(JdbcOperations.class); + assertThat(jdbcOperations.queryForList("select * from INT_MESSAGE")).isEmpty(); + assertThat(jdbcOperations.queryForList("select * from INT_GROUP_TO_MESSAGE")) + .isEmpty(); + assertThat(jdbcOperations.queryForList("select * from INT_MESSAGE_GROUP")) + .isEmpty(); + assertThat(jdbcOperations.queryForList("select * from INT_LOCK")).isEmpty(); + assertThat(jdbcOperations.queryForList("select * from INT_CHANNEL_MESSAGE")) + .isEmpty(); + } + + @Test + public void integrationJdbcDatabaseInitializerDisableWithoutRequiredBeans() { + load(new Class[] { EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + JdbcTemplateAutoConfiguration.class }); + assertThat(this.context.getBean(IntegrationProperties.class).getJdbc() + .getInitializer().isEnabled()).isTrue(); + JdbcOperations jdbcOperations = this.context.getBean(JdbcOperations.class); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_MESSAGE"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_GROUP_TO_MESSAGE"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_MESSAGE_GROUP"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_LOCK"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_CHANNEL_MESSAGE"); + } + + @Test + public void integrationJdbcDisableDatabaseInitializer() { + load(new Class[] { EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + JdbcTemplateAutoConfiguration.class }, + "spring.integration.jdbc.initializer.enabled=false"); + assertThat(this.context.getBean(IntegrationProperties.class).getJdbc() + .getInitializer().isEnabled()).isFalse(); + JdbcOperations jdbcOperations = this.context.getBean(JdbcOperations.class); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_MESSAGE"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_GROUP_TO_MESSAGE"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_MESSAGE_GROUP"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_LOCK"); + this.thrown.expect(BadSqlGrammarException.class); + jdbcOperations.queryForList("select * from INT_CHANNEL_MESSAGE"); + } + private static void assertDomains(MBeanServer mBeanServer, boolean expected, String... domains) { List actual = Arrays.asList(mBeanServer.getDomains()); @@ -144,12 +218,12 @@ public class IntegrationAutoConfigurationTests { load(null, environment); } - private void load(Class config, String... environment) { + private void load(Class[] configs, String... environment) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - if (config != null) { - ctx.register(config); + EnvironmentTestUtils.addEnvironment(ctx, environment); + if (configs != null) { + ctx.register(configs); } - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(ctx, environment); ctx.register(JmxAutoConfiguration.class, IntegrationAutoConfiguration.class); ctx.refresh(); this.context = ctx; @@ -172,6 +246,16 @@ public class IntegrationAutoConfigurationTests { } + @Configuration + static class IntergrationJdbcConfiguration { + + @Bean + public JdbcMessageStore messageStore(DataSource dataSource) { + return new JdbcMessageStore(dataSource); + } + + } + @MessagingGateway public interface TestGateway extends RequestReplyExchanger { 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 591be0d4e4..48e45ae75b 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -890,6 +890,10 @@ content into your application; rather pick only the properties that you need. spring.batch.schema=classpath:org/springframework/batch/core/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema. spring.batch.table-prefix= # Table prefix for all the batch meta-data tables. + # SPRING INTEGRATION ({sc-spring-boot-autoconfigure}/integration/IntegrationProperties.{sc-ext}[IntegrationProperties]) + spring.integration.jdbc.initializer.enabled=true # Create the required integration tables on startup if necessary. + spring.integration.jdbc.schema=classpath:org/springframework/integration/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema. + # JMS ({sc-spring-boot-autoconfigure}/jms/JmsProperties.{sc-ext}[JmsProperties]) spring.jms.jndi-name= # Connection factory JNDI name. When set, takes precedence to others connection factory auto-configurations. spring.jms.listener.acknowledge-mode= # Acknowledge mode of the container. By default, the listener is transacted with automatic acknowledgment. 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 9a159859b5..391549c813 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -5113,10 +5113,19 @@ Spring Boot offers several conveniences for working with Spring Integration, inc the `spring-boot-starter-integration` '`Starter`'. Spring Integration provides abstractions over messaging and also other transports such as HTTP, TCP etc. If Spring Integration is available on your classpath it will be initialized through the -`@EnableIntegration` annotation. Message processing statistics will be published over JMX -if `'spring-integration-jmx'` is also on the classpath. See the +`@EnableIntegration` annotation. + +Spring Boot will also configure some features that are triggered by the presence of +additional Spring Integration modules. Message processing statistics will be published +over JMX if `'spring-integration-jmx'` is also on the classpath. If +`'spring-integration-jdbc'` is available on the classpath default database schema will be +initialized using `'IntegrationDatabaseInitializer'`, which can be further customized +using configuration properties. + +See the {sc-spring-boot-autoconfigure}/integration/IntegrationAutoConfiguration.{sc-ext}[`IntegrationAutoConfiguration`] -class for more details. +and {sc-spring-boot-autoconfigure}/integration/IntegrationProperties.{sc-ext}[`IntegrationProperties`] +classes for more details.