Spring Boot 集成 MyBatis 核心配置与常见坑位排查指南
2026/9/8 17:19:34 网站建设 项目流程

这几年做 Java 后端,Spring Boot 项目基本避不开持久层框架选型。MyBatis 在国内团队里的出场率一直很高,原因很直接:SQL 可控、上手门槛低、动态 SQL 够灵活。但真正把 Spring Boot 和 MyBatis 集成好,不是说加个依赖、写个 Mapper 接口就完事了。数据源怎么配、SQL 日志怎么打、缓存什么时候生效、多数据源下事务怎么走、为什么启动时 Mapper 接口报错,这些坑我基本都踩过一轮。这篇文章我把自己在项目里沉淀下来的集成思路、配置细节、易错点和排查方法做了个相对系统的梳理,适合刚接触 Spring Boot 整合 MyBatis 的初学者,也能帮写过一段时间但没系统性排查过问题的同学查漏补缺。

在开始之前先说一句:下面所有的配置和代码,我都基于 Spring Boot 2.7.x + MyBatis Spring Boot Starter 2.3.x 这套常见组合来写。你在实际项目里如果用 Spring Boot 3.x,需要对应换成 mybatis-spring-boot-starter 3.x,javax 包名也要改成 jakarta,这个版本问题在集成时很关键,先提个醒。

1. 集成前的整体规划:选型与版本匹配

1.1 先想清楚为什么选 MyBatis,而不是 JPA 或 JDBC

很多初学者一上来就搜“Spring Boot 怎么集成 MyBatis”,然后照着教程把依赖复制进去,能跑通就觉得完成了。但实际上,选型阶段的认知会直接决定后面几个月的开发效率。MyBatis 核心价值是“SQL 由开发者掌控”,它不会替你去生成那些你不想要的 SQL,也不会在 N+1 查询这种问题上隐藏成本。对复杂查询、报表统计、多表关联这类场景,写 SQL 反而比 JPA 的实体关系映射更直接。

当然,JPA 在上手速度和对象建模上有优势,这一点不否认。但在生产环境中,我见过不少项目因为 JPA 的懒加载和缓存问题不得不天天查 SQL 日志。既然标题是 MyBatis 集成,我就不展开 JPA 了。你需要知道的是:如果你团队里能写好 SQL 的人占多数,MyBatis 大概率是更可控的选择。

1.2 版本匹配是集成第一步,但不是看了版本号就完事

Spring Boot 集成 MyBatis 最怕的不是写错 Mapper,而是依赖冲突和版本不匹配。MyBatis 官方提供了mybatis-spring-boot-starter,它会自动引入mybatismybatis-spring,你只要在pom.xml里声明 starter 版本即可,不用手动管理底层三个组件的版本。

我给你的建议是:

  • Spring Boot 2.4.x ~ 2.7.x,使用mybatis-spring-boot-starter 2.3.x或稍高一点的 2.x 版本。
  • Spring Boot 3.x,使用mybatis-spring-boot-starter 3.x
  • 避免把org.mybatis.spring.boot的开头和com.baomidou:mybatis-plus-boot-starter混着引。如果你要用 MyBatis-Plus,就直接引 Plus 的 starter,它会带自己的 mybatis 版本。

之前有个项目因为早期引入了 MyBatis 官方 starter,后来又为了“懒人分页”加了 MyBatis-Plus 相关模块,结果两套注解扫描全在工作,Mapper 代理生成完全错乱。那类问题排查起来成本很高,所以先把依赖规划清楚。

1.3 项目里常用的依赖搭配

这里我给出一份比较常见的pom.xml依赖清单,你可以直接参照。注意我没有贴出完整的工程文件,只挑关键的依赖展示:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

关于连接池,很多教程用 HikariCP,因为 Spring Boot 默认集成了。但我个人在国内团队项目里更习惯用 Druid,因为它提供了监控 SQL、慢查询、活跃连接数等很多可视化能力。不需要监控的情况下,HikariCP 也完全够用。如果用了 Druid,可以把防火墙和连接泄漏检测打开,这两个功能在联调环境里帮过我大忙。

2. 从配置到第一个可用 Mapper:实操过程记录

2.1 application.yml 里的关键配置

集成 MyBatis,最核心的配置量并不多,但每项都有讲究。我先给一份带注释的配置,然后再逐个解释。

spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 max-active: 20 min-idle: 5 max-wait: 60000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true default-fetch-size: 100 default-statement-timeout: 30 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

