ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot+MyBatis多数据源硬核配置:配置类绑定绕开AOP

Spring Boot+MyBatis多数据源硬核配置:配置类绑定绕开AOP 简介面向SpringBootMyBatis开发场景这份PDF聚焦于多数据源的最简解决方案服务于需要连接多个数据库的Java工程。针对主从读写分离、分库支撑业务等实际需求从降低配置复杂度的角度出发给出不依赖JPA、也不借助AOP动态切换的思路而是基于Configuration与MapperScan完成数据源、SqlSessionFactory、事务管理器及SqlSessionTemplate的逐层装配并展示test1主库、test2从库在application.properties中的参数写法。内容还覆盖Mapper接口与XML文件的目录拆分、Primary主库指定等关键细节便于读者直接对照复现。该资源为1个PDF文件压缩包大小54KB结构紧凑、聚焦核心代码目前已有2356人学习适合正在配置双数据源的中级Java工程师参考。通过文档可快速掌握双数据源配置的完整链路避免因主库未指定或SqlSessionTemplate注入错误导致的常见报错提升开发与调试效率。1. springbootmybatis多数据源绕开AOP切换用配置类硬解先说结论网上搜springbootmybatis多数据源十篇有八篇在讲AbstractRoutingDataSource配合AOP动态切换数据源剩下一篇讲JPA。但我项目里遇到的是业务分库——用户库、订单库、日志库是不同业务域Mapper固定属于某一个库根本不需要运行时切换。折腾下来发现最稳的反而是看起来最笨的方案每个数据源写一套独立的DataSource配置类用MapperScan把不同的Mapper包绑到不同的SqlSessionTemplate上。这套方案代码重复但逻辑直白没有切面、没有ThreadLocal、没有DS注解任何人接手都能半小时看懂。这个资源就是springbootmybatis多数据源的最简落地写法适合做分库、读写分离背景不复杂、只想快速让两个库跑起来的项目也适合面试时把多数据实现讲清楚。2. 多数据源三种方案AOP切换、JPA、配置级绑定怎么选2.1 AbstractRoutingDataSource动态切换的原理和代价先看最流行的动态切换方案是怎么工作的。Spring提供一个AbstractRoutingDataSource它本身是一个DataSource代理内部维护一个MapObject, DataSource目标数据源集合再通过determineCurrentLookupKey()返回一个key运行时从map里取真正的DataSource。AOP方案的套路一般是定义一个DataSource(name test1)注解切面在方法执行前把name塞进ThreadLocal执行后再清理determineCurrentLookupKey()从ThreadLocal拿key。这套方案的优点是写业务代码时不需要关心底层走哪个库同一个Mapper接口可以被多个数据源共用适合主从读写分离场景——读多写少切到备库读、主库写。但代价很现实第一事务边界和切换时机是玄学。Spring事务管理器在开启事务时就会从数据源拿连接Transactional包住的方法如果内部切换数据源连接已经被绑定到事务上切了也白切SQL还是走旧连接。网上很多人贴DataSource注解切换成功是因为根本没有事务包裹或者数据源已经被代理了一层才勉强能用一旦加上Transactional就翻车。第二切面本身需要维护ThreadLocal的清理漏清理或者异常路径没走干净下一次请求会串DataSource。生产环境出现这种情况排查成本极高因为错误SQL可能在一两个小时后才出现。第三多数据源事务回滚没法做。动态切换的数据源本质上还是单事务管理器跨库操作无法同时回滚这和配置级方案遇到的问题一样但动态切换的代码复杂度反而把这个问题掩盖了。2.2 JPA多数据源方案的适用边界网上另有一批资料是拿Spring Data JPA做多数据源思路和MyBatis类似定义两个EntityScan分包的EntityManagerFactory再用EnableJpaRepositories分别指到不同包。这个方案的优点是把Repository抽象层做得很干净但问题是JPA本身对复杂SQL、动态SQL的支持比MyBatis弱一截很多做分库的项目是从老MyBatis系统迁移过来的历史mapper xml一大堆迁JPA等于重写数据访问层成本根本扛不住。再者JPA多数据源的配置并不比MyBatis简单EntityManagerFactory、TransactionManager、PersistenceUnit三件套照样要写两遍省掉的代码量非常有限。如果项目里已经有mybatis的依赖和xml没必要为了多数据源把整个ORM层换掉老老实实继续用mybatis更稳。2.3 配置级绑定三套Bean各建一次Mapper按包归属这份资源的方案本质是把“数据源-会话工厂-事务管理器-SqlSessionTemplate”四个Bean为一组每组独立创建再通过MapperScan的basePackages和sqlSessionTemplateRef两个参数把不同包下的Mapper接口绑定到对应的SqlSessionTemplate上。从工程视角看这个方案有几个关键优势一是没有运行时切换一个Mapper从出生就确定属于哪个数据源不存在串库的路径。二是每个数据源都是完整独立的SqlSessionFactory事务管理器各管各的单库事务完全正常。三是排查问题简单——数据源配置类就两个翻一遍就能看到test1和test2的连线关系。下表把三者对比一下。对比项AOP动态切换JPA多数据源配置级绑定本方案核心机制运行时按key切DataSource多套EntityManagerFactory按包路由多套SqlSessionTemplate按包绑定适合场景主从读写分离、同结构库切换纯JPA项目分库MyBatis项目分库、不同结构库事务处理单事务管理器跨库回滚难每库独立跨库回滚难每库独立跨库回滚难代码复杂度切面注解ThreadLocal实体扫描配置两套配置类重复写两遍串库风险有无无上手难度中高中低2.4 为什么说这套方案更适合中小项目分库我自己的项目是业务分库模式test1是基础数据test2是业务流水两个库的表结构完全不同接口天然归属于某个库。这种情况下动态切换根本没有意义——我总不可能在一个方法里同时查询两份数据还要求它们同源。配置级绑定反而把结构理得很清楚com.neo.mapper.test1包下面的Mapper永远不会碰test2的连接新人看代码的时候很直观地知道去哪找数据源配置。代价是要接受代码冗余。每个数据源一套相同的四段式Bean注入数据源多了配置类会膨胀这是方案的天花板。好在绝大多数分库项目也就是两三个库写两遍配置类并不算负担相比AOP方案的隐藏问题这点重复代码完全值得。3. 配置文件与目录规划两条数据源怎么拆才不乱3.1 pom依赖少而精别引入多余的东西这套方案的依赖非常简单核心就是mybatis的starter和mysql驱动或对应数据库驱动不需要引入额外的多数据源框架。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency这里mybatis-spring-boot-starter版本按你自己Spring Boot版本匹配Boot 2.5以上用2.3.xBoot 3.x用3.0.x。如果数据库是MySQL 8驱动版本会自动随Boot管理不用特意指定。3.2 application.properties里两条数据源的写法配置文件是整套方案的入口数据源命名规则是spring.datasource.xxx开头后面跟driverClassName、url、username、password。这份资源里test1是主库test2是次库。mybatis.config-locationsclasspath:mybatis/mybatis-config.xml spring.datasource.test1.driverClassName com.mysql.jdbc.Driver spring.datasource.test1.url jdbc:mysql://localhost:3306/test1?useUnicodetruecharacterEncodingutf-8 spring.datasource.test1.username root spring.datasource.test1.password root spring.datasource.test2.driverClassName com.mysql.jdbc.Driver spring.datasource.test2.url jdbc:mysql://localhost:3306/test2?useUnicodetruecharacterEncodingutf-8 spring.datasource.test2.username root spring.datasource.test2.password root注意几个细节。第一driverClassName这里写的MySQL 5的驱动com.mysql.jdbc.DriverMySQL 8环境应该改成com.mysql.cj.jdbc.Driver否则启动会报ClassNotFound。第二url里useUnicodetruecharacterEncodingutf-8是防止中文乱码的标准参数但在properties文件里不用转义在yml里要写成amp;这个很多人第一次踩。第三mybatis.config-locations指定了全局mybatis配置文件的路径如果不需要额外配置这一行可以不写。如果项目用的application.yml写法等价于下面这样spring: datasource: test1: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/test1?useUnicodetruecharacterEncodingutf-8 username: root password: root test2: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/test2?useUnicodetruecharacterEncodingutf-8 username: root password: rootSpring Boot的ConfigurationProperties(prefix spring.datasource.test1)绑定数据源属性时driverClassName和driver-class-name是兼容的url和jdbc-url也是relaxed binding关系选一套风格用到底就行不要混写。3.3 Mapper目录分库Java包和XML必须同构多数据源配置类里的MapperScan是按包扫描的所以dao接口必须按库分包。这份资源的约定是com.neo.mapper.test1 test1库的Mapper接口 com.neo.mapper.test2 test2库的Mapper接口 classpath:mybatis/mapper/test1/*.xml test1库的XMLStatement classpath:mybatis/mapper/test2/*.xml test2库的XMLStatementXML文件放在resources/mybatis/mapper/test1和resources/mybatis/mapper/test2两个目录下与Java包一一对应。这个约定非常重要因为每个SqlSessionFactory的mapperLocations只按通配符加载对应目录写错目录会导致这个数据源的Mapper方法找不到SQL运行时报Invalid bound statement (not found)。目录不合理的第二个问题是XML的namespace指错包。比如test1库的xml里namespace写了com.neo.mapper.test2.User2MapperMyBatis不会报错但会导致你的test1数据源去加载test2的Mapper运行时出现无法预料的查询。所以XML里的namespace、Mapper接口全限定名、接口所在包三者必须严格一致。3.4 mybatis-config.xml里做什么资源里mybatis.config-locations指向的mybatis-config.xml通常用来放一些全局配置。实际项目中我一般会在里面开启驼峰映射或者设置日志实现?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings typeAliases package namecom.neo.entity/ /typeAliases /configuration一个容易忽略的细节是如果mybatis-config.xml里设置了环境environments信息这个环境会被SqlSessionFactoryBean重新设置的数据源覆盖所以不用刻意在xml里配DataSource。另外多数据源场景下typeAliases按包扫描没有问题但是两个实体的包如果不同且类型名相同别名会冲突建议实体类全限定名保持一致或直接使用全限定名。4. 核心实现DataSourceConfig类四段式注入与Mapper绑定4.1 主库配置类四段式Bean的完整写法先看主库test1的配置类这批代码是整套方案的核心。注意Primary注解它声明这个数据源是主数据源避免Spring在注入DataSource时因为有两个实现而抛异常。Configuration MapperScan(basePackages com.neo.mapper.test1, sqlSessionTemplateRef test1SqlSessionTemplate) public class DataSource1Config { Bean(name test1DataSource) ConfigurationProperties(prefix spring.datasource.test1) Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } Bean(name test1SqlSessionFactory) Primary public SqlSessionFactory testSqlSessionFactory(Qualifier(test1DataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mybatis/mapper/test1/*.xml)); return bean.getObject(); } Bean(name test1TransactionManager) Primary public DataSourceTransactionManager testTransactionManager(Qualifier(test1DataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } Bean(name test1SqlSessionTemplate) Primary public SqlSessionTemplate testSqlSessionTemplate(Qualifier(test1SqlSessionFactory) SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }逐层说下注入链路test1DataSource通过DataSourceBuilder.create().build()创建ConfigurationProperties(prefix spring.datasource.test1)把配置文件里spring.datasource.test1前缀下的属性绑定到DataSource。然后创建test1SqlSessionFactory注入test1DataSource并通过bean.setMapperLocations(...)加载test1目录下的xml。再创建test1TransactionManager这是test1库的事务管理器参数为test1DataSource。最后包一层test1SqlSessionTemplate它持有了SqlSessionFactory后续Mapper执行SQL时通过这个模板获得Session。MapperScan是这套方案里最关键的注解两个参数缺一不可basePackages指定扫描哪个包下的Mapper接口sqlSessionTemplateRef指名将这些Mapper绑定到哪个SqlSessionTemplate。注意这个Ref参数必须和Bean的name保持一致写错会导致Mapper创建时找不到模板。4.2 从库配置类没有Primary的镜像第二次配置test2的代码基本是复制粘贴但有一个原则性区别只能有一个数据源标Primary。test2配置类里所有的Bean都不加Primary否则主库身份会冲突。Configuration MapperScan(basePackages com.neo.mapper.test2, sqlSessionTemplateRef test2SqlSessionTemplate) public class DataSource2Config { Bean(name test2DataSource) ConfigurationProperties(prefix spring.datasource.test2) public DataSource testDataSource() { return DataSourceBuilder.create().build(); } Bean(name test2SqlSessionFactory) public SqlSessionFactory testSqlSessionFactory(Qualifier(test2DataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mybatis/mapper/test2/*.xml)); return bean.getObject(); } Bean(name test2TransactionManager) public DataSourceTransactionManager testTransactionManager(Qualifier(test2DataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } Bean(name test2SqlSessionTemplate) public SqlSessionTemplate testSqlSessionTemplate(Qualifier(test2SqlSessionFactory) SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }这里有个Spring Boot的配置细节值得重申如果两个数据源的Bean都不加Primary应用启动事务管理器时Spring无法确定哪个是默认DataSource会抛出NoUniqueBeanDefinitionException。所以主库必须标Primary且只有在主库上才标。而Qualifier(test1DataSource)的作用是精确注入指定名字的数据源Bean保证test1的SqlSessionFactory拿到的必须是test1的DataSource而不是主库的test1。4.3 事务管理器为什么要建两套每个SqlSessionFactory本身不管理事务事务交给DataSourceTransactionManager。这个方案里每个数据源单独建一个事务管理器意味着test1、test2的事务边界是隔离的。这个设计有两层意义。首先单库内部的事务依然完整——比如在User1Mapper.insert加一Transactional test1TransactionManager写test1库的业务抛异常会正常回滚。其次跨库事务是无法靠这个方案解决的因为两个事务管理器各自为战不会互相感知提交或回滚。因此涉及test1、test2两个库的操作不要放在一个方法里开着单个事务去写正确的做法是业务上拆分或者引入分布式事务。4.4 SqlSessionTemplate这层不能省有些人会想绕过SqlSessionTemplate直接把SqlSessionFactory注入Mapper写法更短。但MyBatis的SqlSessionFactory是线程安全的而SqlSession不是。SqlSessionTemplate是MyBatis-Spring提供的线程安全会话模板它在内部为每个Mapper方法调用打开一个SqlSession用完之后自动关闭适合注入到Spring管理的Bean中。如果直接持有SqlSessionFactory并手动openSession就要自己管理会话生命周期一不小心就产生连接泄漏。MapperScan里的sqlSessionTemplateRef指向的正是这个模板它把包内的Mapper接口和模板绑死。换个说法每个库的Mapper拿到的SqlSessionTemplate是独立的执行SQL时连接来自各自的DataSource天然不会串库。4.5 如果数据源变多怎么扩展再加一个test3库步骤固定为第一application.properties加一组spring.datasource.test3.*配置。第二新建DataSource3Config复制DataSource2Config把所有test2改成test3。第三在com.neo.mapper.test3包下放第三方库的Mapper接口在resources/mybatis/mapper/test3/放对应XML。三个库一定别把第几个是主库搞混主库永远只有一份Primary。注意这套方案不依赖自动配置的DataSourceAutoConfiguration。Spring Boot看到没有EnableAutoConfiguration排除时会因为classpath里存在DataSource而触发自动配置可能和你的自定义DataSource产生冲突。比较稳妥的做法是在启动类上加exclude DataSourceAutoConfiguration.classSpringBootApplication(exclude {DataSourceAutoConfiguration.class}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }加了这个排除后数据源完全由自定义Config管理不信你自己试一下把某个Bean删掉再启动报错会直指某个DataSource找不到非常清晰。5. 多数据源常见问题排查五个必踩的坑与修复5.1 启动即报NoUniqueBeanDefinitionException现象Spring Boot启动到一半抛NoUniqueBeanDefinitionException提示有两个DataSource类型的Bean无法确定注入哪个。原因配置类建了两个DataSource Bean但主库没标PrimarySpring在自动装配DataSourcer时找不到默认实现。解决在主库配置类的DataSource、SqlSessionFactory、TransactionManager、SqlSessionTemplate四个Bean上全部标Primary从库一个都不标。如果标了还报错检查启动类上是否加了exclude DataSourceAutoConfiguration.class因为DataSourceAutoConfiguration会在classpath里检测到DataSource后尝试二次注册两个来源的实现Bean会打架。5.2 Mapper方法全部报Invalid bound statement现象应用能启动但调用某个Mapper方法时抛Invalid bound statement (not found): com.neo.mapper.test1.User1Mapper.getAll。原因常见的有三种MapperScan的包路径没扫到该MappermapperLocations的通配路径写错目录xml没有被加载xml的namespace与Mapper的全限定名不一致。解决先看target/classes目录下xml有没有被拷进来。Maven项目里xml如果放在src/main/java下pom没配置resources过滤打包时会漏掉xml需要改pombuild resources resource directorysrc/main/resources/directory includes include**/*.xml/include /includes filteringfalse/filtering /resource /resources /build然后对照mapperLocations路径确认classpath:mybatis/mapper/test1/*.xml与实际文件路径一致。最后检查XML里namespace写的是不是com.neo.mapper.test1.User1Mapper注意test1和test2别串了。5.3 数据源连接偶发串库SQL跑到另一个库现象test1的Mapper查询偶尔返回test2库的数据或者在日志里看到执行SQL的JDBC连接URL是另一个库。原因多数是HikariCP连接池在多数据源下没有正确隔离或者两个SqlSessionFactoryBean创建时引用了同一个DataSource Bean。解决检查每个SqlSessionFactoryBean的setDataSource是否通过Qualifier精确注入了对应数据源。如果直接在方法参数里写DataSource dataSourceSpring会注入Primary那个test1DataSource两个SqlSessionFactory就指向同一个连接池必串库。排查手段是在每个SqlSessionFactoryBean创建时打印数据源信息System.out.println(test1 factory bind to: dataSource);启动日志里能看到两者指向不同的DataSource实例哈希哈希一致说明引用错了。5.4 Transactional下的数据源切换不生效问题现象方法上加了Transactional后实际执行的SQL没有走预期的数据源或者从库写数据时事务没有回滚。原因多数据源配置下Transactional默认的事务管理器是DataSourceTransactionManager类型中标注Primary的那个。如果test2的Mapper操作被方法级Transactional包裹事务管理器默认绑定了test1的模板test2的SQL就会不在任何事务管理下执行。解决跨库操作必须显式指定事务管理器。单库方法上写明Transactional(transactionManager test1TransactionManager)与Transactional(transactionManager test2TransactionManager)分别对应两个库Transactional(transactionManager test2TransactionManager) public void saveUser(UserEntity user) { user2Mapper.insert(user); }特别注意如果同一个方法里既写test1又写test2没有一个事务管理器能同时管理两个库此时要么拆分方法、要么用全局分布式事务不能幻想Transactional帮你搞定一切。这不是Spring的缺陷而是单机事务管理器本就无法跨物理库。5.5 PageHelper分页插件失效或分页串库现象引入PageHelper做分页第一个库的查询分页正常第二个库的分页不生效或者limit字段错乱。原因PageHelper的拦截器是绑定到SqlSessionFactory上的它对拦截器创建时挂载的会话工厂生效。如果只给test1的SqlSessionFactoryBean配置了拦截器test2的SqlSessionFactory没配test2的Mapper查询就不经过拦截器分页也就是一句空话。解决在每个SqlSessionFactoryBean上单独配置拦截器而不是只配置全局的mybatis-configBean(name test2SqlSessionFactory) public SqlSessionFactory testSqlSessionFactory(Qualifier(test2DataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); Interceptor[] interceptors new Interceptor[]{new PageInterceptor()}; bean.setPlugins(interceptors); bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mybatis/mapper/test2/*.xml)); return bean.getObject(); }这段代码里的PageInterceptor是com.github.pagehelper.PageInterceptor要配合在properties里配好pagehelper.helperDialectmysql如果还想优化把pagehelper的offsetAsPageNum等参数在mybatis-config.xml里设置。以后每新增一个数据源都要记得把拦截器同步加进去漏一个就废一个。6. 验证与扩展Controller直连测试和事务边界处理技巧验证这套方案是否真的把两个库分开最快的方式是写一个Controller分别注User1Mapper和User2Mapper执行这批接口后看日志里的连接归属。RestController public class UserController { Autowired private User1Mapper user1Mapper; Autowired private User2Mapper user2Mapper; GetMapping(/getUsers) public ListUserEntity getUsers() { return user1Mapper.getAll(); } GetMapping(/getUser) public UserEntity getUser(Long id) { return user2Mapper.getOne(id); } GetMapping(/add) public void save(UserEntity user) { user2Mapper.insert(user); } GetMapping(/update) public void update(UserEntity user) { user2Mapper.update(user); } DeleteMapping(/delete/{id}) public void delete(PathVariable(id) Long id) { user1Mapper.delete(id); } }注意这里user1Mapper和user2Mapper是两个包的接口注入Spring后都由IoC容器创建Mapper代理不存在冲突。测试时可以加一行日志配置来确认SQL落库去向logging.level.com.neo.mapper.test1debug logging.level.com.neo.mapper.test2debug打开debug日志后每次SQL执行控制台会打印这条SQL的JDBC连接信息你可以直接用这个技巧快速验证两个Mapper是否各自走各自的库不用翻数据库binlog。这个链路里还有一个很小的扩展习惯如果你的分库场景里需要读一个库、主库写一个库而两个库表结构相同那不需要把Mapper包拆开只要保证主库SqlSessionFactory扫描公共Mapper包从库的Mapper包为空即可。但如果是业务分库两个库的表结构不同则必须维持本方案“包按库拆、XML按库拆”的约定。跨库查询是这个方案的软肋也是最容易把项目搞复杂的地方。如果业务上你确实要在一次请求里同时读test1和test2的数据一个Controller里依次调用两个Mapper是没有问题的但不要把它们包在同一个Transactional(transactionManager test1TransactionManager)里那会把test2的写入也归到test1的事务管理回滚和提交完全错乱。最安全的方式是拆成两个方法分别标注各自的事务管理器或者干脆让两个查询都在无事务状态下执行。到这我把这套方案和相关的坑全部理清了一遍。从那以后我每次新建一个数据源配置类都强制走一遍核对流程先确认启动类有没有排除DataSourceAutoConfiguration再看MapperScan的包路径是不是对应Mapper包然后查mapperLocations的xml目录拼写最后盯着Primary有没有加错到从库上。这套笨办法反而帮我避开了绝大多数多数据源翻车点希望帮到你。本文还有配套的精品资源点击获取
返回列表