
这几年做 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.xjavax 包名也要改成 jakarta这个版本问题在集成时很关键先提个醒。1. 集成前的整体规划选型与版本匹配1.1 先想清楚为什么选 MyBatis而不是 JPA 或 JDBC很多初学者一上来就搜“Spring Boot 怎么集成 MyBatis”然后照着教程把依赖复制进去能跑通就觉得完成了。但实际上选型阶段的认知会直接决定后面几个月的开发效率。MyBatis 核心价值是“SQL 由开发者掌控”它不会替你去生成那些你不想要的 SQL也不会在 N1 查询这种问题上隐藏成本。对复杂查询、报表统计、多表关联这类场景写 SQL 反而比 JPA 的实体关系映射更直接。当然JPA 在上手速度和对象建模上有优势这一点不否认。但在生产环境中我见过不少项目因为 JPA 的懒加载和缓存问题不得不天天查 SQL 日志。既然标题是 MyBatis 集成我就不展开 JPA 了。你需要知道的是如果你团队里能写好 SQL 的人占多数MyBatis 大概率是更可控的选择。1.2 版本匹配是集成第一步但不是看了版本号就完事Spring Boot 集成 MyBatis 最怕的不是写错 Mapper而是依赖冲突和版本不匹配。MyBatis 官方提供了mybatis-spring-boot-starter它会自动引入mybatis和mybatis-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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 里我建议显式带上serverTimezoneAsia/Shanghai否则连 MySQL 8 有时候会报时间区错误。第二类配置是 Mapper 相关。mapper-locations用于指定 XML 文件位置这里的写法是classpath:mapper/*.xml表示把编译后的mapper目录下所有 XML 都加载进来。type-aliases-package可以简写 Mapper XML 里的resultType比如resultTypeUser会自动匹配到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); ListUser selectPage(Param(offset) int offset, Param(limit) int limit); }在启动类上要先加MapperScan(com.example.demo.mapper)这个注解负责扫描 Mapper 接口并生成动态代理。很多新手的“Mapper 无法注入”问题八成是没扫描到或路径写错。如果不想用MapperScan也可以在每个 Mapper 接口上加Mapper但比较繁琐我一般只用MapperScan方式。Mapper XML 文件可以这么写?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectById resultTypeUser select id, name, age, email from user where id #{id} /select select idselectPage resultTypeUser 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.activedev生产常用第二种。不过要注意的是用命令行跑时最容易出现的问题就是appliocation.yml或mapper/*.xml没有被正确打进 jar。检查方式很简单jar tf target/xxx.jar | grep mapper如果 XML 没在 jar 里大概率是构建插件把 resources 目录过滤掉了。解决办法是在pom.xml的buildresources中显式声明保留 xml。3. 深入核心细节动态 SQL、分页和逻辑删除3.1 动态 SQL 标签别死记硬背先理解场景MyBatis 最核心的亮点是动态 SQL也是面试和工作中少不了的考察点。if、choose、when、otherwise、trim、where、set、foreach这些标签各有用途但如果你不理解场景仅仅背标签没有任何意义。最常见的场景是前端传了一堆筛选条件后端要做多条件查询。你可能会先想出这样的 SQLselect idlistByCondition resultTypeUser select * from user where 1 1 if testname ! null and name ! and name like concat(%, #{name}, %) /if if testage ! null and age #{age} /if /selectwhere 1 1是老式写法能用但不太好看。MyBatis 提供了where标签它能自动去除多余的and或orselect idlistByCondition resultTypeUser select * from user where if testname ! null and name ! and name like concat(%, #{name}, %) /if if testage ! null and age #{age} /if /where /selectforeach则适用于in查询select idlistByIds resultTypeUser select * from user where id in foreach collectionids itemid open( separator, close) #{id} /foreach /select这里有一个我一直强调的小细节collection的值要和接口参数对应。如果方法签名是ListUser listByIds(Param(ids) ListLong ids)collection 就写ids如果没有任何Param直接传一个 ListMyBatis 会自动包装成list这时 collection 要写成list。3.2 分页查询不用傻傻手写 limit分页插件和手写如何选标题里的热搜词“mybatis查询增加行号”很贴近真实开发需求。在 MySQL 中你可以用row_number()开窗函数也可以用自定义变量。比如select idlistWithRowNo resultTypemap 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之类的条件这在绝大多数业务下是正确的。但有时候你需要去“包括已删除数据”的历史表里捞数据比如做数据对账、运营后台看全量记录这时候自动拼接的deleted0反而成了障碍。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拦截Executor的query和update方法用 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 跑出来的Preparing和Parameters日志帮你拼出可直接执行的 SQL省去手动把?替换成参数的过程。在开发环境确实很有用。但我个人不建议过度依赖插件拼出来的 SQL因为序列化参数格式和数据库约束不一定一致尤其是LocalDateTime、json类型字段。排查问题时还是以原生日志为准插件可辅助快速复制出一条手写查询。如果企业级项目中你用 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去执行。所以核心链路是Mapper 接口 - 动态代理接口方法名 namespace - 定位 MappedStatementMappedStatement 里的 SQL - 交给 Executor 执行Executor 底层通过 JDBC 操作数据库处理结果映射这个链路搞清楚后什么“Invalid bound statement”“Mapper method returned null”基本都能自动推导出问题在哪个环节。5.2 单参数还是多参数别把小问题变成诡异的报错很多报错的根源其实是对参数包装规则不熟。举一个真实场景Mapper 接口方法写的是User selectByNameAndAge(String name, Integer age);XML 里写select idselectByNameAndAge resultTypeUser 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 接口的每个方法都由动态代理映射到对应 SQLExecutor 负责执行 JDBCStatementHandler 处理参数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。排查顺序可以是这样检查 Mapper 接口方法名与 XML 中select/update的 id 是否一致。检查 XML mapper 的 namespace 是否是接口全限定名。检查编译后的 target/classes 是否存在 XML 文件。Mapper XML 在 src/main/java 目录里容易被打包遗漏我建议放在 src/main/resources/mapper 下。检查mybatis.mapper-locations配置是否匹配实际目录。检查启动类上的MapperScan路径是否真的覆盖了 Mapper 接口所在包。如果接口和 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-locationsFailed to configure a DataSource没有数据源配置或自动配置失败检查 datasource 配置、驱动坐标Table doesnt 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-PlusMyBatis 全局配置里map-underscore-to-camel-case必须开log-impl如果不是生产环境就设成 StdOutImpl方便联调生产环境的日志级别记得调成 warn 以上避免把参数打印到线上日志。还有一个细节容易被忽略写 SQL 时别为了“看起来简洁”而省略列名尽量不要用select *。MyBatis 做结果映射时如果某个字段查出来了但没有对应属性虽然不会直接报错但会浪费内存反过来如果数据库表加了新字段而实体类没有同步也会在 map-underscore 开启后出现“查了但映射不了”的隐性空值问题。项目上线久了这种隐藏问题比显式报错更难查。如果你正在搭建一个新项目我的建议是先花小半天把“依赖版本、配置参数、日志链路、分页方式、异常排查”这几件事打成团队共识再去写业务代码。这样后面几十个 Mapper 的开发都是顺水推舟不会一天到晚在启动报错和数据源路由里打转。集成 MyBatis 本身不是目的让人能在长期维护中不迷路才是这件事真正的意义。