第一类配置是数据源。driver-class-name在 MySQL 8.x 下必须是com.mysql.cj.jdbc.Driver,不要再用老的com.mysql.jdbc.Driver,否则容易遇到警告或者驱动加载异常。URL 里我建议显式带上serverTimezone=Asia/Shanghai,否则连 MySQL 8 有时候会报时间区错误。

第二类配置是 Mapper 相关。mapper-locations用于指定 XML 文件位置,这里的写法是classpath:mapper/*.xml,表示把编译后的mapper目录下所有 XML 都加载进来。type-aliases-package可以简写 Mapper XML 里的resultType,比如resultType="User"会自动匹配到com.example.demo.entity.User

第三类配置是 MyBatis 全局行为。map-underscore-to-camel-case太好用了,开启后数据库字段user_name能直接映射到 Java 属性userName,不用每个字段都写 resultMap。如果你用了 Lombok 且字段命名规范,基本可以省掉八成的手动映射。

2.2 实现一个最小可用的 Mapper 链路

我以一个用户表举例,实体类很简单:

@Data public class User { private Long id; private String name; private Integer age; private String email; }

Mapper 接口:

public interface UserMapper { User selectById(@Param("id") Long id); List<User> selectPage(@Param("offset") int offset, @Param("limit") int limit); }

在启动类上要先加@MapperScan("com.example.demo.mapper"),这个注解负责扫描 Mapper 接口并生成动态代理。很多新手的“Mapper 无法注入”问题八成是没扫描到或路径写错。如果不想用@MapperScan,也可以在每个 Mapper 接口上加@Mapper,但比较繁琐,我一般只用@MapperScan方式。

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"> <select id="selectById" resultType="User"> select id, name, age, email from user where id = #{id} </select> <select id="selectPage" resultType="User"> select id, name, age, email from user order by id limit #{offset}, #{limit} </select> </mapper>

这里的 namespace 必须和接口全限定名一致,id也必须和接口方法名一致。看起来是强约定,但执行时 MyBatis 正是通过这个 namespace 来绑定代理方法。如果 XML 里的 namespace 写错或没写,启动时不一定会报错,但调用时会告诉你Invalid bound statement (not found),这种翻译过来就是最常见的绑定异常。

2.3 开发环境命令行运行项目的两种方式

我以前带项目时,常有同事问“开发环境不用 IDEA 怎么跑 Spring Boot 集成 MyBatis 的项目”。绝大多数情况下,如果你的电脑上装了 Maven,直接进到项目根目录执行:

mvn spring-boot:run

这种方式会先编译项目,然后启动内嵌 Tomcat,读取 classpath 下的application.yml。如果你的项目里还引入了 Spring Boot Maven 插件,也可以用:

mvn clean package -DskipTests java -jar target/xxx.jar --spring.profiles.active=dev

生产常用第二种。不过要注意的是,用命令行跑时最容易出现的问题就是appliocation.ymlmapper/*.xml没有被正确打进 jar。检查方式很简单:

jar tf target/xxx.jar | grep mapper

如果 XML 没在 jar 里,大概率是构建插件把 resources 目录过滤掉了。解决办法是在pom.xml<build><resources>中显式声明保留 xml。

3. 深入核心细节:动态 SQL、分页和逻辑删除

3.1 动态 SQL 标签别死记硬背,先理解场景

MyBatis 最核心的亮点是动态 SQL,也是面试和工作中少不了的考察点。ifchoosewhenotherwisetrimwheresetforeach这些标签各有用途,但如果你不理解场景,仅仅背标签没有任何意义。

最常见的场景是前端传了一堆筛选条件,后端要做多条件查询。你可能会先想出这样的 SQL:

<select id="listByCondition" resultType="User"> select * from user where 1 = 1 <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="age != null"> and age = #{age} </if> </select>

where 1 = 1是老式写法,能用但不太好看。MyBatis 提供了<where>标签,它能自动去除多余的andor

<select id="listByCondition" resultType="User"> select * from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="age != null"> and age = #{age} </if> </where> </select>

foreach则适用于in查询:

<select id="listByIds" resultType="User"> select * from user where id in <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

这里有一个我一直强调的小细节:collection的值要和接口参数对应。如果方法签名是List<User> listByIds(@Param("ids") List<Long> ids),collection 就写ids;如果没有任何@Param,直接传一个 List,MyBatis 会自动包装成list,这时 collection 要写成list

3.2 分页查询不用傻傻手写 limit?分页插件和手写如何选

标题里的热搜词“mybatis查询增加行号”很贴近真实开发需求。在 MySQL 中,你可以用row_number()开窗函数,也可以用自定义变量。比如:

<select id="listWithRowNo" resultType="map"> select (@rownum := @rownum + 1) AS row_no, id, name from user, (select @rownum := 0) r </select>

这里的事务情景有点像帮助排显示顺序,缺点是比较容易受连接顺序影响,而且稍微复杂点 SQL 就可能失效。我更推荐配合分页时用标准 SQL 的方式:先按排序规则取出子查询,再用row_number()包一层。

至于分页插件,常见选择是 MyBatis-Plus 自带的 PaginationInnerInterceptor。手写limit #{offset}, #{limit}也很清晰,唯一的痛点是要自己计算 offset,所以很多项目还是上了 MyBatis-Plus。毕竟 MyBatis-Plus 的分页插件在物理分页层面做得比较成熟,可以避免内存分页的问题。

3.3 和 MyBatis-Plus 的差距:从“查询时禁用逻辑删除”说起

热搜词里有“mybatis plus 查询 禁用逻辑删除”,这确实是工作中常遇到的坑。MyBatis-Plus 的逻辑删除配置后,每次查询都会自动追加deleted = 0之类的条件,这在绝大多数业务下是正确的。但有时候你需要去“包括已删除数据”的历史表里捞数据,比如做数据对账、运营后台看全量记录,这时候自动拼接的deleted=0反而成了障碍。

MyBatis-Plus 官方提供了一个@InterceptorIgnore注解,可以作用于方法级别,在指定查询时让拦截器失效。但如果你的项目只用官方 MyBatis,而不是 MyBatis-Plus,就没有这个逻辑删除概念。你以为你在看 MyBatis 集成文章,其实你遇到的问题属于 MyBatis-Plus 扩展功能。为了避免混淆,我们做一个明确区分:

  • 原生 MyBatis:没有内置逻辑删除,没有内置分页插件,CRUD 要自己写得相对细。
  • MyBatis-Plus:增强工具,在 MyBatis 基础上提供通用 Mapper、分页插件、逻辑删除、代码生成器等能力。
  • 两者不是二选一对立的,MyBatis-Plus 底层依赖的就是 MyBatis。

你要是长期做企业内部标准 CRUD 系统,可以直接用 MyBatis-Plus;你要是查复杂 SQL,觉得还是要精细控制,原生 MyBatis 更合适。我在多个项目中甚至见过原生 MyBatis 和 MyBatis-Plus 混用的情况:业务复杂查询用 XML,简单单表 CRUD 和安全能力用 Plus。做法是可行的,但依赖版本要梳理清楚。

3.4 MyBatis Flex 是什么?需要关注吗

提到了“mybatis flex两个or”,确实在当前社区里有一个叫 MyBatis-Flex 的框架,它也是一个 MyBatis 增强框架,功能和 MyBatis-Plus 有些重叠,不过它在某些 API 设计上更接近 Flex 风格。说句实话,如果你还没有在任何项目中稳定使用 MyBatis-Flex,我不推荐现在贸然引入核心链路。框架选择要稳定大于新颖。如果只是做个人项目或学习,可以关注它和 MyBatis-Plus 的 API 差异,但生产上你需要的是一套团队熟悉、文档充分、踩坑资料多的方案。

4. Mapper 之外的思考:监控、缓存和 SQL 日志

4.1 Spring Boot Actuator 和 MyBatis 能搭在一起做什么

看到热搜词里有一串“micrometer + spring boot actuator”,这个和 MyBatis 集成也有很强的关联。Spring Boot Actuator 是 Spring Boot 提供的运维监控端点。Micrometer 是它的监控指标门面。MyBatis 可以通过自定义拦截器和 Micrometer 结合,把 SQL 执行时间、慢查询次数、连接池状态暴露给监控系统。

我做过一个偏运维向的做法是增加一个 MyBatis 的Interceptor,拦截Executorqueryupdate方法,用 Micrometer 的Timer统计耗时,并把执行超过 2 秒的 SQL 通过日志系统告警。这个做法的价值在于,它不依赖具体业务代码,一个全局切面就能让你看到“哪条 SQL 平均耗时最高”。

@Component @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) public class SqlCostInterceptor implements Interceptor { // 这里可统计耗时,并记录 MappedStatement.getId() }

拦截器的原理是 MyBatis 提供的插件机制。理解它之前先想一个问题:MyBatis 是如何把 Mapper 接口和 XML 关联起来的?答案是 JDK 动态代理。MapperProxy会在接口方法调用时通过MapperMethod找到对应 SQL。而插件就可以被代理链路拦截,这是面试官经常会追问到的一层。

4.2 二级缓存不要默认开,一级缓存范围要清楚

MyBatis 缓存也是面试高频题。一级缓存是 SqlSession 级别,也就是说同一个 SqlSession 内,执行两条相同 SQL(前提是参数和查询条件一致)时,第二条可能直接命中缓存,不会再去查数据库。但在 Spring Boot + MyBatis 项目中,每次请求 Mapper 方法通常是重新获取 SqlSession,所以一级缓存的效果没有你想象中那么“可靠”。

二级缓存是 Mapper 级别,可以跨 SqlSession 共享。但如果你的项目同时被多个应用实例访问同一个数据库,二级缓存没有分布式方案时,缓存命中后可能读到脏数据。这种用本地缓存承载业务数据的方式,在大部分规模较小的后台系统里还能接受,在交易类场景中我不推荐开启。真要缓存,请优先用独立的 Redis,而不是让 MyBatis 二级缓存承担跨服务一致性任务。

4.3 SQL 日志打印与 IDAE 增强工具

排查 MyBatis 问题,第一步一定是看到底执行了什么 SQL,传了什么参数。最基础的方法是在application.yml里加:

logging: level: com.example.demo.mapper: debug

这个配置是 MyBatis 打印 SQL 最可靠的方式。记住要对准 Mapper 接口的包名,不是 XML 目录。开启后日志里会出现 Preparing、Parameters、Total 等信息。

IDEA 里那两个热搜词 “idea mybatis log free” 和 “mybatis log easyplus” 指的是插件工具。它们的本质是解析 MyBatis 跑出来的PreparingParameters日志,帮你拼出可直接执行的 SQL,省去手动把?替换成参数的过程。在开发环境确实很有用。但我个人不建议过度依赖插件拼出来的 SQL,因为序列化参数格式和数据库约束不一定一致,尤其是LocalDateTimejson类型字段。排查问题时还是以原生日志为准,插件可辅助快速复制出一条手写查询。

如果企业级项目中你用 P6spy 或 Druid 自身监控来打 SQL,也未尝不可。Druid 的 Web Stat Filter 可以把 SQL 执行明细展现在管理页面上,这种方式在做性能调优时很友好。

5. 高频面试点与源码层面的理解

5.1 为什么 MyBatis 的 Mapper 只要写接口,没有实现类也能调用

这个话题是 MyBatis 源码和面试题里的常客。从集成视角看,你在启动类加@MapperScan后,MyBatis 会扫描所有 Mapper 接口,然后通过MapperProxyFactory为每个接口生成 JDK 动态代理。真正调用接口方法时,实际执行逻辑在MapperProxy#invoke里。它构建了一个MapperMethod,从 configuration 中找到对应的MappedStatement,再委托给SqlSession去执行。

所以核心链路是:

  1. Mapper 接口 -> 动态代理
  2. 接口方法名 + namespace -> 定位 MappedStatement
  3. MappedStatement 里的 SQL -> 交给 Executor 执行
  4. Executor 底层通过 JDBC 操作数据库,处理结果映射

这个链路搞清楚后,什么“Invalid bound statement”“Mapper method returned null”基本都能自动推导出问题在哪个环节。

5.2 单参数还是多参数:别把小问题变成诡异的报错

很多报错的根源,其实是对参数包装规则不熟。举一个真实场景:Mapper 接口方法写的是:

User selectByNameAndAge(String name, Integer age);

XML 里写:

<select id="selectByNameAndAge" resultType="User"> select * from user where name = #{name} and age = #{age} </select>

这样的写法 99% 会报错,或者查不出结果。MyBatis 对多个参数会以param1, param2...或者arg0, arg1...来命名,所以 XML 里无法直接使用方法参数名。解决办法有几种:

  • 方法参数上加@Param("name")@Param("age"),XML 里用 name 和 age。
  • XML 里改用#{param1}#{param2}虽然能用,但可读性很差。
  • 在 Spring Boot 编译期开启-parameters参数后,部分版本可以识别原参数名,但为了兼容,我每次都建议加@Param

以前我给团队定过一个规则:任何超过一个参数的方法,必须加@Param;两个及其以上都算超过。这个规则至今没有给代码库带来额外负担,但显著降低了新手踩坑率。

5.3 面试题关键词里的 MyBatis 核心问题

标题附带了一系列高热度 MyBatis 面试题。我从里面挑几个容易张口就说错的点,结合我的实际理解总结一下:

  • MyBatis 是否支持延迟加载?支持。但使用关联查询时延迟加载只对嵌套子查询有效,对一条大 SQL 拼出的关联结果集无效。因此“全局开了懒加载,为何联表查询还是全部加载”这个问题,要先从 SQL 结构找原因。
  • #{}${}的区别?#{}是预编译占位符,能防 SQL 注入;${}是字符串拼接。动态表名、排序字段没法用#{},这也是使用${}的少数合法场景。只要涉及用户输入且用了${},必须做白名单校验。
  • 一级缓存失效的场景有哪些?跨 SqlSession、SqlSession 中没有执行相同 SQL、两次查询之间执行过增删改、手动清空缓存等。
  • Mapper 中定义的方法能重载吗?接口方法重载会带来奇怪的绑定问题,不是所有版本都支持,所以不建议在 Mapper 接口里重载方法。

面试中还有个“MyBatis 工作原理”的基础题。完整链路可以概括为 SqlSessionFactoryBuilder 读取 mybatis-config.xml 或 Spring 配置,构建 Configuration 和 MapperRegistry,再通过 SqlSessionFactory 创建 SqlSession。Mapper 接口的每个方法都由动态代理映射到对应 SQL,Executor 负责执行 JDBC,StatementHandler 处理参数,ResultSetHandler 处理结果集。这些东西看似八股,但遇到线上 SQL 执行异常时,脑子里能把这套链路过一遍,定位问题会快很多。

5.4 MyBatis 和 MyBatis-Plus 区别

这个问题也是面试常客,我并不觉得它是一个“二选一”的优劣题,更像是一个职责边界题。MyBatis 本身是一个持久层框架,定位在数据库操作,它把 SQL 和 Java 方法绑定,开发灵活但没有给你封装好单表 CRUD。MyBatis-Plus 是站在 MyBatis 肩膀上的增强工具,它把常见的单表增删改查、分页查询、乐观锁、逻辑删除、代码生成器都封装好了。使用 MyBatis-Plus 时,你仍然可以在 XML 中写复杂自定义 SQL,两者不冲突。区别主要体现在写单表 CRUD 时的工作量、扩展功能的接入成本和学习门槛上。

6. 常见问题与排查技巧实录

6.1 “write operations are not allowed in read-only mode” 处理记录

热搜词里那条 “mybatis报错write operations are not allowed in read-only mode (flushmode.man” 是属于只读事务模式下执行写操作的典型报错。出现这个问题的原因大多在三层之一:

  • 你的服务方法标注了@Transactional(readOnly = true),但在方法里执行了 insert、update 或 delete。
  • Spring Data 或其他框架把当前连接设置成了只读,数据源路由又指向了只读从库。
  • MyBatis 通过 Spring ManagedTransaction 拿到连接时,自动根据事务定义设置只读。

排查时先顺着调用链找事务注解,注意看类级别有没有@Transactional(readOnly = true),如果继承了一个基类,要去基类方法看。另外如果你在多数据源场景中,检查事务管理器是否正确绑定了写库。只用只读库跑写操作,显然会出现这个错。

6.2 MyBatis 报 “Invalid bound statement (not found)” 的完整排查路径

这是我见过最多的报错。它一般表示运行时通过 Mapper 接口完全找不到 XML 或注解 SQL。排查顺序可以是这样:

  1. 检查 Mapper 接口方法名与 XML 中<select>/<update>的 id 是否一致。
  2. 检查 XML mapper 的 namespace 是否是接口全限定名。
  3. 检查编译后的 target/classes 是否存在 XML 文件。Mapper XML 在 src/main/java 目录里容易被打包遗漏,我建议放在 src/main/resources/mapper 下。
  4. 检查mybatis.mapper-locations配置是否匹配实际目录。
  5. 检查启动类上的@MapperScan路径是否真的覆盖了 Mapper 接口所在包。
  6. 如果接口和 XML 都正常,但 Service 里注入后调用报错,排查是不是代理对象被 AOP 拦截后类型转换出问题。

6.3 MyBatis 多数据源问题的避坑经验

“mybatis的saveorupdatebatch多数据源的问题”这类报错,多见于业务里同时连了多个库,团队里大概率使用了 AbstractRoutingDataSource 或类似数据源路由方式。多数据源场景下核心要关注的不是 MyBatis 本身,而是你的事务管理器。你把多个数据源都交给同一个 DataSourceTransactionManager 管理,它只会绑定一个连接,这样就导致你路由到库 A 执行后再切到库 B 时,事务已经拿错连接了。

我的建议是多数据源项目先确定主事务边界,尽量不要在一个事务里跨多个数据源做强一致写操作。如果有强一致需求,优先考虑用统一事务方案;如果不允许引入分布式事务,很多业务场景下可以改成“先后写不同库 + 本地消息表/对账”的方式来实现最终一致。这个思路比追求“一个注解搞定分布式事务”要稳妥得多。

同时,如果使用 MyBatis-Plus 的saveOrUpdateBatch,底层其实是逐条判断存在后执行 insert 或 update,那么对接多数据源时请确认每条操作的 SqlSession 来自正确的目标数据源。否则非常容易出现某个更新执行到了只读库或历史库的情况。排查时可以在每批次前打印当前数据源 key。

6.4 启动报错的快速检查表

下面这个表格整理了我见过的高频启动报错和排查方向,你可以直接收藏当速查表:

报错关键字可能原因排查方向
Invalid bound statementXML 与接口无法绑定namespace、id、mapper-locations
Failed to configure a DataSource没有数据源配置或自动配置失败检查 datasource 配置、驱动坐标
Table doesn't exist建表脚本未执行或库选错检查 URL 中的库名
ClassNotFoundException依赖缺失检查 MySQL 驱动或其他依赖
Property 'sqlSessionFactory' or 'sqlSessionTemplate' required多数据源配置时缺少 SqlSessionFactory配置 MyBatis 多数据源时显式注册
write operations are not allowed in read-only mode只读事务里出现写操作查事务注解、数据源路由

6.5 开发期常见的小工具和扩展

开发过程中很多人喜欢用 MyBatis 插件自动生成代码。这类插件的价值不是“生成出来就能直接用”,而是它能减少写基础 CRUD 的时间。但团队里要约定好生成后的规则,比如统一删除生成器生成的多余注释、统一主键策略、统一逻辑删除字段。如果不做二次规范,生成的实体类里一堆@TableField注解,反而降低可读性。

另外,如果你手头维护的是老项目,里面有大量 XML 文件,建议用 XML 格式化工具统一格式。虽然 MyBatis 对空行和缩进不敏感,但多人协作时如果没有格式化规范,代码评审的效率会很差。这里的小技巧是,在 IDEA 里把 XML 文件 code style 调整成2空格缩进,然后在提交前统一格式化,至少能减少一半无意义的 diff。

7. 最后说点个人经验

从开始接触 Spring Boot 2 整合 MyBatis 到现在,我最大的体感是:MyBatis 的集成文档并不复杂,真正决定项目质量的往往是工程纪律。比如 SQL 放 XML 还是注解,团队里最好定一个原则。我个人推荐所有动态 SQL、复杂查询都放 XML,单表简单查询可以用注解。之前见过一个项目把十几行动态 SQL 写在注解里,@Select 上加一堆<script>,维护体验真的差到不行。

另外,我给新项目的默认建议是:连接池选 Druid(要监控),分页如果量不大用手写 limit 或者 MyBatis-Plus;MyBatis 全局配置里map-underscore-to-camel-case必须开,log-impl如果不是生产环境就设成 StdOutImpl,方便联调;生产环境的日志级别记得调成 warn 以上,避免把参数打印到线上日志。

还有一个细节容易被忽略:写 SQL 时别为了“看起来简洁”而省略列名,尽量不要用select *。MyBatis 做结果映射时,如果某个字段查出来了但没有对应属性,虽然不会直接报错但会浪费内存;反过来,如果数据库表加了新字段,而实体类没有同步,也会在 map-underscore 开启后出现“查了但映射不了”的隐性空值问题。项目上线久了,这种隐藏问题比显式报错更难查。

如果你正在搭建一个新项目,我的建议是先花小半天把“依赖版本、配置参数、日志链路、分页方式、异常排查”这几件事打成团队共识,再去写业务代码。这样后面几十个 Mapper 的开发都是顺水推舟,不会一天到晚在启动报错和数据源路由里打转。集成 MyBatis 本身不是目的,让人能在长期维护中不迷路,才是这件事真正的意义。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询