org.springframework.boot.jdbc.DataSourceBuilder Maven / Gradle / Ivy
/*
* Copyright 2012-2023 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
*
* https://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.jdbc;
import java.beans.PropertyVetoException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import javax.sql.DataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.zaxxer.hikari.HikariDataSource;
import oracle.jdbc.datasource.OracleDataSource;
import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceImpl;
import org.apache.commons.dbcp2.BasicDataSource;
import org.h2.jdbcx.JdbcDataSource;
import org.postgresql.ds.PGSimpleDataSource;
import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Convenience class for building a {@link DataSource}. Provides a limited subset of the
* properties supported by a typical {@link DataSource} as well as detection logic to pick
* the most suitable pooling {@link DataSource} implementation.
*
* The following pooling {@link DataSource} implementations are supported by this builder.
* When no {@link #type(Class) type} has been explicitly set, the first available pool
* implementation will be picked:
*
* - Hikari ({@code com.zaxxer.hikari.HikariDataSource})
* - Tomcat JDBC Pool ({@code org.apache.tomcat.jdbc.pool.DataSource})
* - Apache DBCP2 ({@code org.apache.commons.dbcp2.BasicDataSource})
* - Oracle UCP ({@code oracle.ucp.jdbc.PoolDataSourceImpl})
*
*
* The following non-pooling {@link DataSource} implementations can be used when
* explicitly set as a {@link #type(Class) type}:
*
* - Spring's {@code SimpleDriverDataSource}
* ({@code org.springframework.jdbc.datasource.SimpleDriverDataSource})
* - Oracle ({@code oracle.jdbc.datasource.OracleDataSource})
* - H2 ({@code org.h2.jdbcx.JdbcDataSource})
* - Postgres ({@code org.postgresql.ds.PGSimpleDataSource})
* - Any {@code DataSource} implementation with appropriately named methods
*
*
* This class is commonly used in an {@code @Bean} method and often combined with
* {@code @ConfigurationProperties}.
*
* @param the {@link DataSource} type being built
* @author Dave Syer
* @author Madhura Bhave
* @author Fabio Grassi
* @author Phillip Webb
* @since 2.0.0
* @see #create()
* @see #create(ClassLoader)
* @see #derivedFrom(DataSource)
*/
public final class DataSourceBuilder {
private final ClassLoader classLoader;
private final Map values = new HashMap<>();
private Class type;
private final DataSource deriveFrom;
private DataSourceBuilder(ClassLoader classLoader) {
this.classLoader = classLoader;
this.deriveFrom = null;
}
@SuppressWarnings("unchecked")
private DataSourceBuilder(T deriveFrom) {
Assert.notNull(deriveFrom, "DataSource must not be null");
this.classLoader = deriveFrom.getClass().getClassLoader();
this.type = (Class) deriveFrom.getClass();
this.deriveFrom = deriveFrom;
}
/**
* Set the {@link DataSource} type that should be built.
* @param the datasource type
* @param type the datasource type
* @return this builder
*/
@SuppressWarnings("unchecked")
public DataSourceBuilder type(Class type) {
this.type = (Class) type;
return (DataSourceBuilder) this;
}
/**
* Set the URL that should be used when building the datasource.
* @param url the JDBC url
* @return this builder
*/
public DataSourceBuilder url(String url) {
set(DataSourceProperty.URL, url);
return this;
}
/**
* Set the driver class name that should be used when building the datasource.
* @param driverClassName the driver class name
* @return this builder
*/
public DataSourceBuilder driverClassName(String driverClassName) {
set(DataSourceProperty.DRIVER_CLASS_NAME, driverClassName);
return this;
}
/**
* Set the username that should be used when building the datasource.
* @param username the user name
* @return this builder
*/
public DataSourceBuilder username(String username) {
set(DataSourceProperty.USERNAME, username);
return this;
}
/**
* Set the password that should be used when building the datasource.
* @param password the password
* @return this builder
*/
public DataSourceBuilder password(String password) {
set(DataSourceProperty.PASSWORD, password);
return this;
}
private void set(DataSourceProperty property, String value) {
this.values.put(property, value);
}
/**
* Return a newly built {@link DataSource} instance.
* @return the built datasource
*/
public T build() {
DataSourceProperties properties = DataSourceProperties.forType(this.classLoader, this.type);
DataSourceProperties deriveFromProperties = getDeriveFromProperties();
Class extends T> instanceType = (this.type != null) ? this.type : properties.getDataSourceInstanceType();
T dataSource = BeanUtils.instantiateClass(instanceType);
Set applied = new HashSet<>();
for (DataSourceProperty property : DataSourceProperty.values()) {
String value = this.values.get(property);
if (value == null && deriveFromProperties != null && properties.canSet(property)) {
value = deriveFromProperties.get(this.deriveFrom, property);
}
if (value != null) {
properties.set(dataSource, property, value);
applied.add(property);
}
}
if (!applied.contains(DataSourceProperty.DRIVER_CLASS_NAME)
&& properties.canSet(DataSourceProperty.DRIVER_CLASS_NAME)
&& this.values.containsKey(DataSourceProperty.URL)) {
String url = this.values.get(DataSourceProperty.URL);
DatabaseDriver driver = DatabaseDriver.fromJdbcUrl(url);
properties.set(dataSource, DataSourceProperty.DRIVER_CLASS_NAME, driver.getDriverClassName());
}
return dataSource;
}
@SuppressWarnings("unchecked")
private DataSourceProperties getDeriveFromProperties() {
if (this.deriveFrom == null) {
return null;
}
return DataSourceProperties.forType(this.classLoader, (Class) this.deriveFrom.getClass());
}
/**
* Create a new {@link DataSourceBuilder} instance.
* @return a new datasource builder instance
*/
public static DataSourceBuilder> create() {
return create(null);
}
/**
* Create a new {@link DataSourceBuilder} instance.
* @param classLoader the classloader used to discover preferred settings
* @return a new {@link DataSource} builder instance
*/
public static DataSourceBuilder> create(ClassLoader classLoader) {
return new DataSourceBuilder<>(classLoader);
}
/**
* Create a new {@link DataSourceBuilder} instance derived from the specified data
* source. The returned builder can be used to build the same type of
* {@link DataSource} with {@code username}, {@code password}, {@code url} and
* {@code driverClassName} properties copied from the original when not specifically
* set.
* @param dataSource the source {@link DataSource}
* @return a new {@link DataSource} builder
* @since 2.5.0
*/
public static DataSourceBuilder> derivedFrom(DataSource dataSource) {
if (dataSource instanceof EmbeddedDatabase) {
try {
dataSource = dataSource.unwrap(DataSource.class);
}
catch (SQLException ex) {
throw new IllegalStateException("Unable to unwrap embedded database", ex);
}
}
return new DataSourceBuilder<>(dataSource);
}
/**
* Find the {@link DataSource} type preferred for the given classloader.
* @param classLoader the classloader used to discover preferred settings
* @return the preferred {@link DataSource} type
*/
public static Class extends DataSource> findType(ClassLoader classLoader) {
MappedDataSourceProperties> mappings = MappedDataSourceProperties.forType(classLoader, null);
return (mappings != null) ? mappings.getDataSourceInstanceType() : null;
}
/**
* An individual DataSource property supported by the builder.
*/
private enum DataSourceProperty {
URL(false, "url", "URL"),
DRIVER_CLASS_NAME(true, "driverClassName"),
USERNAME(false, "username", "user"),
PASSWORD(false, "password");
private final boolean optional;
private final String[] names;
DataSourceProperty(boolean optional, String... names) {
this.optional = optional;
this.names = names;
}
boolean isOptional() {
return this.optional;
}
@Override
public String toString() {
return this.names[0];
}
Method findSetter(Class> type) {
return findMethod("set", type, String.class);
}
Method findGetter(Class> type) {
return findMethod("get", type);
}
private Method findMethod(String prefix, Class> type, Class>... paramTypes) {
for (String name : this.names) {
String candidate = prefix + StringUtils.capitalize(name);
Method method = ReflectionUtils.findMethod(type, candidate, paramTypes);
if (method != null) {
return method;
}
}
return null;
}
}
private interface DataSourceProperties {
Class extends T> getDataSourceInstanceType();
boolean canSet(DataSourceProperty property);
void set(T dataSource, DataSourceProperty property, String value);
String get(T dataSource, DataSourceProperty property);
static DataSourceProperties forType(ClassLoader classLoader, Class type) {
MappedDataSourceProperties mapped = MappedDataSourceProperties.forType(classLoader, type);
return (mapped != null) ? mapped : new ReflectionDataSourceProperties<>(type);
}
}
private static class MappedDataSourceProperties implements DataSourceProperties {
private final Map> mappedProperties = new HashMap<>();
private final Class dataSourceType;
@SuppressWarnings("unchecked")
MappedDataSourceProperties() {
this.dataSourceType = (Class) ResolvableType.forClass(MappedDataSourceProperties.class, getClass())
.resolveGeneric();
}
@Override
public Class extends T> getDataSourceInstanceType() {
return this.dataSourceType;
}
protected void add(DataSourceProperty property, Getter getter, Setter setter) {
add(property, String.class, getter, setter);
}
protected void add(DataSourceProperty property, Class type, Getter getter, Setter setter) {
this.mappedProperties.put(property, new MappedDataSourceProperty<>(property, type, getter, setter));
}
@Override
public boolean canSet(DataSourceProperty property) {
return this.mappedProperties.containsKey(property);
}
@Override
public void set(T dataSource, DataSourceProperty property, String value) {
MappedDataSourceProperty mappedProperty = getMapping(property);
if (mappedProperty != null) {
mappedProperty.set(dataSource, value);
}
}
@Override
public String get(T dataSource, DataSourceProperty property) {
MappedDataSourceProperty mappedProperty = getMapping(property);
if (mappedProperty != null) {
return mappedProperty.get(dataSource);
}
return null;
}
private MappedDataSourceProperty getMapping(DataSourceProperty property) {
MappedDataSourceProperty mappedProperty = this.mappedProperties.get(property);
UnsupportedDataSourcePropertyException.throwIf(!property.isOptional() && mappedProperty == null,
() -> "No mapping found for " + property);
return mappedProperty;
}
static MappedDataSourceProperties forType(ClassLoader classLoader, Class type) {
MappedDataSourceProperties pooled = lookupPooled(classLoader, type);
if (type == null || pooled != null) {
return pooled;
}
return lookupBasic(classLoader, type);
}
private static MappedDataSourceProperties lookupPooled(ClassLoader classLoader,
Class type) {
MappedDataSourceProperties result = null;
result = lookup(classLoader, type, result, "com.zaxxer.hikari.HikariDataSource",
HikariDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.apache.tomcat.jdbc.pool.DataSource",
TomcatPoolDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.apache.commons.dbcp2.BasicDataSource",
MappedDbcp2DataSource::new);
result = lookup(classLoader, type, result, "oracle.ucp.jdbc.PoolDataSourceImpl",
OraclePoolDataSourceProperties::new, "oracle.jdbc.OracleConnection");
result = lookup(classLoader, type, result, "com.mchange.v2.c3p0.ComboPooledDataSource",
ComboPooledDataSourceProperties::new);
return result;
}
private static MappedDataSourceProperties lookupBasic(ClassLoader classLoader,
Class dataSourceType) {
MappedDataSourceProperties result = null;
result = lookup(classLoader, dataSourceType, result,
"org.springframework.jdbc.datasource.SimpleDriverDataSource", SimpleDataSourceProperties::new);
result = lookup(classLoader, dataSourceType, result, "oracle.jdbc.datasource.OracleDataSource",
OracleDataSourceProperties::new);
result = lookup(classLoader, dataSourceType, result, "org.h2.jdbcx.JdbcDataSource",
H2DataSourceProperties::new);
result = lookup(classLoader, dataSourceType, result, "org.postgresql.ds.PGSimpleDataSource",
PostgresDataSourceProperties::new);
return result;
}
@SuppressWarnings("unchecked")
private static MappedDataSourceProperties lookup(ClassLoader classLoader,
Class dataSourceType, MappedDataSourceProperties existing, String dataSourceClassName,
Supplier> propertyMappingsSupplier, String... requiredClassNames) {
if (existing != null || !allPresent(classLoader, dataSourceClassName, requiredClassNames)) {
return existing;
}
MappedDataSourceProperties> propertyMappings = propertyMappingsSupplier.get();
return (dataSourceType == null
|| propertyMappings.getDataSourceInstanceType().isAssignableFrom(dataSourceType))
? (MappedDataSourceProperties) propertyMappings : null;
}
private static boolean allPresent(ClassLoader classLoader, String dataSourceClassName,
String[] requiredClassNames) {
boolean result = ClassUtils.isPresent(dataSourceClassName, classLoader);
for (String requiredClassName : requiredClassNames) {
result = result && ClassUtils.isPresent(requiredClassName, classLoader);
}
return result;
}
}
private static class MappedDataSourceProperty {
private final DataSourceProperty property;
private final Class type;
private final Getter getter;
private final Setter setter;
MappedDataSourceProperty(DataSourceProperty property, Class type, Getter getter, Setter setter) {
this.property = property;
this.type = type;
this.getter = getter;
this.setter = setter;
}
void set(T dataSource, String value) {
try {
if (this.setter == null) {
UnsupportedDataSourcePropertyException.throwIf(!this.property.isOptional(),
() -> "No setter mapped for '" + this.property + "' property");
return;
}
this.setter.set(dataSource, convertFromString(value));
}
catch (SQLException ex) {
throw new IllegalStateException(ex);
}
}
String get(T dataSource) {
try {
if (this.getter == null) {
UnsupportedDataSourcePropertyException.throwIf(!this.property.isOptional(),
() -> "No getter mapped for '" + this.property + "' property");
return null;
}
return convertToString(this.getter.get(dataSource));
}
catch (SQLException ex) {
throw new IllegalStateException(ex);
}
}
@SuppressWarnings("unchecked")
private V convertFromString(String value) {
if (String.class.equals(this.type)) {
return (V) value;
}
if (Class.class.equals(this.type)) {
return (V) ClassUtils.resolveClassName(value, null);
}
throw new IllegalStateException("Unsupported value type " + this.type);
}
private String convertToString(V value) {
if (String.class.equals(this.type)) {
return (String) value;
}
if (Class.class.equals(this.type)) {
return ((Class>) value).getName();
}
throw new IllegalStateException("Unsupported value type " + this.type);
}
}
private static class ReflectionDataSourceProperties implements DataSourceProperties {
private final Map getters;
private final Map setters;
private final Class dataSourceType;
ReflectionDataSourceProperties(Class dataSourceType) {
Assert.state(dataSourceType != null, "No supported DataSource type found");
Map getters = new HashMap<>();
Map setters = new HashMap<>();
for (DataSourceProperty property : DataSourceProperty.values()) {
putIfNotNull(getters, property, property.findGetter(dataSourceType));
putIfNotNull(setters, property, property.findSetter(dataSourceType));
}
this.dataSourceType = dataSourceType;
this.getters = Collections.unmodifiableMap(getters);
this.setters = Collections.unmodifiableMap(setters);
}
private void putIfNotNull(Map map, DataSourceProperty property, Method method) {
if (method != null) {
map.put(property, method);
}
}
@Override
public Class getDataSourceInstanceType() {
return this.dataSourceType;
}
@Override
public boolean canSet(DataSourceProperty property) {
return this.setters.containsKey(property);
}
@Override
public void set(T dataSource, DataSourceProperty property, String value) {
Method method = getMethod(property, this.setters);
if (method != null) {
ReflectionUtils.invokeMethod(method, dataSource, value);
}
}
@Override
public String get(T dataSource, DataSourceProperty property) {
Method method = getMethod(property, this.getters);
if (method != null) {
return (String) ReflectionUtils.invokeMethod(method, dataSource);
}
return null;
}
private Method getMethod(DataSourceProperty property, Map methods) {
Method method = methods.get(property);
if (method == null) {
UnsupportedDataSourcePropertyException.throwIf(!property.isOptional(),
() -> "Unable to find suitable method for " + property);
return null;
}
ReflectionUtils.makeAccessible(method);
return method;
}
}
@FunctionalInterface
private interface Getter {
V get(T instance) throws SQLException;
}
@FunctionalInterface
private interface Setter {
void set(T instance, V value) throws SQLException;
}
/**
* {@link DataSourceProperties} for Hikari.
*/
private static class HikariDataSourceProperties extends MappedDataSourceProperties {
HikariDataSourceProperties() {
add(DataSourceProperty.URL, HikariDataSource::getJdbcUrl, HikariDataSource::setJdbcUrl);
add(DataSourceProperty.DRIVER_CLASS_NAME, HikariDataSource::getDriverClassName,
HikariDataSource::setDriverClassName);
add(DataSourceProperty.USERNAME, HikariDataSource::getUsername, HikariDataSource::setUsername);
add(DataSourceProperty.PASSWORD, HikariDataSource::getPassword, HikariDataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for Tomcat Pool.
*/
private static class TomcatPoolDataSourceProperties
extends MappedDataSourceProperties {
TomcatPoolDataSourceProperties() {
add(DataSourceProperty.URL, org.apache.tomcat.jdbc.pool.DataSource::getUrl,
org.apache.tomcat.jdbc.pool.DataSource::setUrl);
add(DataSourceProperty.DRIVER_CLASS_NAME, org.apache.tomcat.jdbc.pool.DataSource::getDriverClassName,
org.apache.tomcat.jdbc.pool.DataSource::setDriverClassName);
add(DataSourceProperty.USERNAME, org.apache.tomcat.jdbc.pool.DataSource::getUsername,
org.apache.tomcat.jdbc.pool.DataSource::setUsername);
add(DataSourceProperty.PASSWORD, org.apache.tomcat.jdbc.pool.DataSource::getPassword,
org.apache.tomcat.jdbc.pool.DataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for DBCP2.
*/
private static class MappedDbcp2DataSource extends MappedDataSourceProperties {
MappedDbcp2DataSource() {
add(DataSourceProperty.URL, BasicDataSource::getUrl, BasicDataSource::setUrl);
add(DataSourceProperty.DRIVER_CLASS_NAME, BasicDataSource::getDriverClassName,
BasicDataSource::setDriverClassName);
add(DataSourceProperty.USERNAME, BasicDataSource::getUsername, BasicDataSource::setUsername);
add(DataSourceProperty.PASSWORD, BasicDataSource::getPassword, BasicDataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for Oracle Pool.
*/
private static class OraclePoolDataSourceProperties extends MappedDataSourceProperties {
@Override
public Class extends PoolDataSource> getDataSourceInstanceType() {
return PoolDataSourceImpl.class;
}
OraclePoolDataSourceProperties() {
add(DataSourceProperty.URL, PoolDataSource::getURL, PoolDataSource::setURL);
add(DataSourceProperty.DRIVER_CLASS_NAME, PoolDataSource::getConnectionFactoryClassName,
PoolDataSource::setConnectionFactoryClassName);
add(DataSourceProperty.USERNAME, PoolDataSource::getUser, PoolDataSource::setUser);
add(DataSourceProperty.PASSWORD, null, PoolDataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for C3P0.
*/
private static class ComboPooledDataSourceProperties extends MappedDataSourceProperties {
ComboPooledDataSourceProperties() {
add(DataSourceProperty.URL, ComboPooledDataSource::getJdbcUrl, ComboPooledDataSource::setJdbcUrl);
add(DataSourceProperty.DRIVER_CLASS_NAME, ComboPooledDataSource::getDriverClass, this::setDriverClass);
add(DataSourceProperty.USERNAME, ComboPooledDataSource::getUser, ComboPooledDataSource::setUser);
add(DataSourceProperty.PASSWORD, ComboPooledDataSource::getPassword, ComboPooledDataSource::setPassword);
}
private void setDriverClass(ComboPooledDataSource dataSource, String driverClass) {
try {
dataSource.setDriverClass(driverClass);
}
catch (PropertyVetoException ex) {
throw new IllegalArgumentException(ex);
}
}
}
/**
* {@link DataSourceProperties} for Spring's {@link SimpleDriverDataSource}.
*/
private static class SimpleDataSourceProperties extends MappedDataSourceProperties {
@SuppressWarnings("unchecked")
SimpleDataSourceProperties() {
add(DataSourceProperty.URL, SimpleDriverDataSource::getUrl, SimpleDriverDataSource::setUrl);
add(DataSourceProperty.DRIVER_CLASS_NAME, Class.class, (dataSource) -> dataSource.getDriver().getClass(),
SimpleDriverDataSource::setDriverClass);
add(DataSourceProperty.USERNAME, SimpleDriverDataSource::getUsername, SimpleDriverDataSource::setUsername);
add(DataSourceProperty.PASSWORD, SimpleDriverDataSource::getPassword, SimpleDriverDataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for Oracle.
*/
private static class OracleDataSourceProperties extends MappedDataSourceProperties {
OracleDataSourceProperties() {
add(DataSourceProperty.URL, OracleDataSource::getURL, OracleDataSource::setURL);
add(DataSourceProperty.USERNAME, OracleDataSource::getUser, OracleDataSource::setUser);
add(DataSourceProperty.PASSWORD, null, OracleDataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for H2.
*/
private static class H2DataSourceProperties extends MappedDataSourceProperties {
H2DataSourceProperties() {
add(DataSourceProperty.URL, JdbcDataSource::getUrl, JdbcDataSource::setUrl);
add(DataSourceProperty.USERNAME, JdbcDataSource::getUser, JdbcDataSource::setUser);
add(DataSourceProperty.PASSWORD, JdbcDataSource::getPassword, JdbcDataSource::setPassword);
}
}
/**
* {@link DataSourceProperties} for Postgres.
*/
private static class PostgresDataSourceProperties extends MappedDataSourceProperties {
PostgresDataSourceProperties() {
add(DataSourceProperty.URL, PGSimpleDataSource::getUrl, PGSimpleDataSource::setUrl);
add(DataSourceProperty.USERNAME, PGSimpleDataSource::getUser, PGSimpleDataSource::setUser);
add(DataSourceProperty.PASSWORD, PGSimpleDataSource::getPassword, PGSimpleDataSource::setPassword);
}
}
}