diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 2fbb97c8de1..258b19a5eb2 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.springframework.boot @@ -61,6 +62,11 @@ tomcat-jdbc true + + com.zaxxer + HikariCP + true + org.eclipse.jetty jetty-webapp diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index 5224b70f506..ad0d88961a9 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -156,6 +156,13 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { } + @Conditional(DataSourceAutoConfiguration.HikariDatabaseCondition.class) + @ConditionalOnMissingBean(DataSource.class) + @Import(HikariDataSourceConfiguration.class) + protected static class HikariConfiguration { + + } + @Conditional(DataSourceAutoConfiguration.BasicDatabaseCondition.class) @ConditionalOnMissingBean(DataSource.class) @Import(CommonsDataSourceConfiguration.class) @@ -260,6 +267,8 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { */ static class BasicDatabaseCondition extends NonEmbeddedDatabaseCondition { + private final Condition hikariCondition = new HikariDatabaseCondition(); + private final Condition tomcatCondition = new TomcatDatabaseCondition(); @Override @@ -270,7 +279,30 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - if (matches(context, metadata, this.tomcatCondition)) { + if (anyMatches(context, metadata, this.hikariCondition, this.tomcatCondition)) { + return ConditionOutcome.noMatch("other DataSource"); + } + return super.getMatchOutcome(context, metadata); + } + + } + + /** + * {@link Condition} to detect when a Hikari DataSource backed database is used. + */ + static class HikariDatabaseCondition extends NonEmbeddedDatabaseCondition { + + private final Condition tomcatCondition = new TomcatDatabaseCondition(); + + @Override + protected String getDataSourceClassName() { + return "com.zaxxer.hikari.HikariDataSource"; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + if (anyMatches(context, metadata, this.tomcatCondition)) { return ConditionOutcome.noMatch("Tomcat DataSource"); } return super.getMatchOutcome(context, metadata); @@ -295,6 +327,8 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { */ static class EmbeddedDatabaseCondition extends SpringBootCondition { + private final SpringBootCondition hikariCondition = new HikariDatabaseCondition(); + private final SpringBootCondition tomcatCondition = new TomcatDatabaseCondition(); private final SpringBootCondition dbcpCondition = new BasicDatabaseCondition(); @@ -302,7 +336,8 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - if (anyMatches(context, metadata, this.tomcatCondition, this.dbcpCondition)) { + if (anyMatches(context, metadata, this.hikariCondition, this.tomcatCondition, + this.dbcpCondition)) { return ConditionOutcome .noMatch("existing non-embedded database detected"); } @@ -321,6 +356,8 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { */ static class DatabaseCondition extends SpringBootCondition { + private final SpringBootCondition hikariCondition = new HikariDatabaseCondition(); + private final SpringBootCondition tomcatCondition = new TomcatDatabaseCondition(); private final SpringBootCondition dbcpCondition = new BasicDatabaseCondition(); @@ -331,8 +368,8 @@ public class DataSourceAutoConfiguration implements EnvironmentAware { public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - if (anyMatches(context, metadata, this.tomcatCondition, this.dbcpCondition, - this.embeddedCondition)) { + if (anyMatches(context, metadata, this.hikariCondition, this.tomcatCondition, + this.dbcpCondition, this.embeddedCondition)) { return ConditionOutcome.match("existing auto database detected"); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfiguration.java new file mode 100644 index 00000000000..618f859be85 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfiguration.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2014 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 java.util.Properties; + +import javax.annotation.PreDestroy; +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import com.zaxxer.hikari.HikariDataSource; + +/** + * Configuration for a HikariCP database pool. The HikariCP pool is a popular data source + * implementation that provides high performance as well as some useful opinionated + * defaults. For compatibility with other DataSource implementations accepts configuration + * via properties in "spring.datasource.*", e.g. "url", "driverClassName", "username", + * "password" (and some others but the full list supported by the Tomcat pool is not + * applicable). Note that the Hikari team recommends using a "dataSourceClassName" and a + * Properties instance (specified here as "spring.datasource.hikari.*"). This makes the + * binding potentially vendor specific, but gives you full control of all the native + * features in the vendor's DataSource. + * + * @author Dave Syer + * @see DataSourceAutoConfiguration + */ +@Configuration +public class HikariDataSourceConfiguration extends AbstractDataSourceConfiguration { + + private String dataSourceClassName; + private String username; + + private HikariDataSource pool; + private Properties hikari = new Properties(); + + @Bean(destroyMethod = "shutdown") + public DataSource dataSource() { + this.pool = new HikariDataSource(); + if (this.dataSourceClassName == null) { + this.pool.setDriverClassName(getDriverClassName()); + } + else { + this.pool.setDataSourceClassName(this.dataSourceClassName); + this.pool.setDataSourceProperties(this.hikari); + } + this.pool.setJdbcUrl(getUrl()); + if (getUsername() != null) { + this.pool.setUsername(getUsername()); + } + if (getPassword() != null) { + this.pool.setPassword(getPassword()); + } + this.pool.setMaximumPoolSize(getMaxActive()); + this.pool.setMinimumIdle(getMinIdle()); + if (isTestOnBorrow()) { + this.pool.setConnectionInitSql(getValidationQuery()); + } + else { + this.pool.setConnectionTestQuery(getValidationQuery()); + } + if (getMaxWaitMillis() != null) { + this.pool.setMaxLifetime(getMaxWaitMillis()); + } + return this.pool; + } + + @PreDestroy + public void close() { + if (this.pool != null) { + this.pool.close(); + } + } + + /** + * @param dataSourceClassName the dataSourceClassName to set + */ + public void setDataSourceClassName(String dataSourceClassName) { + this.dataSourceClassName = dataSourceClassName; + } + + @Override + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the hikari data source properties + */ + public Properties getHikari() { + return this.hikari; + } + + @Override + protected String getUsername() { + if (StringUtils.hasText(this.username)) { + return this.username; + } + if (this.dataSourceClassName == null + && EmbeddedDatabaseConnection.isEmbedded(getDriverClassName())) { + return "sa"; + } + return null; + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 56a564e83b9..885aa73cd3d 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.net.URL; +import java.net.URLClassLoader; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverPropertyInfo; @@ -44,6 +46,8 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.util.ClassUtils; +import com.zaxxer.hikari.HikariDataSource; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -73,6 +77,30 @@ public class DataSourceAutoConfigurationTests { assertNotNull(this.context.getBean(DataSource.class)); } + @Test + public void testTomcatIsFallback() throws Exception { + EnvironmentTestUtils.addEnvironment(this.context, + "spring.datasource.driverClassName:org.hsqldb.jdbcDriver", + "spring.datasource.url:jdbc:hsqldb:mem:testdb"); + this.context.setClassLoader(new URLClassLoader(new URL[0], getClass() + .getClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (name.startsWith("org.apache.tomcat")) { + throw new ClassNotFoundException(); + } + return super.loadClass(name, resolve); + } + }); + this.context.register(DataSourceAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + DataSource bean = this.context.getBean(DataSource.class); + HikariDataSource pool = (HikariDataSource) bean; + assertEquals("jdbc:hsqldb:mem:testdb", pool.getJdbcUrl()); + } + @Test public void testEmbeddedTypeDefaultsUsername() throws Exception { EnvironmentTestUtils.addEnvironment(this.context, diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java new file mode 100644 index 00000000000..6593a8b46b9 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2014 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 java.lang.reflect.Field; + +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.util.ReflectionUtils; + +import com.zaxxer.hikari.HikariDataSource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Tests for {@link HikariDataSourceConfiguration}. + * + * @author Dave Syer + */ +public class HikariDataSourceConfigurationTests { + + private static final String PREFIX = "spring.datasource."; + + private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + @After + public void restore() { + EmbeddedDatabaseConnection.override = null; + } + + @Test + public void testDataSourceExists() throws Exception { + this.context.register(HikariDataSourceConfiguration.class); + this.context.refresh(); + assertNotNull(this.context.getBean(DataSource.class)); + assertNotNull(this.context.getBean(HikariDataSource.class)); + } + + @Test + public void testDataSourcePropertiesOverridden() throws Exception { + this.context.register(HikariDataSourceConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, PREFIX + + "url:jdbc:foo//bar/spam"); + EnvironmentTestUtils.addEnvironment(this.context, PREFIX + "maxWait:1234"); + this.context.refresh(); + HikariDataSource ds = this.context.getBean(HikariDataSource.class); + assertEquals("jdbc:foo//bar/spam", ds.getJdbcUrl()); + assertEquals(1234, ds.getMaxLifetime()); + // TODO: test JDBC4 isValid() + } + + @Test + public void testDataSourceGenericPropertiesOverridden() throws Exception { + this.context.register(HikariDataSourceConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, PREFIX + + "hikari.databaseName:foo", PREFIX + + "dataSourceClassName:org.h2.JDBCDataSource"); + this.context.refresh(); + HikariDataSource ds = this.context.getBean(HikariDataSource.class); + assertEquals("foo", ds.getDataSourceProperties().getProperty("databaseName")); + } + + @Test + public void testDataSourceDefaultsPreserved() throws Exception { + this.context.register(HikariDataSourceConfiguration.class); + this.context.refresh(); + HikariDataSource ds = this.context.getBean(HikariDataSource.class); + assertEquals(1800000, ds.getMaxLifetime()); + } + + @Test(expected = BeanCreationException.class) + public void testBadUrl() throws Exception { + EmbeddedDatabaseConnection.override = EmbeddedDatabaseConnection.NONE; + this.context.register(HikariDataSourceConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertNotNull(this.context.getBean(DataSource.class)); + } + + @Test(expected = BeanCreationException.class) + public void testBadDriverClass() throws Exception { + EmbeddedDatabaseConnection.override = EmbeddedDatabaseConnection.NONE; + this.context.register(HikariDataSourceConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + this.context.refresh(); + assertNotNull(this.context.getBean(DataSource.class)); + } + + @SuppressWarnings("unchecked") + public static T getField(Class target, String name) { + Field field = ReflectionUtils.findField(target, name, null); + ReflectionUtils.makeAccessible(field); + return (T) ReflectionUtils.getField(field, target); + } +} diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 0cbb6396a6c..9d8b0dff163 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -61,6 +61,7 @@ ${hibernate.version} 1.0.1.Final 5.0.3.Final + 1.3.5 4.3.3 4.0.1 2.3.2 @@ -145,6 +146,11 @@ jackson-datatype-joda ${jackson.version} + + com.zaxxer + HikariCP + ${hikaricp.version} + commons-dbcp commons-dbcp