news 2026/10/13 5:11:54

Spring Boot 整合 MyBatis 实战:从依赖选型到多数据源与事务管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 整合 MyBatis 实战:从依赖选型到多数据源与事务管理

1. 为什么还要聊 Spring Boot 整合 MyBatis

1.1 一个老生常谈但绕不开的组合

但凡做过几年 Java 后端的人,简历上大概率都写过“熟悉 Spring Boot、MyBatis”。但真到了新项目搭架子的时候,很多人还是会卡在几个具体问题上:依赖到底引哪个 starter、Mapper 接口要不要加@Mapper注解、XML 文件放哪里才能被扫描到、多数据源怎么切、事务为什么不生效。这些问题单看都不难,凑在一起就足够让人调半天。

我这几年带过几个刚入行的同学,发现一个共性现象:他们能照着教程把 CRUD 跑通,但一旦报错就完全不知道从哪查起。根子在于,他们只记住了“怎么写”,没搞明白“为什么这么写”。Spring Boot 整合 MyBatis 这件事,表面上是加两个依赖、写几个注解,背后其实是 Spring 的自动装配机制、MyBatis 的 SqlSession 生命周期、以及两者之间那层适配胶水在起作用。把这层逻辑理清楚,后面遇到再奇怪的问题,你都能顺着线索摸到源头。

这篇内容我打算按实际搭项目的顺序来讲:从依赖选型开始,到配置、编码、事务、多数据源,最后是排查问题的思路。适合刚接触这个组合的新手,也适合用了很久但一直“知其然不知其所以然”的朋友。我会尽量把每个选择背后的理由说透,而不是甩一段配置让你抄。

1.2 先搞清楚 MyBatis 在 Spring Boot 里的角色定位

很多人把 MyBatis 理解成“一个操作数据库的工具”,这个说法不算错,但太粗。准确点讲,MyBatis 是一个半自动的持久层框架,它负责的是 Java 对象和 SQL 之间的映射,把 JDBC 那套繁琐的 Connection、Statement、ResultSet 操作封装掉,但把 SQL 的控制权完整留给你。

Spring Boot 在这里扮演的是“容器”和“装配工”的角色。它通过自动配置,帮你把SqlSessionFactory、SqlSessionTemplate、DataSource这些对象创建好并放进容器,你只需要告诉它:数据源在哪、Mapper 接口在哪、XML 映射文件在哪。剩下的对象创建、依赖注入、事务绑定,全由 Spring 接管。

理解这个分工很关键。当你遇到“Mapper 注入失败”的时候,你要想的是 Spring 有没有扫描到这个接口;当你遇到“Invalid bound statement”的时候,你要想的是 MyBatis 有没有加载到对应的 XML 或注解 SQL。两个框架各管一段,报错信息其实已经暗示了该往哪个方向查。

2. 依赖选型与版本搭配的门道

2.1 starter 到底选哪个

打开任何一个依赖仓库搜 mybatis spring boot,你会看到好几个长得差不多的东西,新手最容易在这里犯迷糊。常见的有两类:一类是 MyBatis 官方团队维护的mybatis-spring-boot-starter,另一类是第三方封装的增强工具(比如带通用 Mapper、分页插件的那种一体化 starter)。

我的建议很明确:新项目优先用官方 starter。原因是它足够轻,只做整合这件事,不夹带私货。你想加分页就单独引分页插件,想加通用 Mapper 就单独引,职责清晰,出问题也好定位。那些“全家桶”式的 starter 看着省事,但一旦版本冲突或者某个功能不符合预期,排查成本会成倍上升。

依赖大概长这样:

<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

这里有个细节值得说:MySQL 驱动从 8.0 之后,groupId 从mysql变成了com.mysql,artifactId 也从mysql-connector-java改成了mysql-connector-j。如果你从老项目复制依赖,很可能引到一个已经停止维护的旧坐标,虽然还能跑,但没必要。

2.2 版本搭配的坑

Spring Boot 和 mybatis-spring-boot-starter 之间有版本对应关系,这个不是随便配的。大原则是:starter 的主版本要跟 Spring Boot 的主版本对齐。比如 Spring Boot 3.x 对应 starter 3.x,Spring Boot 2.x 对应 starter 2.x。

