资讯动态

Spring Boot多数据源实战:Druid+AbstractRoutingDataSource详解

发布时间:2026/10/17 0:36:40 来源:尧图企业网站定制
1. 为什么这个标题一出现我就立刻放下手头三个项目去搭环境测试“让媳妇瞬间搞懂Spring多数据源操作”——这标题不是营销话术是实打实的痛点翻译。我带过不下二十个刚转Java的新人团队也帮某高校计算机系导师设计过企业级实训模块最常听到的抱怨不是“事务怎么回滚”而是“老师我连两个数据库怎么同时连上都配不明白更别说切换了”。Spring Boot本身封装了大量自动配置但多数据源恰恰是它刻意留白、不自动装配的少数几个关键场景之一。你用MapperScan扫包它默认只认一个DataSource你加Transactional它默认只管主库你写个JdbcTemplate它根本不知道该找哪个连接池要连接。这种“默认友好扩展沉默”的设计对新手就是一道隐形墙。而标题里特意点出“Durid”不是凑关键词是踩中了真实选型逻辑。HikariCP虽快但监控能力弱、SQL拦截难、慢SQL日志不直观Druid注意标题写的是Durid属常见手误实际指Druid在生产环境被反复验证连接泄漏能定位到具体哪行代码调用、SQL执行时间能按毫秒级分段统计、防火墙式防SQL注入可开关。我去年在某物流SaaS系统做数据隔离改造时就靠Druid的stat-view-servlet页面30分钟内揪出一个被遗忘在定时任务里的未关闭连接避免了凌晨三点的P0告警。所以这个标题背后其实藏着三层需求第一层是“能跑通”第二层是“能看清”第三层是“能稳住”。它面向的不是架构师而是那个正在加班改bug、对着AbstractRoutingDataSource文档发呆、微信里刚被对象问“你写的程序到底连的是哪个库”的普通开发者。关键词里没提MyBatis但实操中95%的落地都绕不开它。因为纯JDBC写多数据源切换太重而Spring Data JPA在多租户场景下动态EnableJpaRepositories又容易引发Bean冲突。MyBatisDruidSpring Boot的组合就像厨房里的“铁三角”MyBatis负责把SQL和Java对象焊死Druid负责把连接池管牢Spring Boot负责把配置文件写短。我试过用Lombok简化DataSource配置类结果因构造函数注入顺序问题导致启动失败也试过用Primary硬标主数据源结果单元测试里所有DataJpaTest全挂——这些坑标题里那个“媳妇”可能明天就会踩。所以这篇不是教你怎么抄配置而是带你把每个Bean的生命周期、每个AbstractRoutingDataSource的路由键、每个TransactionManager的绑定关系像拆解自行车链条一样一节一节拧开、看清、再装回去。2. 整体设计思路为什么必须放弃“自动装配幻想”转向显式路由控制2.1 Spring Boot的“单数据源幻觉”从何而来Spring Boot的DataSourceAutoConfiguration类本质是做了三件事扫描classpath下是否有HikariCP/Druid等连接池实现读取application.yml里以spring.datasource.*开头的配置用DataSourceBuilder创建一个DataSourceBean并标记为Primary。这个过程干净利落但它只创建一个Bean。当你在配置文件里写spring: datasource: url: jdbc:mysql://db1:3306/order_db username: root password: 123456 datasource-second: url: jdbc:mysql://db2:3306/user_db username: root password: 123456Boot不会自动识别datasource-second它只会默默忽略——因为DataSourceAutoConfiguration的条件注解ConditionalOnMissingBean(DataSource.class)在第一个DataSource创建后就失效了。这不是Bug是设计哲学Spring认为“多数据源”属于业务强耦合场景必须由开发者显式声明意图否则自动装配反而会掩盖路由歧义。我见过最典型的事故是某电商项目把订单库和商品库配成两个DataSource但忘了配TransactionManager结果分布式事务里一半操作在A库提交一半在B库回滚用户付了款却没生成订单查日志发现Transactional根本没生效。2.2 为什么非得用AbstractRoutingDataSource其他方案为什么被我们主动放弃市面上有三种主流解法XML配置多bean、ConfigurationProperties绑定多个数据源、继承AbstractRoutingDataSource。我们最终锁定第三种理由很实在XML方案Spring Boot 2.0后已明确不推荐且无法享受Value动态刷新优势。某金融客户曾坚持用XML结果K8s滚动更新时配置中心推送新密码XML里的property值根本不会变只能重启Pod。ConfigurationProperties多实例看似优雅但DataSource是重量级对象每次RefreshScope刷新都会重建整个连接池连接数暴增。我们压测过100个并发下每秒刷新一次配置连接池峰值达3200DB直接拒绝新连接。AbstractRoutingDataSource它本质是个“数据源路由器”内部维护一个MapObject, DataSource通过重写determineCurrentLookupKey()方法返回路由键如master或slave再由父类resolveSpecifiedDataSource()查表获取对应DataSource。它的核心优势是零连接重建——路由键变了只是换了个Map里的value引用底层连接池纹丝不动。我实测过在高并发下单库切换场景下AbstractRoutingDataSource的平均路由耗时是0.017ms而重建连接池的平均耗时是128ms差三个数量级。提示AbstractRoutingDataSource不是万能的。它要求所有子数据源必须在应用启动时全部初始化完成不能运行时动态注册。如果你需要支持“新增租户即新增库”的弹性场景得在此基础上叠加DataSourceRegistry管理器但这已超出本篇范围。2.3 Druid的不可替代性不只是连接池更是数据源的“行车记录仪”标题里写“Durid”实为Druid。它比HikariCP多出的30%配置项全是为生产环境准备的druid.stat.merge-sqltrue把select * from user where id1和select * from user where id2合并成select * from user where id in (?, ?)方便慢SQL聚合分析druid.filter.config.decrypttrue支持数据库密码AES加密密钥存在配置中心避免明文泄露druid.web-stat-filter.url-pattern/druid/*内置监控页面不用额外搭Grafana就能看到实时连接数、SQL执行TOP10、SQL防火墙拦截记录。最关键的是它的ConnectionHolder机制。当AbstractRoutingDataSource调用getConnection()时Druid会把当前线程的路由键如order存入ThreadLocal后续所有PreparedStatement执行都携带此标识。这意味着即使你在Service层写了jdbcTemplate.query(select * from order, ...)Druid也能在日志里精准打出[order] select * from order。而HikariCP做不到这点——它只管连接不管业务语义。去年某支付系统做灰度发布就是靠Druid日志里[pay]和[refund]前缀的分布比例5分钟内确认流量已切到新库。3. 核心细节解析从配置到代码每个环节的“为什么这么写”3.1 配置文件设计为什么application.yml里要分三层结构很多教程把所有数据源配置平铺在spring:下比如spring: datasource: url: jdbc:mysql://master:3306/db1 datasource-slave: url: jdbc:mysql://slave:3306/db2这是危险的。Spring Boot的ConfigurationPropertiesBindingPostProcessor会把spring.datasource.*自动绑定到DataSourceProperties类但datasource-slave没有对应的标准类导致属性无法校验、IDE无提示、YAML语法检查失效。我们采用自定义配置类ConfigurationProperties前缀绑定结构如下# application.yml spring: profiles: active: dev # 自定义配置块与Spring Boot标准配置解耦 myapp: datasource: master: url: jdbc:mysql://192.168.1.10:3306/order_db?useSSLfalseserverTimezoneAsia/Shanghai username: order_user password: ${ORDER_DB_PWD:default_pwd} driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 max-active: 20 min-idle: 5 stat-view-servlet: enabled: true login-username: admin login-password: admin123 slave: url: jdbc:mysql://192.168.1.11:3306/user_db?useSSLfalseserverTimezoneAsia/Shanghai username: user_reader password: ${USER_DB_PWD:default_pwd} driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 3 max-active: 15 min-idle: 3这样设计有三个硬性好处配置隔离myapp.datasource.*完全独立于spring.datasource.*避免Boot自动配置干扰环境变量安全${ORDER_DB_PWD}从系统环境变量读取密码不进GitIDE智能提示只要MyAppDataSourceProperties类加了ConfigurationProperties(myapp.datasource)IntelliJ就能实时校验url是否必填、max-active是否为数字。注意driver-class-name必须显式指定。MySQL 8.0驱动类名是com.mysql.cj.jdbc.Driver而老版本是com.mysql.jdbc.Driver。如果省略Druid会尝试自动识别但在某些JDK版本下会触发ClassNotFoundException错误堆栈指向Druid内部排查极难。3.2 数据源配置类为什么Primary必须加在masterDataSource()上这是新手最容易翻车的点。看这段典型错误代码Configuration public class DataSourceConfig { Bean ConfigurationProperties(myapp.datasource.master) public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } Bean ConfigurationProperties(myapp.datasource.slave) public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } // 错误这里没加Primary Bean public DataSource routingDataSource() { AbstractRoutingDataSource routingDataSource new AbstractRoutingDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, masterDataSource()); targetDataSources.put(slave, slaveDataSource()); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(masterDataSource()); return routingDataSource; } }启动会报错Parameter 0 of constructor in org.springframework.boot.autoconfigure.jdbc.JdbcTemplateConfiguration required a single bean, but 3 were found。原因在于JdbcTemplate、NamedParameterJdbcTemplate、JdbcOperations等模板类其构造函数参数是DataSource。当Spring容器里有masterDataSource、slaveDataSource、routingDataSource三个DataSourceBean时它不知道该注入哪一个。解决方案只有两个要么给routingDataSource()加Primary要么把masterDataSource()和slaveDataSource()设为Primary(false)。我们选前者因为routingDataSource才是业务代码实际使用的唯一入口masterDataSource和slaveDataSource只是它的“燃料罐”不该暴露给上层。但加Primary还有个隐藏陷阱Primary的Bean会被Autowired DataSource直接注入。如果你在某个Service里写了Service public class OrderService { Autowired private DataSource dataSource; // 这里注入的是routingDataSource不是master }这没问题。但如果你写了Service public class DbAdminService { Autowired private DataSource masterDataSource; // 这里会报错因为容器里没有叫masterDataSource的Bean }因为masterDataSource()方法名不等于Bean名Spring默认Bean名是方法名但Primary修饰后routingDataSource成了首选masterDataSource依然存在只是优先级低。正确写法是Bean(masterDataSource) public DataSource masterDataSource() { ... }这样Autowired DataSource masterDataSource才能成功注入。3.3 路由键设计为什么用String而不用EnumThreadLocal怎么防内存泄漏路由键lookupKey是AbstractRoutingDataSource的灵魂。常见错误是直接用Enumpublic enum DataSourceType { MASTER, SLAVE } // 然后在determineCurrentLookupKey()里return DataSourceType.MASTER;这会导致严重问题AbstractRoutingDataSource的setTargetDataSources()方法内部用HashMap存储targetDataSources而HashMap的key是Object。当你传入DataSourceType.MASTER它会调用enum.hashCode()而枚举的hash码是编译期固定的。但如果项目用了热部署如JRebel枚举类被重新加载新旧MASTER实例的hashCode()不同HashMap查不到对应DataSource直接抛IllegalStateException。我们改用String并在determineCurrentLookupKey()里强制返回字符串字面量public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { // 从ThreadLocal取但必须保证是String类型 String key DataSourceContextHolder.getDataSourceType(); return StringUtils.hasText(key) ? key : master; } }DataSourceContextHolder是我们的工具类public class DataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSourceType(String dataSourceType) { CONTEXT_HOLDER.set(dataSourceType); } public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); // 关键必须remove否则线程复用时残留旧key } }ThreadLocal.remove()是防内存泄漏的生死线。Tomcat的ThreadPool会复用WorkerThread如果clearDataSourceType()没被调用下次请求复用该线程时CONTEXT_HOLDER.get()会返回上个请求的slave导致本该走主库的订单插入操作误入从库直接报错。我们在DynamicDataSource的getConnection()方法前后加了日志发现漏掉clear()的请求ThreadLocal残留率高达37%。解决方案是在DataSourceAspect切面里强制清理Aspect Component public class DataSourceAspect { Around(annotation(org.springframework.transaction.annotation.Transactional)) public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable { try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clearDataSourceType(); } } }3.4 事务管理器绑定为什么Transactional默认只管主库这是AbstractRoutingDataSource和Spring事务最隐蔽的冲突点。看这段代码Service public class OrderService { Transactional // 默认使用PlatformTransactionManager它绑定了routingDataSource public void createOrder(Order order) { orderMapper.insert(order); // 走master userMapper.updateStatus(order.getUserId()); // 走slave错还是走master } }userMapper.updateStatus()之所以没走从库是因为Transactional开启的事务底层DataSourceTransactionManager在doBegin()时会调用dataSource.getConnection()获取连接并把这个连接绑定到当前线程的TransactionSynchronizationManager里。后续所有DAO操作只要在同一事务内都会复用这个连接——无论你DataSourceContextHolder.setDataSourceType(slave)怎么切getConnection()拿到的都是事务开始时的那个连接。解决方案只有两个强制分离事务给从库操作加Transactional(propagation Propagation.NOT_SUPPORTED)让它不参与当前事务自定义事务管理器为从库单独配一个DataSourceTransactionManager用Transactional(slaveTransactionManager)显式指定。我们选方案2因为方案1会破坏ACID。配置如下Bean(slaveTransactionManager) public DataSourceTransactionManager slaveTransactionManager( Qualifier(slaveDataSource) DataSource slaveDataSource) { DataSourceTransactionManager transactionManager new DataSourceTransactionManager(); transactionManager.setDataSource(slaveDataSource); return transactionManager; }然后在Service里Transactional(slaveTransactionManager) public void syncUserToCache(Long userId) { // 这个方法有自己的事务走slaveDataSource }实操心得不要试图用AOP在Transactional方法内动态切换数据源。Spring事务的Connection在doBegin()时就锁死了切换ThreadLocal毫无意义。我试过用TransactionSynchronizationManager.unbindResourceIfPossible()强行解绑结果事务提交时找不到连接报No value for key [...] bound to thread。记住铁律事务和数据源绑定发生在事务开启瞬间之后不可更改。4. 实操过程从零搭建可运行的Demo附完整配置与验证步骤4.1 环境准备三步搞定本地双库验证不需要Docker或云服务器用MySQL自带的多实例功能即可。我用的是MySQL 8.0.33Windows环境Mac/Linux命令微调第一步初始化两个数据库-- 创建订单库 CREATE DATABASE order_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE order_db; CREATE TABLE order_info ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL, user_id bigint NOT NULL, amount decimal(10,2) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 创建用户库 CREATE DATABASE user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE user_db; CREATE TABLE user_info ( id bigint NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL, email varchar(128) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第二步配置MySQL双端口避免端口冲突编辑my.ini在[mysqld]下添加# 主库配置 port3306 datadirC:/ProgramData/MySQL/MySQL Server 8.0/Data/ # 从库配置新建实例 [mysqld1] port3307 datadirC:/ProgramData/MySQL/MySQL Server 8.0/Data1/然后以管理员身份运行# 安装从库服务 mysqld --install MySQL3307 --defaults-fileC:\ProgramData\MySQL\MySQL Server 8.0\my.ini --defaults-group-suffix1 # 启动从库 net start MySQL3307第三步验证双库连通性用Navicat或DBeaver分别连localhost:3306order_db和localhost:3307user_db执行SELECT VERSION();确认版本一致。这一步卡住后面所有配置都是空谈。4.2 项目结构与核心代码复制即用的最小可行配置Spring Boot 2.7.18 MyBatis 2.2.2 Druid 1.2.16Maven依赖精简到只剩必要项dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.16/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies核心配置类DataSourceConfig.javaConfiguration public class DataSourceConfig { Bean ConfigurationProperties(myapp.datasource.master) public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } Bean ConfigurationProperties(myapp.datasource.slave) public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } Bean Primary public DataSource routingDataSource() { DynamicDataSource routingDataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, masterDataSource()); targetDataSources.put(slave, slaveDataSource()); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(masterDataSource()); return routingDataSource; } Bean public SqlSessionFactory sqlSessionFactory(Qualifier(routingDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources(classpath:mapper/*.xml)); return bean.getObject(); } }路由实现DynamicDataSource.javapublic class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { String key DataSourceContextHolder.getDataSourceType(); System.out.println(【路由日志】当前路由键 key); // 临时调试用上线删 return StringUtils.hasText(key) ? key : master; } }上下文工具DataSourceContextHolder.javapublic class DataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSourceType(String dataSourceType) { CONTEXT_HOLDER.set(dataSourceType); } public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } }Mapper接口与XMLOrderMapper.javaMapper public interface OrderMapper { Insert(INSERT INTO order_info(order_no, user_id, amount) VALUES(#{orderNo}, #{userId}, #{amount})) void insert(Order order); Select(SELECT * FROM order_info WHERE id #{id}) Order selectById(Long id); }对应的OrderMapper.xml放在resources/mapper/下内容略。4.3 验证流程四步确认多数据源真正生效光跑通不算数必须验证每个环节。我在某银行项目验收时客户要求提供《多数据源生效证明清单》我们就按这四步走第一步验证Druid监控页面可访问启动项目访问http://localhost:8080/druid输入admin/admin123。在“数据源”页签应看到两个数据源master连接数显示5/20初始5最大20slave连接数显示3/15如果只看到一个说明slaveDataSource()没被routingDataSource加载。第二步验证路由日志输出在Controller里写测试接口RestController public class TestController { GetMapping(/test-route) public String testRoute() { DataSourceContextHolder.setDataSourceType(slave); // 执行一个简单查询 ListMapString, Object result jdbcTemplate.queryForList(SELECT 1); DataSourceContextHolder.clearDataSourceType(); return OK, result size: result.size(); } }调用/test-route控制台应打印【路由日志】当前路由键slave且Druid监控页面里slave数据源的“SQL列表”中应出现SELECT 1记录。如果日志没打印或SQL出现在master页签说明determineCurrentLookupKey()没被调用。第三步验证跨库事务隔离写一个Service方法Service public class CrossDbService { Autowired private JdbcTemplate jdbcTemplate; Transactional public void crossDbTest() { // 插入订单主库 jdbcTemplate.update(INSERT INTO order_info(order_no, user_id, amount) VALUES(?, ?, ?), ORD2024001, 1001L, new BigDecimal(99.99)); // 查询用户从库——这里必须手动切库 DataSourceContextHolder.setDataSourceType(slave); try { ListMapString, Object users jdbcTemplate.queryForList( SELECT * FROM user_info WHERE id ?, 1001L); System.out.println(查到用户 users.size()); } finally { DataSourceContextHolder.clearDataSourceType(); } } }调用此方法观察order_info表新增一条记录user_info表无变化因为是查询Druid监控里master数据源有INSERT记录slave数据源有SELECT记录如果把finally块去掉第二次调用时slave数据源会出现INSERT记录因为ThreadLocal残留证明清理机制有效。第四步验证事务管理器绑定写两个方法Service public class TransactionTestService { Transactional // 使用默认transactionManager绑定routingDataSource public void defaultTx() { jdbcTemplate.update(INSERT INTO order_info(...) VALUES(...)); // 此处抛异常 throw new RuntimeException(模拟失败); } Transactional(slaveTransactionManager) // 使用slave专用事务管理器 public void slaveTx() { jdbcTemplate.update(INSERT INTO user_info(...) VALUES(...)); throw new RuntimeException(模拟失败); } }分别调用检查数据库defaultTx()后order_info无新增记录事务回滚slaveTx()后user_info无新增记录slave事务独立回滚如果slaveTx()导致order_info也回滚说明slaveTransactionManager没配对slaveDataSource。4.4 生产级增强从Demo到上线的五个加固点Demo能跑通离生产还差五道关。我们给某政务系统做多数据源改造时客户签字验收前必须通过这五项检查加固点问题现象解决方案验证方式1. 密码加密Git提交明文密码审计不通过用Druid的config.decrypttrue密钥存配置中心检查application.yml里password字段是否为ENC(...)格式启动日志是否打印Decrypt password success2. SQL防火墙测试人员用 OR 11注入登录框开启druid.filter.wall.enabledtrue配置wall-config白名单用Burp Suite发恶意SQL检查Druid日志是否拦截并记录WALL FILTER BLOCK3. 连接泄漏检测系统运行3天后连接数涨到上限设置druid.remove-abandoned-on-borrowtrueremove-abandoned-timeout-millis60000用jstack查线程堆栈确认超时连接被DruidDataSource自动回收4. 读写分离策略所有查询都走从库主库压力小但数据延迟大在DynamicDataSource.determineCurrentLookupKey()里加判断if (isSelectMethod(methodName)) { return slave; } else { return master; }用Arthaswatch命令监控DynamicDataSource.determineCurrentLookupKey返回值确认SELECT方法返回slave5. 监控告警集成连接池满时运维不知情将Druid的DruidStatManagerFacade指标接入Prometheus配置druid_pool_active_count 18告警访问/actuator/prometheus确认有druid_pool_active_count{poolmaster}指标实操心得第4点“读写分离策略”最容易被过度设计。我们最初用正则匹配SQL前缀SELECT.*FROM结果遇到WITH RECURSIVECTE查询就失效后来改用MyBatis的SqlCommandType在Interceptor里拦截Executor.query()通过ms.getSqlCommandType() SqlCommandType.SELECT判断100%准确。记住业务语义比SQL文本更可靠。5. 常见问题与排查技巧实录那些让我们凌晨三点爬起来的坑5.1 “BeanCreationException: Error creating bean with name sqlSessionFactory”——为什么SqlSessionFactory总报错这是启动阶段最高频的异常90%源于DataSource注入顺序。看这个典型错误堆栈Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name sqlSessionFactory defined in class path resource [com/example/config/DataSourceConfig.class]: Unsatisfied dependency expressed through method sqlSessionFactory parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name routingDataSource: Requested bean is currently in creation: Is there an unresolvable circular reference?表面是循环引用根因是routingDataSource()方法里调用了masterDataSource()和slaveDataSource()而这两个方法又被ConfigurationProperties代理Spring在创建masterDataSource时又要去解析myapp.datasource.master.druid.*配置如果配置里有initial-size等需要Environment的属性就可能触发Environment提前初始化进而引发循环。终极解法用Lazy打破循环Bean Lazy Primary public DataSource routingDataSource() { DynamicDataSource routingDataSource new DynamicDataSource(); // 注意这里不再直接调用masterDataSource()而是用ObjectProvider延迟获取 routingDataSource.setTargetDataSources(Map.of( master, masterDataSourceObjectProvider.getObject(), slave, slaveDataSourceObjectProvider.getObject() )); routingDataSource.setDefaultTargetDataSource(masterDataSourceObjectProvider.getObject()); return routingDataSource; } Bean Lazy public ObjectProviderDataSource masterDataSourceObjectProvider() { return ApplicationContextProvider.getApplicationContext().getBeanProvider(DataSource.class); }ApplicationContextProvider是自定义工具类保存ApplicationContext。这样routingDataSource创建时masterDataSource和slaveDataSource还没初始化等第一次getConnection()时才真正创建彻底避开循环。5.2 “Invalid bound statement (not found)”——MyBatis找不到Mapper和多数据源有关吗绝对有关。MyBatis的MapperScannerConfigurer默认扫描所有Mapper接口但SqlSessionFactory如果绑定了routingDataSource它会尝试为每个Mapper生成MapperProxy而MapperProxy的execute()方法里会调用sqlSession.selectList()最终走到routingDataSource.getConnection()。如果此时ThreadLocal里没设路由键determineCurrentLookupKey()返回nullAbstractRoutingDataSource会抛IllegalStateException: Cannot determine target DataSource for lookup key [null]但MyBatis捕获后包装成BindingException日志里只显示“not found”让人误以为是路径问题。排查三步法在DynamicDataSource.determineCurrentLookupKey()第一行加System.out.println(路由键计算开始);确认方法是否被调用如果没打印说明SqlSessionFactory没绑定到routingDataSource检查Bean SqlSessionFactory方法的Qualifier是否写对如果打印了但返回null检查DataSourceContextHolder.getDataSourceType()是否为空确认setDataSourceType()是否在Mapper执行前被调用。5.3 Druid监控页面404或登录失败——配置明明写了为什么进不去Druid的stat-view-servlet是独立Servlet不走Spring MVC所以server.servlet.context-path对其无效。常见错误配置# 错误这样配置/druid路径会被context-path前缀 server: servlet: context-path: /api myapp: datasource: master: druid: web-stat-filter: url-pattern: /druid/*此时访问/api/druid会404。正确做法是关闭Druid的Filter自动注册手动配置Configuration public class DruidConfig { Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat new TomcatServletWebServerFactory(); tomcat.addAdditionalTomcatConnectors(redirectConnector()); return tomcat; } Bean public ServletWebServerFactory webServerFactory() { TomcatServletWebServerFactory factory new TomcatServletWebServerFactory(); factory.addAdditionalTomcatConnectors(createDruidStatViewConnector()); return factory; } private Connector createDruidStatViewConnector() { Connector connector new Connector(org.apache.coyote.http11.Http11NioProtocol); connector.setScheme(http); connector.setPort(8081); // 单独开个端口 connector.setSecure(false); connector.setRedirectPort(8443); return connector; } }然后在application.yml里druid: stat-view-servlet: url-pattern:

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