DataSource implementations, routing, and configuration.
Simple, non-pooled DataSource for testing.
import org.springframework.jdbc.datasource.DriverManagerDataSource;
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setUrl("jdbc:postgresql://localhost:5432/mydb");
ds.setUsername("user");
ds.setPassword("password");
// Constructor shorthand
DriverManagerDataSource ds = new DriverManagerDataSource(
"jdbc:postgresql://localhost:5432/mydb",
"user",
"password"
);Use case: Testing only. Does NOT pool connections.
Single shared Connection for testing.
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
SingleConnectionDataSource ds = new SingleConnectionDataSource(
"jdbc:h2:mem:testdb",
"sa",
"",
true // suppress close() calls
);Use case: Unit tests with in-memory databases.
Use dedicated connection pools:
HikariCP (recommended):
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
HikariDataSource ds = new HikariDataSource(config);Spring Boot:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: user
password: password
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000Route to different DataSources based on lookup key.
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String type) {
contextHolder.set(type);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
// Configuration
@Bean
public DataSource routingDataSource(
@Qualifier("primaryDataSource") DataSource primary,
@Qualifier("replicaDataSource") DataSource replica
) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primary);
targetDataSources.put("replica", replica);
RoutingDataSource routingDS = new RoutingDataSource();
routingDS.setTargetDataSources(targetDataSources);
routingDS.setDefaultTargetDataSource(primary);
routingDS.afterPropertiesSet();
return routingDS;
}
// Usage
DataSourceContextHolder.setDataSourceType("replica");
try {
// Queries go to replica
List<User> users = jdbc.query("SELECT * FROM users", new UserRowMapper());
} finally {
DataSourceContextHolder.clearDataSourceType();
}Read-write splitting:
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(readOnly)")
public Object routeDataSource(ProceedingJoinPoint pjp, ReadOnly readOnly) throws Throwable {
DataSourceContextHolder.setDataSourceType("replica");
try {
return pjp.proceed();
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ReadOnly {}
// Usage
@ReadOnly
public List<User> findAllUsers() {
return jdbc.query("SELECT * FROM users", new UserRowMapper());
}Delays Connection acquisition until first use.
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
@Bean
public DataSource dataSource() {
HikariDataSource target = new HikariDataSource(config);
return new LazyConnectionDataSourceProxy(target);
}Use case: Read-only transactions that may not actually query database.
Makes DataSource participate in Spring transactions.
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
@Bean
public DataSource dataSource() {
HikariDataSource target = new HikariDataSource(config);
return new TransactionAwareDataSourceProxy(target);
}Use case: When using DataSource directly (not via JdbcTemplate).
import org.springframework.jndi.JndiObjectFactoryBean;
@Bean
public JndiObjectFactoryBean dataSource() {
JndiObjectFactoryBean jndi = new JndiObjectFactoryBean();
jndi.setJndiName("java:comp/env/jdbc/MyDB");
jndi.setResourceRef(true);
jndi.setProxyInterface(DataSource.class);
return jndi;
}
// Spring Boot
spring.datasource.jndi-name=java:comp/env/jdbc/MyDBpublic class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) {
currentTenant.set(tenant);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
@Configuration
public class MultiTenantConfig {
@Bean
public DataSource dataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("tenant1", createDataSource("jdbc:postgresql://localhost/tenant1_db"));
dataSourceMap.put("tenant2", createDataSource("jdbc:postgresql://localhost/tenant2_db"));
TenantRoutingDataSource routing = new TenantRoutingDataSource();
routing.setTargetDataSources(dataSourceMap);
routing.setDefaultTargetDataSource(dataSourceMap.get("tenant1"));
routing.afterPropertiesSet();
return routing;
}
private DataSource createDataSource(String url) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername("user");
config.setPassword("password");
return new HikariDataSource(config);
}
}
// Interceptor to set tenant
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-Tenant-ID");
TenantContext.setCurrentTenant(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
}
}HikariCP configuration:
# Core settings
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
# Performance
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.connection-test-query=SELECT 1
# Leak detection
spring.datasource.hikari.leak-detection-threshold=60000Pool size formula:
connections = ((core_count * 2) + effective_spindle_count)For typical web app on 4-core machine: 10-20 connections.
import org.springframework.jdbc.datasource.DataSourceUtils;
// Get connection (participates in transaction)
Connection conn = DataSourceUtils.getConnection(dataSource);
try {
// Use connection
} finally {
DataSourceUtils.releaseConnection(conn, dataSource);
}
// Check if connection is transactional
boolean isTransactional = DataSourceUtils.isConnectionTransactional(conn, dataSource);spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: user
password: password
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
pool-name: MyHikariPoolMultiple DataSources:
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource primaryDataSource() {
return primaryDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSourceProperties secondaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource secondaryDataSource() {
return secondaryDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
}spring:
datasource:
primary:
url: jdbc:postgresql://localhost:5432/primary_db
username: user
password: password
secondary:
url: jdbc:postgresql://localhost:5432/secondary_db
username: user
password: password