Add support for HikariDataSource

We still prefer Tomcat if it is available (that can change
if the community asks loudly enough). Hikari is supported
via the same spring.datasource.* properties as Tomcat (and
DBCP), with some modifications:

* The validation and timeout settings are not as fine-grained
in Hikari, so many of them will simply be ignored. The most
common options (url, username, password, driverClassName) all
work as expected.

* The Hikari team recommends using a vendor-specific DataSource
via spring.datasource.dataSourceClassName and supplying it with
Properties (spring.datasource.hikari.*).

Hikari prefers the JDBC4 isValid() API (encapsulates vendor-
specific queries) which is probably a good thing, but we
haven't provided any explicit support or testing for that yet.

Fixes gh-418
This commit is contained in:
Dave Syer 2014-04-30 09:49:35 +01:00
parent f46d281b22
commit 50190a4de7
6 changed files with 320 additions and 5 deletions

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
@ -61,6 +62,11 @@
<artifactId>tomcat-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>

View File

@ -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");
}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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> T getField(Class<?> target, String name) {
Field field = ReflectionUtils.findField(target, name, null);
ReflectionUtils.makeAccessible(field);
return (T) ReflectionUtils.getField(field, target);
}
}

View File

@ -61,6 +61,7 @@
<hibernate-entitymanager.version>${hibernate.version}</hibernate-entitymanager.version>
<hibernate-jpa-api.version>1.0.1.Final</hibernate-jpa-api.version>
<hibernate-validator.version>5.0.3.Final</hibernate-validator.version>
<hikaricp.version>1.3.5</hikaricp.version>
<httpclient.version>4.3.3</httpclient.version>
<httpasyncclient.version>4.0.1</httpasyncclient.version>
<hsqldb.version>2.3.2</hsqldb.version>
@ -145,6 +146,11 @@
<artifactId>jackson-datatype-joda</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>