From 17be9052134353345a6bce6ebbdc3ece413232aa Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Fri, 15 Feb 2019 16:31:02 +0200 Subject: [PATCH] This commit adds spring.datasource.lazy-connection property into a DataSourceProperties class to enable a LazyConnectionDataSourceBeanPostProcessor. BeanPostProcessor wraps an auto-configured DataSource in a LazyConnectionDataSourceProxy. It will be added only if a spring.datasource.lazy-connection property has true and a LazyConnectionDataSourceProxy class is present. The default value of the spring.datasource.lazy-connection property is false, which means a lazy-connection feature is disabled by default. closes gh-15480 --- .../jdbc/DataSourceAutoConfiguration.java | 8 ++- .../jdbc/DataSourceProperties.java | 15 ++++- ...ConnectionDataSourceBeanPostProcessor.java | 42 ++++++++++++ ...LazyConnectionDataSourceConfiguration.java | 41 ++++++++++++ .../jdbc/XADataSourceAutoConfiguration.java | 2 + .../DataSourceAutoConfigurationTests.java | 65 ++++++++++++++++++- .../XADataSourceAutoConfigurationTests.java | 23 ++++++- .../main/asciidoc/spring-boot-features.adoc | 17 ++++- 8 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceBeanPostProcessor.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceConfiguration.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index 4c4e8d2c9402..f45745eece02 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -57,7 +57,8 @@ public class DataSourceAutoConfiguration { @Configuration @Conditional(EmbeddedDatabaseCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) - @Import(EmbeddedDataSourceConfiguration.class) + @Import({ EmbeddedDataSourceConfiguration.class, + LazyConnectionDataSourceConfiguration.class }) protected static class EmbeddedDatabaseConfiguration { } @@ -67,7 +68,8 @@ protected static class EmbeddedDatabaseConfiguration { @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, - DataSourceJmxConfiguration.class }) + DataSourceJmxConfiguration.class, + LazyConnectionDataSourceConfiguration.class }) protected static class PooledDataSourceConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java index aa9878a2e7d1..3f9c97a5deae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -149,6 +149,11 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB */ private Charset sqlScriptEncoding; + /** + * Whether datasource must be wrapped in a LazyConnectionDataSourceProxy. + */ + private boolean lazyConnection = false; + private EmbeddedDatabaseConnection embeddedDatabaseConnection = EmbeddedDatabaseConnection.NONE; private Xa xa = new Xa(); @@ -478,6 +483,14 @@ public void setXa(Xa xa) { this.xa = xa; } + public boolean getLazyConnection() { + return this.lazyConnection; + } + + public void setLazyConnection(boolean lazyConnection) { + this.lazyConnection = lazyConnection; + } + /** * XA Specific datasource settings. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceBeanPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceBeanPostProcessor.java new file mode 100644 index 000000000000..ac5072ac85c7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceBeanPostProcessor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2019 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.jdbc; + +import javax.sql.DataSource; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +/** + * {@link BeanPostProcessor} to wrap a {@link DataSource} with a + * {@link LazyConnectionDataSourceProxy}. + * + * @author Dmytro Nosan + */ +class LazyConnectionDataSourceBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof DataSource) { + return new LazyConnectionDataSourceProxy((DataSource) bean); + } + return bean; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceConfiguration.java new file mode 100644 index 000000000000..e293854e7976 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/LazyConnectionDataSourceConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2019 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.jdbc; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +/** + * Configuration to register {@link LazyConnectionDataSourceBeanPostProcessor}, if a + * {@code spring.datasource.lazy-connection} property is present and value is + * {@code true}. + * + * @author Dmytro Nosan + */ +@Configuration +@ConditionalOnClass(LazyConnectionDataSourceProxy.class) +@ConditionalOnProperty(prefix = "spring.datasource", name = "lazy-connection", havingValue = "true") +class LazyConnectionDataSourceConfiguration { + + @Bean + public static LazyConnectionDataSourceBeanPostProcessor lazyConnectionDataSourceBeanPostProcessor() { + return new LazyConnectionDataSourceBeanPostProcessor(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java index afe3a9eeb181..3dbac8eb1729 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.boot.jdbc.XADataSourceWrapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -59,6 +60,7 @@ EmbeddedDatabaseType.class }) @ConditionalOnBean(XADataSourceWrapper.class) @ConditionalOnMissingBean(DataSource.class) +@Import(LazyConnectionDataSourceConfiguration.class) public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { private ClassLoader classLoader; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index f16dd6f56a9a..4a419bf8681e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -38,6 +38,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jdbc.DataSourceUnwrapper; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.test.context.FilteredClassLoader; @@ -46,7 +47,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -140,7 +143,7 @@ public void commonsDbcp2ValidatesConnectionByDefault() { Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat"), (dataSource) -> { assertThat(dataSource.getTestOnBorrow()).isTrue(); assertThat(dataSource.getValidationQuery()).isNull(); // Use - // Connection#isValid() + // Connection#isValid() }); } @@ -224,6 +227,64 @@ public void testDataSourceIsInitializedEarly() { .isTrue()); } + @Test + public void ignoreLazyConnectionOverriddenDataSource() { + this.contextRunner.withPropertyValues("spring.datasource.lazy-connection=true") + .withUserConfiguration(TestDataSourceConfiguration.class) + .run((context) -> assertThat(context).getBean(DataSource.class) + .isInstanceOf(BasicDataSource.class)); + } + + @Test + public void lazyConnectionEmbeddedDataSource() { + assertLazyDataSource(EmbeddedDatabase.class, + Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat", + "org.apache.commons.dbcp2"), + (dataSource) -> assertThat(DataSourceUnwrapper.unwrap(dataSource, + SimpleDriverDataSource.class)).isNotNull() + .extracting(SimpleDriverDataSource::getUrl).asString() + .startsWith("jdbc:h2:mem:testdb")); + } + + @Test + public void lazyConnectionDefaultDataSource() { + assertLazyDataSource(HikariDataSource.class, Collections.emptyList(), + (dataSource) -> assertThat(dataSource.getJdbcUrl()) + .startsWith("jdbc:hsqldb:mem:testdb")); + } + + @Test + public void lazyConnectionTomcatIsFallback() { + assertLazyDataSource(org.apache.tomcat.jdbc.pool.DataSource.class, + Collections.singletonList("com.zaxxer.hikari"), + (dataSource) -> assertThat(dataSource.getUrl()) + .startsWith("jdbc:hsqldb:mem:testdb")); + } + + @Test + public void lazyConnectionCommonsDbcp2IsFallback() { + assertLazyDataSource(BasicDataSource.class, + Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat"), + (dataSource) -> assertThat(dataSource.getUrl()) + .startsWith("jdbc:hsqldb:mem:testdb")); + } + + private void assertLazyDataSource(Class expectedType, + List hiddenPackages, Consumer consumer) { + FilteredClassLoader classLoader = new FilteredClassLoader( + StringUtils.toStringArray(hiddenPackages)); + this.contextRunner.withPropertyValues("spring.datasource.lazy-connection=true") + .withClassLoader(classLoader).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource) + .isInstanceOf(LazyConnectionDataSourceProxy.class); + DataSource targetDataSource = ((LazyConnectionDataSourceProxy) dataSource) + .getTargetDataSource(); + assertThat(targetDataSource).isInstanceOf(expectedType); + consumer.accept(expectedType.cast(targetDataSource)); + }); + } + private void assertDataSource(Class expectedType, List hiddenPackages, Consumer consumer) { FilteredClassLoader classLoader = new FilteredClassLoader( diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java index 9d3819c8594d..1cc0d3d820f7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.sql.Connection; + import javax.sql.DataSource; import javax.sql.XADataSource; @@ -28,8 +30,10 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; /** @@ -39,6 +43,17 @@ */ public class XADataSourceAutoConfigurationTests { + @Test + public void lazyXaDataSource() { + ApplicationContext context = createContext(WrapExisting.class, + "spring.datasource.lazy-connection=true"); + assertThat(context.getBean(DataSource.class)) + .isInstanceOf(LazyConnectionDataSourceProxy.class); + XADataSource source = context.getBean(XADataSource.class); + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + assertThat(wrapper.getXaDataSource()).isEqualTo(source); + } + @Test public void wrapExistingXaDataSource() { ApplicationContext context = createContext(WrapExisting.class); @@ -111,9 +126,11 @@ private static class MockXADataSourceWrapper implements XADataSourceWrapper { private XADataSource dataSource; @Override - public DataSource wrapDataSource(XADataSource dataSource) { + public DataSource wrapDataSource(XADataSource dataSource) throws Exception { this.dataSource = dataSource; - return mock(DataSource.class); + DataSource mockDataSource = mock(DataSource.class); + doReturn(mock(Connection.class)).when(mockDataSource).getConnection(); + return mockDataSource; } public XADataSource getXaDataSource() { diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 5ce90dcd6168..2365dcbbecde 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -3631,7 +3631,22 @@ TIP: See <> for advanced examples, typically to take full control over the configuration of the DataSource. +[[boot-features-configure-lazy-datasource]] +=== Configure a lazy DataSource +Lazy `javax.sql.DataSource` (`org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy`) allows delaying the obtaining of a `javax.sql.Connection` until a `javax.sql.Connection` is really required. +JDBC transaction control can happen without fetching a `javax.sql.Connection` from the pool or communicating with the database. +`Connection` initialization properties like `auto-commit`, `transaction-isolation` and `read-only` +will be kept and applied to the actual JDBC `javax.sql.Connection` as soon as an actual `Connection` is fetched. +Consequently, _commit_ and _rollback_ calls will be *ignored* if no Statements have been created. + +To *enable* this feature `spring.datasource.lazy-connection=true` must be set, by default this feature is disabled. + +[NOTE] +==== +Lazy `javax.sql.DataSource` proxy needs to return wrapped Connections in order to handle lazy fetching of an actual JDBC `Connection`. +Use `javax.sql.Connection#unwrap` or `static org.springframework.boot.jdbc.DataSourceUnwrapper#unwrap` to retrieve the native JDBC `Connection`. +==== [[boot-features-embedded-database-support]] ==== Embedded Database Support @@ -3755,8 +3770,6 @@ example: spring.datasource.tomcat.test-on-borrow=true ---- - - [[boot-features-connecting-to-a-jndi-datasource]] ==== Connection to a JNDI DataSource If you deploy your Spring Boot application to an Application Server, you might want to