为什么这么讲究?因为 Spring Boot 3 把底层从 Java EE 换成了 Jakarta EE,包名从javax.*变成了jakarta.*。如果你的 starter 版本没跟上,启动时就会报ClassNotFoundException或者NoClassDefFoundError,而且报的类名往往很底层,新手看了完全懵。我见过有人在这上面耗了一下午,最后发现就是版本差了一个大版本号。

给你一个简单的对照参考:

Spring Boot 版本starter 版本JDK 要求
2.7.x2.3.xJDK 8+
3.0.x - 3.1.x3.0.xJDK 17+
3.2.x 及以上3.0.3+JDK 17+

提示:升级 Spring Boot 大版本时,一定要同步检查 starter 版本,别只改一个。

2.3 要不要引连接池

有人会问,Spring Boot 不是自带 HikariCP 吗,还需要单独引吗?答案是:只要你引了spring-boot-starter-jdbc(mybatis starter 会间接带上它),HikariCP 就已经在了,不用额外引。HikariCP 是目前性能和稳定性都比较均衡的选择,默认配置对大多数中小项目够用。

但如果你用的是 Druid,那就需要单独引依赖并写配置。Druid 的优势在于监控面板和丰富的连接池参数,适合对数据库连接有精细化管控需求的场景。选哪个没有绝对对错,看团队习惯和运维要求。我个人的经验是:业务系统用 HikariCP 就够了,除非你有明确的监控诉求,否则没必要为了一个面板引入额外复杂度。

3. 配置文件的写法与关键参数

3.1 数据源配置的最小集

application.yml里数据源这块,最小可用配置其实就四项:url、username、password、driver。但实际项目里我建议把连接池的几个关键参数也写上,避免默认值在高并发下出问题。

spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

逐个说下这些参数为什么这么设。maximum-pool-size设 20 是个经验值,公式上一般参考CPU核数 * 2 + 磁盘数,但实际还要看数据库能承受多少连接。设太大反而会把数据库压垮,设太小又会在高峰期排队。connection-timeout30 秒是给足重试空间,太短容易误报,太长会让请求堆积。max-lifetime设 1800 秒(30 分钟)是为了避开数据库服务端可能存在的连接空闲回收时间,防止拿到已经被服务端关掉的死连接。

URL 里的serverTimezone一定要写,不写在某些驱动版本下会报时区错误。useSSL=false在本地开发环境可以关掉,生产环境按实际安全要求决定。

3.2 MyBatis 相关配置

MyBatis 自己的配置项,放在mybatis节点下:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true cache-enabled: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

