Why Is This Needed?

If your application connects to one database only, this is simple. But production systems often need multiple databases. For example, in replicated environments you may read from replica by default, and switch to master when replication issues occur.

This kind of dynamic DataSource routing can be implemented with Spring JDBC AbstractRoutingDataSource. Let’s review a concrete example.


Gradle Project Setup

We’ll use a gradle project here, but maven is fine too. Non-essential parts are omitted.

plugins {
    id 'org.springframework.boot' version '2.3.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter:2.4.0'
    implementation 'org.springframework.boot:spring-boot-starter-web:2.4.0'
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc:2.4.0'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1'
    compile 'mysql:mysql-connector-java:5.1.42'
}


AbstractRoutingDataSource

Before writing DataSource config, define a class extending AbstractRoutingDataSource. Override determineCurrentLookupKey to decide which database key to use.

In this example, assume two databases (Master/Replica). For simplicity, use URI pattern: if request URI starts with /master, use master, otherwise replica. Return value is the key used in DataSource target mapping.

public class MyRoutingDataSource extends AbstractRoutingDataSource {
	@Override
	protected Object determineCurrentLookupKey() {
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        if (request.getRequestURI().startsWith("/master")) {
            return "master";
        } else {
            return "replica";
        }	
	}
}

This example uses RequestURI, but Session/ThreadLocal-based approaches are also possible. You can also combine this with Spring Cloud Config for dynamic runtime configuration.


Database Config

Now write database config class. Depending on your architecture, registered DataSources differ. Because focus here is AbstractRoutingDataSource, we use simple methods returning different DataSources.

@Configuration
@MapperScan(value = "com.example.madplay.mapper")
public class DatabaseConfig {

	@Bean(name = "dbDataSource")
	public DataSource RouterDataSource() {
		Map<Object, Object> targetSources = new HashMap<>();
        // Must match key values returned in `AbstractRoutingDataSource`.
        targetSources.put("master", getMasterDataSource());
		targetSources.put("replica", getReplicaDataSource());

        MyRoutingDataSource dataSource = new MyRoutingDataSource();
		dataSource.setTargetDataSources(targetSources);
		return dataSource;
	}

    private DataSource getMasterDataSource() {
		com.zaxxer.hikari.HikariDataSource dataSource = new com.zaxxer.hikari.HikariDataSource();
		// ... driver, username, password omitted
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/masterDB?autoReconnect=true&useSSL=false");
		return dataSource;
	}

	private DataSource getReplicaDataSource() {
		com.zaxxer.hikari.HikariDataSource dataSource = new com.zaxxer.hikari.HikariDataSource();
		// ... driver, username, password omitted
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/replicaDB?autoReconnect=true&useSSL=false");
		return dataSource;
	}
}


Mapper Setup

Let’s define MyBatis mapper. You can write SQL in separate XML files or directly with annotations.

In this example, table/column names are simplified for testing. Both masterDB and replicaDB have same table/column schema. masterDB stores db_name = master, and replicaDB stores db_name = replica.

Option 1. SQL in separate XML

<mapper namespace="com.example.madplay.mapper.MyMapper">
    <select id="selectDbName" resultType="String">
        SELECT
            db_name
        FROM
            my_table
    </select>
</mapper>

If you use separate XML files, configure location in application.properties. Adjust path based on your project layout.

mybatis.mapper-locations=classpath:mapper/*.xml

Option 2. SQL with annotations

Write SQL directly in mapper interface:

@Mapper
public interface MyMapper {
	@Select("SELECT db_name FROM my_table")
	String selectDbName();
}


Test It

Finally, write a controller to test routing behavior. It returns selected DB name based on request URL.

@RestController
public class MyController {
	private final MyMapper myMapper;

	public MyController(MyMapper myMapper) {
		this.myMapper = myMapper;
	}

	@GetMapping("/master")
	public String master() {
		return String.format("DB: %s", myMapper.selectDbName());
	}

	@GetMapping("/replica")
	public String replica() {
		return String.format("DB: %s", myMapper.selectDbName());
	}
}

Now call endpoints and verify routing behavior. As shown below, database connection changes based on requested URL.

$ curl "http://localhost:8080/master"
DB: master

$ curl "http://localhost:8080/replica"
DB: replica