在复杂的企业应用中,多数据源管理是常见需求。本文将介绍如何基于Spring Boot实现优雅的动态数据源切换方案,通过自定义注解和AOP实现透明化切换。
核心设计思路
通过三层结构实现数据源动态路由:
- 注解层:声明式标记数据源
- 路由层:基于ThreadLocal的上下文管理
- 切面层:在方法执行前后自动切换数据源
核心实现代码
1. 数据源注解定义
less
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String name() default ""; // 数据源名称
}
2. 动态数据源上下文
typescript
public class DynamicDataSourceContext extends AbstractRoutingDataSource {
private static String DEFAULT_DATASOURCE_NAME;
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public DynamicDataSourceContext(String defaultDataSourceName,
Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(targetDataSources.get(defaultDataSourceName));
super.setTargetDataSources(targetDataSources);
DEFAULT_DATASOURCE_NAME = defaultDataSourceName;
super.afterPropertiesSet(); // 关键初始化
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceKey(); // 获取当前数据源标识
}
// 数据源操作工具方法
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
public static String getDefaultDataSourceName() {
return DEFAULT_DATASOURCE_NAME;
}
}
3. AOP切面实现
less
@Aspect
@Component
@Order(-1) // 确保在事务切面前执行
public class DataSourceAspect {
@Around("@within(DataSource) || @annotation(DataSource)")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 优先级:方法注解 > 类注解 > 默认数据源
DataSource methodAnno = method.getAnnotation(DataSource.class);
DataSource classAnno = method.getDeclaringClass().getAnnotation(DataSource.class);
String dataSource = DynamicDataSourceContext.getDefaultDataSourceName();
if (methodAnno != null && StringUtils.hasText(methodAnno.name())) {
dataSource = methodAnno.name();
} else if (classAnno != null && StringUtils.hasText(classAnno.name())) {
dataSource = classAnno.name();
}
try {
DynamicDataSourceContext.setDataSourceKey(dataSource);
return point.proceed(); // 执行目标方法
} finally {
DynamicDataSourceContext.clearDataSourceKey(); // 清理数据源标识
}
}
}
自动配置
在src/main/resources/META-INF/spring
目录下创建文件:
org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.test.datasourcestater.aspect.DataSourceAspect
使用示例
1、新增动态切换数据源定义
less
@Configuration
@Component
public class DynamicDataSourceConfig {
//默认数据源定义
@Resource(name = "defaultDataSource")
private DataSource defaultDataSource;
//其他数据源定义
@Resource(name = "testDataSource")
private DataSource testDataSource;
@Bean("dynamicDataSource")
@Primary
public DynamicDataSourceContext dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
//添加默认数据源和其他数据源
targetDataSources.put("default",defaultDataSource);
targetDataSources.put("testDataSource",testDataSource);
return new DynamicDataSourceContext("default", targetDataSources);
}
@Bean("dataSourceTransactionManager")
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
其他数据源定义:
typescript
@Configuration
@ConfigurationProperties(prefix = "xxx")
@Data
public class TestDataSourceConfig {
private String driverClassName;
private String url;
private String username;
private String password;
private String validationQuery;
private int initialSize;
private int maxActive;
private int maxIdle;
private int minIdle;
@Bean("testDataSource")
public BasicDataSourceDecrypt basicDataSourceDecrypt(){
BasicDataSourceDecrypt basicDataSourceDecrypt = new BasicDataSourceDecrypt();
basicDataSourceDecrypt.setUsername(username);
basicDataSourceDecrypt.setPassword(password);
basicDataSourceDecrypt.setDriverClassName(driverClassName);
basicDataSourceDecrypt.setPoolName("xxx");
// 数据库连接地址
basicDataSourceDecrypt.setJdbcUrl(url);
// 最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
basicDataSourceDecrypt.setMinimumIdle(minIdle);
// 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值
basicDataSourceDecrypt.setMaximumPoolSize(maxActive);
// 空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
basicDataSourceDecrypt.setIdleTimeout(30000);
// 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
basicDataSourceDecrypt.setMaxLifetime(360000L);
// 连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒
basicDataSourceDecrypt.setConnectionTimeout(500);
// 用于测试连接是否可用的查询语句
basicDataSourceDecrypt.setConnectionTestQuery(validationQuery);
return basicDataSourceDecrypt;
}
}
2、修改原始的DataSourceConfig为默认数据源
- 将datasource bean 定义名称改成@Bean("defaultDataSource")
- 将@Qualifier("dataSource")改成@Qualifier("dynamicDataSource") 参考代码,如下:
typescript
@Configuration
@ConfigurationProperties(prefix = "xxx")
@Data
public class DataSourceConfig {
private String driverClassName;
private String url;
private String username;
private String password;
private String validationQuery;
private int initialSize;
private int maxActive;
private int maxIdle;
private int minIdle;
@Bean("defaultDataSource")
public BasicDataSourceDecrypt basicDataSourceDecrypt(){
BasicDataSourceDecrypt basicDataSourceDecrypt = new BasicDataSourceDecrypt();
basicDataSourceDecrypt.setDriverClassName(driverClassName);
basicDataSourceDecrypt.setUsername(username);
basicDataSourceDecrypt.setPassword(password);
basicDataSourceDecrypt.setPoolName("xx");
// 数据库连接地址
basicDataSourceDecrypt.setJdbcUrl(url);
// 最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
basicDataSourceDecrypt.setMinimumIdle(minIdle);
// 最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值
basicDataSourceDecrypt.setMaximumPoolSize(maxActive);
// 空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
basicDataSourceDecrypt.setIdleTimeout(60000);
// 连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
basicDataSourceDecrypt.setMaxLifetime(600000);
// 连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒
basicDataSourceDecrypt.setConnectionTimeout(30000);
// 用于测试连接是否可用的查询语句
basicDataSourceDecrypt.setConnectionTestQuery(validationQuery);
return basicDataSourceDecrypt;
}
@Bean("messageResource")
public ResourceBundleMessageSource resourceBundleMessageSource(){
ResourceBundleMessageSource messageResource = new ResourceBundleMessageSource();
messageResource.setDefaultEncoding("UTF-8");
messageResource.setCacheSeconds(0);
return messageResource;
}
@Bean("dataSourceTransactionManager")
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") BasicDataSourceDecrypt dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") BasicDataSourceDecrypt dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
3、修改mybatisPlusConfig配置
dataSource注入,改成注入@Resource(name="dynamicDataSource")
less
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Resource(name="dynamicDataSource")
private DataSource dataSource;
@Autowired
private MybatisPlusProperties properties;
@Autowired
private ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired(required = false)
private DatabaseIdProvider databaseIdProvider;
@Bean
public DatabaseIdProvider getDatabaseIdProvider() {
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}
/**
* mybatis-plus分页插件
*/
@Bean("paginationInterceptor")
public PaginationInnerInterceptor paginationInterceptor(@Value("${database.type:mysql}") String databaseType) {
PaginationInnerInterceptor page = new PaginationInnerInterceptor();
page.setDbType(DbType.getDbType(databaseType));
return page;
}
@Bean("optimisticLockerInterceptor")
public OptimisticLockerInnerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
@Bean("mybatisPlusInterceptor")
public MybatisPlusInterceptor mybatisPlusInterceptor(@Qualifier("paginationInterceptor") PaginationInnerInterceptor paginationInterceptor,
@Qualifier("optimisticLockerInterceptor") OptimisticLockerInnerInterceptor optimisticLockerInterceptor){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(paginationInterceptor);
mybatisPlusInterceptor.addInnerInterceptor(optimisticLockerInterceptor);
return mybatisPlusInterceptor;
}
/**
* 这里全部使用mybatis-autoconfigure 已经自动加载的资源。不手动指定
* 配置文件和mybatis-boot的配置文件同步
* @return
*/
@Bean("sqlSessionFactory")
public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean(@Qualifier("globalConfiguration") GlobalConfig globalConfig,
@Qualifier("mybatisPlusInterceptor") MybatisPlusInterceptor mybatisPlusInterceptor) {
MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean();
mybatisPlus.setDataSource(dataSource);
mybatisPlus.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
mybatisPlus.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
mybatisPlus.setConfiguration(properties.getConfiguration());
mybatisPlus.setPlugins(mybatisPlusInterceptor);
mybatisPlus.setGlobalConfig(globalConfig);
MybatisConfiguration mc = new MybatisConfiguration();
mc.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
mybatisPlus.setConfiguration(mc);
if (this.databaseIdProvider != null) {
mybatisPlus.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
mybatisPlus.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
mybatisPlus.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
mybatisPlus.setMapperLocations(this.properties.resolveMapperLocations());
}
return mybatisPlus;
}
@Bean("globalConfiguration")
public GlobalConfig globalConfig(@Qualifier("myMetaObjectHandler") ModelMetaObjectHandler myMetaObjectHandler,
@Qualifier("customIdGenerator") CustomerIdGenerator customIdGenerator){
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(myMetaObjectHandler);
globalConfig.setIdentifierGenerator(customIdGenerator);
return globalConfig;
}
@Bean("myMetaObjectHandler")
public ModelMetaObjectHandler modelMetaObjectHandler(){
return new ModelMetaObjectHandler();
}
@Bean("customIdGenerator")
public CustomerIdGenerator customerIdGenerator(){
return new CustomerIdGenerator();
}
}
4、使用说明
在Controller层、Service层和Dao层的方法或者类加上@DataSource(name="数据源名字")注解,完成数据源的自动切换,其中数据源名字来自DynamicDataSourceConfig中dynamicDataSource方法中定义的数据源
4.1、类级别注解
在类上添加多数据源注解,类中的所有方法都是使用注解中设置的数据源
4.1.1、Controller层用法
less
@RestController
@DataSource(name = "default")
@RequestMapping("/Order")
public class OrderController{
// 所有方法默认使用default数据源
@GetMapping("/list")
public List<Order> queryAll() {
// ...
}
// 默认使用default数据源
@GetMapping("/{id}")
public Order selectById(@PathVariable Long id) {
// ...
}
}
4.1.2、Service层用法
Service层使用多数据源注解时,需使用在@Service修饰的类上多数据源注解才能生效
less
@Service
@DataSource(name = "default")
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
// 所有方法默认使用default数据源
public List<Order> findAll() {
// ...
}
// 默认使用default数据源
public Order findById() {
// ...
}
}
4.1.3、Dao层用法
Dao层使用多数据源注解时,需使用在@Component、@Repository或者@Mapper修饰的Dao层接口上多数据源注解才能生效
less
@Component
@DataSource(name = "default")
public interface OrderMapper extends BaseMapper<Order> {
// 使用default数据源
Order getById(@Param("id")Long id);
// 使用default数据源
List<Order> getList();
}
4.2、方法级别注解
在方法上添加多数据源注解,具体方法使用注解中设置的数据源
less
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
// 使用testDataSource数据源
@DataSource(name = "testDataSource")
public User getUserById(Long id) {
// ...
}
// 使用default数据源
@DataSource(name = "default")
public User deleteUserById(Long id) {
// ...
}
// 使用默认数据源(default数据源)
public void updateUser(User user) {
// ...
}
}
Controller层与Dao层用法与类级别注解部分的使用介绍类似,此处不再赘述。
4.3、混合使用
多数据源注解的优先级别:方法级别注解>类级别注解>无注解(默认数据源)
当类和方法中都使用多数据源注解,会按照优先级别选择具体数据源
less
@Service
@DataSource(name = "testDataSource")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService{
// 继承类注解,使用testDataSource数据源
public Product getProduct(Long id) {
// ...
}
// 方法级别注解的优先级最高,优先使用方法注解,使用default数据源
@DataSource(name = "default")
public void updateProduct(Product product) {
// ...
}
}
Controller层与Dao层用法与类级别注解部分的使用介绍类似,此处不再赘述。
5、注意事项
⚠️ 重要限制 :由于数据源切换基于AOP,与@Transactional
注解联用时需注意:
- 事务注解应加在数据源注解外层:
less
// ✅ 正确:事务在外层
@Transactional
@DataSource(name = "slave1")
public void transactionalMethod() { /* ... */ }
// ❌ 危险:数据源切换可能不生效
@DataSource(name = "slave1")
@Transactional
public void riskyMethod() { /* ... */ }
总结
本文实现的多数据源方案具有以下优势:
- 非侵入式:通过注解透明切换,不影响业务逻辑
- 灵活配置:支持方法级和类级数据源指定
- 线程安全:基于ThreadLocal的上下文管理
- 易于扩展:可快速添加新数据源
通过这种设计,开发者可以轻松管理多个数据源,特别适用于多租户系统、读写分离、分库分表等复杂场景。完整代码已托管至Gitee,gitee地址:gitee.com/mutigmss/mu...
欢迎在评论区交流使用体验和优化建议!