mapper-locations告诉 MyBatis 去哪找 XML 文件。这里有个容易踩的坑:如果你用 Maven 构建,XML 文件放在src/main/java目录下,默认是不会被打包进 classpath 的,需要在pom.xml里配置resources把**/*.xml包含进去。很多人本地 IDE 里跑得好好的,一打包就报找不到映射文件,就是这个原因。

map-underscore-to-camel-case这个开关强烈建议打开。数据库字段习惯用下划线(比如user_name),Java 属性习惯用驼峰(userName),打开这个开关后 MyBatis 会自动帮你转换,省掉大量手动映射。不开的话,每个字段都得在 resultMap 里写一遍,纯属给自己找活干。

log-impl设成标准输出是为了开发期能看到实际执行的 SQL,方便调试。上线前记得关掉或者换成日志框架实现,不然控制台会被刷屏,也影响性能。

3.3 一个常被忽略的配置:别名包

type-aliases-package指定后,XML 里的resultType就可以只写类名,不用写全限定名。比如com.example.demo.entity.User可以直接写成User。这个在实体类多的项目里能省不少事,但要注意别让不同包下有同名类,否则会冲突。

4. 编码层面的核心要点

4.1 Mapper 接口的两种扫描方式

Mapper 接口要被 Spring 识别成 Bean,有两种做法。第一种是在每个接口上加@Mapper注解,简单直接,但接口多了就得一个个加。第二种是在启动类上加@MapperScan("com.example.demo.mapper"),一次性扫描整个包。

我推荐第二种。原因有两个:一是省事,新增接口不用管;二是集中管理,扫描路径一目了然。用@MapperScan的时候注意包路径别写太宽,比如写成com.example会把 service、controller 也扫进去,虽然一般不会出错,但没必要。

@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

注意:@Mapper和@MapperScan不要同时用,虽然不报错,但容易让人困惑到底哪个在起作用。

4.2 XML 映射文件的规范写法

一个标准的 Mapper XML 长这样:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <resultMap id="BaseResultMap" type="User"> <id column="id" property="id"/> <result column="user_name" property="userName"/> <result column="create_time" property="createTime"/> </resultMap> <select id="selectById" resultMap="BaseResultMap"> SELECT id, user_name, create_time FROM t_user WHERE id = #{id} </select> </mapper>

几个关键点。namespace必须和 Mapper 接口的全限定名完全一致,差一个字母都会报Invalid bound statement。id必须和接口方法名一致。resultMap和resultType二选一,字段名和属性名能自动对上就用resultType,对不上或者有复杂映射就用resultMap。

#{}和${}的区别必须搞清楚。#{}是预编译占位符,会生成?,能防 SQL 注入;${}是字符串拼接,直接替换进 SQL,有注入风险。除非是动态表名、动态排序字段这种没法用占位符的场景,否则一律用#{}。我见过有人图省事全用${},这是很危险的习惯。

4.3 注解方式和 XML 方式怎么选

MyBatis 支持在接口方法上直接写 SQL 注解,比如@Select、@Insert。简单查询用注解确实清爽,不用来回切文件。但一旦 SQL 变复杂,比如带动态条件、多表关联,注解里拼字符串会非常难维护。

我的经验分界线是:单表简单 CRUD 用注解,复杂查询用 XML。一个项目里两种方式混用完全没问题,MyBatis 都支持。但同一个方法不要既写注解又在 XML 里写,会冲突。

动态 SQL 是 XML 的强项,<if>、<choose>、<foreach>、<where>这些标签组合起来,能优雅地处理各种条件拼接。比如批量插入:

<insert id="batchInsert"> INSERT INTO t_user (user_name, create_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.userName}, #{item.createTime}) </foreach> </insert>

用注解写这个就很别扭。所以别纠结哪种“更高级”,按场景选就行。

5. 事务管理与多数据源实战

5.1 事务为什么有时候不生效

@Transactional注解失效是高频问题。常见原因有这么几个:方法不是 public 的、同类内部方法直接调用、异常被 catch 了没抛出去、或者异常类型不在回滚范围内。

Spring 的声明式事务是基于代理实现的。同类内部调用走的是this引用,不经过代理对象,事务自然不生效。解决办法是把方法拆到另一个 Bean 里,或者注入自己。异常这块,默认只对RuntimeException和Error回滚,如果你抛的是受检异常,得显式写@Transactional(rollbackFor = Exception.class)。

@Service public class UserService { @Transactional(rollbackFor = Exception.class) public void createUser(User user) { userMapper.insert(user); // 其他数据库操作 } }

我一般建议直接加上rollbackFor = Exception.class,省得以后忘了。多花几个字符,换来的是一份安心。

5.2 多数据源的配置思路

单数据源靠 Spring Boot 自动配置就够了,多数据源就得手动配置。核心思路是:为每个数据源单独创建DataSource、SqlSessionFactory、SqlSessionTemplate,并指定各自的 Mapper 扫描路径。

配置类大概这样:

@Configuration @MapperScan(basePackages = "com.example.demo.mapper.primary", sqlSessionTemplateRef = "primarySqlSessionTemplate") public class PrimaryDataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/primary/*.xml")); return bean.getObject(); } @Bean public SqlSessionTemplate primarySqlSessionTemplate( @Qualifier("primarySqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } }

第二个数据源照葫芦画瓢,改包路径和 Bean 名字。这里的关键是@MapperScan里的sqlSessionTemplateRef要指向对应的模板,否则 Mapper 会绑错数据源。

多数据源下事务会更麻烦,因为默认的DataSourceTransactionManager只能管一个数据源。跨库事务要么用分布式事务方案,要么在业务上拆开,别硬凑在一个事务里。我的建议是:能不用多数据源就不用,实在要分库,尽量让业务边界清晰,避免跨库写操作。

6. 常见问题排查速查表

6.1 启动阶段报错

报错信息大概率原因排查方向
Invalid bound statement (not found)XML 没被加载或 namespace/id 不匹配检查 mapper-locations、namespace、方法名
Consider defining a bean of type 'xxxMapper'Mapper 没被扫描到检查 @MapperScan 路径
ClassNotFoundException: javax.*版本不匹配检查 starter 与 Spring Boot 版本
Failed to configure a DataSource数据源配置缺失检查 url、driver 配置

6.2 运行阶段问题

Invalid bound statement这个报错我单独拎出来说,因为它太常见了。排查顺序是:先确认 XML 文件在不在编译后的 classpath 里(去 target 目录看),再确认 namespace 和接口全限定名是否一字不差,最后确认方法名和 id 是否对应。三步走下来基本都能定位。

还有一个隐蔽的坑:如果你的 XML 放在src/main/java下,IDE 里能跑是因为 IDE 把源目录也当资源目录处理了,但 Maven 打包不会。所以一定要在 pom 里配:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>

6.3 性能相关的排查

慢 SQL 是另一个高频问题。开发期把log-impl打开,能看到每条 SQL 和参数,配合数据库的慢查询日志,基本能定位到具体语句。常见原因有:没走索引、返回了过多字段、N+1 查询。

N+1 查询在 MyBatis 里通常是因为用了嵌套查询(<association>里再 select),一条主查询带出 N 条子查询。解决办法是改成嵌套结果(用 join 一次查出来再映射),或者业务层批量查。这个坑在关联对象多的场景特别容易踩。

提示:开发期一定要开 SQL 日志,上线前关掉。这个习惯能帮你省下大量排查时间。

7. 我踩过的几个真实坑

7.1 驼峰转换没开导致的“字段全 null”

刚工作那会儿,查出来的对象除了 id 全是 null,查了半天以为是映射写错了。后来才发现是map-underscore-to-camel-case没开,数据库的user_name映射不到userName。这个开关默认是 false,一定要手动打开。开了之后,只要字段名和属性名符合下划线转驼峰的规则,就不用写 resultMap 了。

7.2 批量操作时的 foreach 陷阱

批量插入用<foreach>的时候,如果集合为空,生成的 SQL 会变成INSERT INTO t_user VALUES,直接语法错误。所以调用前一定要判空。另外collection属性的值,如果参数没加@Param,默认是list或array,加了@Param("users")之后就得写users。这个细节不注意就会报Parameter 'list' not found。

7.3 事务里查不到刚插入的数据

这个现象是:同一个事务里,先 insert 再 select,select 查不到刚插的数据。原因是 MyBatis 的一级缓存和数据库的隔离级别共同作用。MyBatis 一级缓存默认开启,同一个 SqlSession 里相同的查询会走缓存。如果中间有写操作,缓存会被清掉,正常情况能查到。但如果你的查询和插入走了不同的 SqlSession(比如多数据源或者手动开了新会话),就可能出现查不到的情况。排查时先确认是不是同一个事务、同一个 SqlSession。

7.4 连接池耗尽

有次压测,请求量一上来就报Connection is not available, request timed out。查下来是maximum-pool-size设太小,加上有个慢查询一直占着连接不释放。解决办法是先把慢查询优化掉,再适当调大连接池,同时把connection-timeout调短一点,让拿不到连接的请求快速失败而不是一直等。连接池参数没有万能值,一定要结合压测结果调。

8. 一些能提升效率的实践建议

8.1 用 Lombok 简化实体类

实体类的 getter、setter、toString 写起来很烦,用 Lombok 的@Data一行搞定。配合@Builder还能链式构造对象。这个不算 MyBatis 的内容,但实际项目里几乎都会用,能省下大量样板代码。

8.2 分页插件的选择

MyBatis 本身不带分页,需要自己写 limit 或者引插件。常用的分页插件配置简单,加个依赖和拦截器配置就能用,PageHelper.startPage(pageNum, pageSize)之后紧跟查询即可。注意startPage只对后面第一条查询生效,别写错位置。

8.3 把 SQL 日志接入日志框架

开发期用StdOutImpl没问题,但生产环境应该用Slf4jImpl,把 SQL 打到日志文件里,方便事后追溯。配置改成log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl,再调整对应 logger 的级别就行。

8.4 定期检查 Mapper 与 XML 的一致性

项目大了之后,接口方法和 XML 里的 id 容易对不上,尤其是重构改名的时候。可以写个简单的单元测试,启动上下文后遍历所有 Mapper 接口的方法,检查是否有对应的 statement。这个投入不大,但能提前发现不少低级错误。

这套组合用了这么多年,我的整体感受是:它足够灵活,SQL 完全可控,适合对性能有要求、SQL 写得比较讲究的团队。代价就是配置和排查需要一定的经验积累。把上面这些点吃透,日常开发基本不会再被卡住。真正难的从来不是“怎么整合”,而是“出了问题怎么快速定位”,而这恰恰是靠理解原理和踩坑积累出来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:11:29

SpringBoot汽车美容保养预约系统实战解析

很多准备做Java毕业设计的同学&#xff0c;估计都见过这类标题&#xff1a;基于SpringBoot的汽车美容与保养网站系统、汽车养护web系统、4S店保养服务管理系统。说实话&#xff0c;这几个名字听起来像三个项目&#xff0c;但本质上说的都是同一类东西——围绕汽车后市场服务场景…

作者头像 李华
网站建设 2026/10/13 5:11:23

Wireshark抓HTTP包:从字节流到协议分析的实战指南

1. 这不是“看个包”那么简单&#xff1a;HTTP抓包背后的真实工作流Wireshark抓HTTP包&#xff0c;很多人第一反应是“点开软件&#xff0c;点一下开始&#xff0c;等网页加载完&#xff0c;再点停止”——这确实能抓到数据&#xff0c;但离真正理解网络行为差了至少三层楼。我…

作者头像 李华
网站建设 2026/10/13 5:10:58

C#+SQL Server外卖系统课设:数据库设计到三种角色全流程解析

简介&#xff1a;这份餐饮外卖销售系统数据库设计压缩包&#xff0c;基于C#与SQL Server 2019开发&#xff0c;覆盖商家、客户、骑手三类角色界面及注册登录模块&#xff0c;面向高校数据库课程设计参考&#xff0c;适合需要完成同类业务系统设计的中高级学生。压缩包共46个文件…

作者头像 李华
网站建设 2026/10/13 5:10:41

Python量化交易入门:从数据清洗到回测引擎的完整实践

做量化交易这一行&#xff0c;绕不开 Python&#xff0c;但凡你把“入门”两个字当真&#xff0c;就不该急着去买策略、看实时行情、上模拟盘。我第一次接触这个方向时&#xff0c;以为只要把一个现成的金叉死叉策略跑通&#xff0c;就能躺赢&#xff0c;结果连续两个周末都在跟…

作者头像 李华
网站建设 2026/10/13 5:10:39

AI研发自动化与智能爆炸:正反馈回路、验证瓶颈与工程推演

1. 从一个思想实验说起&#xff1a;为什么“智能爆炸”值得认真对待“如果 AI 研发自动化触发了智能爆炸&#xff0c;会怎样&#xff1f;”这个问题我第一次看到时&#xff0c;第一反应是把它归类成科幻小说的桥段。但仔细琢磨之后发现&#xff0c;它其实是一个相当严肃的系统工…

作者头像 李华
网站建设 2026/10/13 5:10:23

心理健康监测疏导小程序全链路复盘:测评、预警与数据安全设计

先说说最近在复盘的一个项目——心理健康监测与疏导服务平台小程序。这些年心理服务类产品越来越多&#xff0c;但真正把“监测”和“疏导”串成一个完整闭环的不算多。大部分产品要么只做量表测评&#xff0c;测完给一段文案就结束了&#xff1b;要么只做倾诉聊天&#xff0c;…

作者头像 李华