MyBatis实战进阶:从核心原理到高频问题解决方案

发布时间:2026/8/28 14:13:42
MyBatis实战进阶:从核心原理到高频问题解决方案 1. 项目概述MyBatis的“奇巧yin技”到底是什么如果你用过MyBatis不管是原生的还是整合了MyBatis-Plus大概率都经历过一些“诡异”的时刻。比如明明代码逻辑看着没问题但SQL就是没执行或者分页查询时想查全部数据却不知道那个神秘的pageSize参数到底该设成-1还是0又或者你兴冲冲地写了个批量更新日志里也显示执行了可数据库里愣是一条记录都没变。这些场景往往不是MyBatis的Bug而是你对它那些“潜规则”和“隐藏特性”了解不够深入。所谓“奇巧yin技”指的就是这些官方文档不会重点强调但在实际开发中能极大提升效率、规避深坑的实战技巧与深度原理理解。这不仅仅是几个配置项那么简单。它关乎你对MyBatis整个执行链路、参数映射、SQL生成、缓存机制乃至与Spring整合后生命周期管理的透彻理解。很多人用MyBatis好几年可能还停留在“写Mapper接口和XML”的层面一旦遇到复杂动态SQL、性能问题或诡异的行为排查起来就异常吃力。接下来我会结合我踩过的无数个坑把这些分散的、隐晦的“奇技”系统地梳理出来让你不仅能解决问题更能理解问题背后的“为什么”。2. 核心原理与工作机制深度拆解要玩转MyBatis的“奇巧yin技”第一步必须是理解它的“五脏六腑”。很多人觉得会用就行但当你需要定制化、优化或排查问题时原理就是你的导航图。2.1 SQL执行的核心链路从接口方法到JDBC Statement当你调用userMapper.selectById(1)时背后发生的故事远比想象中复杂。这不是一个简单的代理调用而是一个精密的流水线作业。首先MyBatis通过动态代理为你的Mapper接口生成了一个代理对象通常是MapperProxy。当你调用方法时拦截器会开始工作。核心环节在于MapperMethod它像一个调度中心根据方法名和签名决定本次执行是SELECT、UPDATE、INSERT还是DELETE。接下来重中之重是SqlSession。你可以把它理解为一个数据库会话的上下文它持有Executor执行器。Executor才是真正的苦力它负责调度。这里有个关键点默认的SimpleExecutor会对每条SQL都创建一个新的PreparedStatement执行完就关闭。而ReuseExecutor则会复用同一个PreparedStatement对象。对于短时间内大量执行相同SQL模板仅参数不同的场景比如循环插入使用ReuseExecutor能显著减少数据库创建Statement的开销。你可以在全局配置中通过defaultExecutorType来指定。settings !-- 可选值SIMPLE, REUSE, BATCH -- setting namedefaultExecutorType valueREUSE/ /settingsExecutor会调用StatementHandler来创建具体的Statement对象比如PreparedStatementHandler。在这之前ParameterHandler会出场它的任务是把你的Java方法参数可能是一个Map、一个POJO或多个Param注解的参数“翻译”成SQL语句中的?占位符对应的值。这个“翻译”过程就是参数映射也是很多坑的来源。最后ResultSetHandler负责将JDBC返回的ResultSet转换成你方法声明的返回类型无论是单个对象、List还是Map。整个链路中各个环节都有插件Interceptor可以介入这也就是MyBatis插件如分页插件能够工作的基础。理解这个链路你就能明白为什么打印SQL日志的插件要挂在StatementHandler上也能明白批量操作时为什么必须用BATCH执行器。2.2 参数映射的“黑魔法”与OGNL表达式MyBatis参数映射的强大与“诡异”并存。它底层使用了OGNLObject-Graph Navigation Language表达式来从参数对象中取值。场景一当参数是单个基本类型或StringUser selectById(Integer id);在XML中你可以直接用#{id}也可以随便用其他名字如#{value}但${}方式必须用${value}。这是因为MyBatis在处理单个简单类型参数时会将其特殊处理为一个名为_parameter的对象同时也会把它赋值给一个叫value的键在3.4.1版本后也可以用_parameter。使用${}进行字符串替换时它严格依赖于这个内部命名。场景二使用Param注解明确命名User selectByCondition(Param(“userId”) Integer id, Param(“userName”) String name);这是最清晰、最推荐的方式。在XML中你可以直接使用#{userId}和#{userName}。MyBatis会将这些参数封装到一个Map中键就是你注解里写的名字。场景三参数是一个POJO对象int updateUser(User user);在XML中你可以直接使用POJO的属性名如#{name},#{age}。MyBatis会通过OGNL表达式user.getName()来获取值。这里就引出一个高级技巧嵌套属性访问。如果User对象里有一个Dept类型的dept属性你可以直接用#{dept.deptName}来映射MyBatis会帮你调用user.getDept().getDeptName()。场景四参数是Mapint updateByMap(MapString, Object params);在XML中直接使用Map的Key即可如#{userName}。这是最灵活但也最容易出问题的方式因为Key是字符串拼写错误在编译期无法发现。避坑指南强烈建议在复杂查询或多参数场景下使用Param注解。它能让你的意图更清晰避免因MyBatis版本或参数类型变化导致的映射错误。对于POJO参数确保属性有标准的getter方法否则OGNL无法正确取值。2.3 结果映射的灵活性与性能陷阱resultMap是MyBatis的灵魂之一它定义了如何将数据库列column映射到Java对象属性property。高级映射技巧关联查询association与集合查询collection这是处理一对一、一对多关系的利器。但这里有个巨大的性能陷阱N1查询问题。如果你在resultMap里配置了一个collection并且它的select属性指向另一个查询嵌套Select那么当主查询返回N条数据时会对这个collection额外执行N次查询。对于数据量大的场景这是灾难性的。!-- 可能导致N1问题的写法 -- resultMap id“blogResult” type“Blog” collection property“posts” javaType“ArrayList” ofType“Post” column“id” select“selectPostsForBlog”/ /resultMap解决方案优先使用连接查询JOIN然后在resultMap中通过association/collection进行结果集映射无需select属性。虽然结果集会有冗余数据但一次查询搞定性能远胜N1。自动映射与autoMappingBehaviorMyBatis默认会尝试自动映射未在resultMap中明确定义的列列名与属性名忽略大小写匹配。你可以通过全局设置autoMappingBehavior来控制PARTIAL默认只自动映射没有定义嵌套结果映射的属性。FULL自动映射所有属性无论是否嵌套。NONE禁用自动映射。 我的经验是在复杂的、涉及多表关联的查询中建议设置为NONE或PARTIAL并显式定义resultMap避免因列名冲突导致意外映射。构造器映射constructor如果你的实体类使用带参数的构造器并且想通过构造器注入而非setter方法可以使用constructor标签。这在某些不可变类Immutable Class的场景下很有用。3. 实战“奇技”与高频问题破解理解了原理我们来看实战中那些最常遇到、也最容易让人头疼的问题和对应的“巧技”。3.1 SQL日志打印的“终极方案”排查问题第一步看执行的SQL到底是什么。网上很多方案但都不够完美。标准配置法最常用在application.yml或mybatis-config.xml中配置。# application.yml mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出到控制台 # log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 输出到SLF4J同时还需要在logback-spring.xml等日志配置文件中将你的Mapper接口所在包或org.apache.ibatis的日志级别设为DEBUG。logger name“com.yourpackage.mapper” level“DEBUG”/缺点日志输出格式可能不友好参数是?替换的看不到实际值。第三方插件法推荐使用p6spy或mybatis-log-pluginIDEA插件。p6spy它是一个数据库驱动代理能拦截所有JDBC操作打印出完整的、带真实参数的SQL语句。集成后你需要将数据源驱动从com.mysql.cj.jdbc.Driver改为com.p6spy.engine.spy.P6SpyDriver并配置spy.properties。输出格式清晰是线上问题排查的利器。MyBatis Log Plugin这是IDEA的一款免费插件。安装后在控制台会有一个“MyBatis Log”的标签页。它能在你运行程序时自动检测并格式化MyBatis执行的SQL将?替换为真实值无需任何代码和配置改动开发调试极其方便。自定义拦截器法最灵活自己实现一个MyBatis的Interceptor拦截StatementHandler的prepare或parameterize方法从中获取BoundSql对象就能拿到完整的SQL和参数。你可以定制输出格式甚至将SQL发送到监控系统。这是最强大的方式但需要一定的开发成本。实操心得日常开发强烈推荐MyBatis Log Plugin无侵入、即装即用。线上环境或需要持久化SQL日志时使用p6spy。自定义拦截器适用于有特殊监控或审计需求的团队。3.2 分页查询的“正确姿势”与深坑分页是高频操作坑也最多。坑1如何“全查”PageHelper设置pageSize-1这是一个流传甚广的误解。在PageHelper国内最常用的分页插件中设置pageSize-1或pageSize0在某些旧版本中确实会被解释为“查询全部”。但这并非官方标准行为且依赖版本非常不可靠。正确做法如果你需要分页查询和全量查询两种模式应该在Service层进行逻辑判断。public ListUser getUsers(PageParam param) { if (param ! null param.getPageNum() ! null param.getPageSize() ! null) { // 执行分页 PageHelper.startPage(param.getPageNum(), param.getPageSize()); return userMapper.selectAll(); } else { // 执行全量查询确保不调用PageHelper.startPage return userMapper.selectAll(); } }核心原则只有在调用PageHelper.startPage()之后的第一条Mapper查询才会被分页。只要不调用它就是全量查询。坑2大数据量分页的性能问题使用limit offset, size进行深分页如limit 100000, 20时MySQL需要先扫描前100000条记录然后扔掉再取20条性能极差。优化方案方案一基于主键或有序索引的“上一页/下一页”查询。适用于无限滚动场景。记录上一页最后一条记录的ID然后查询where id lastId limit 20。但这不支持跳转到任意页码。方案二子查询优化。先通过子查询获取起始位置的主键ID再用主查询通过ID范围快速定位。SELECT * FROM your_table t1 JOIN (SELECT id FROM your_table ORDER BY create_time DESC LIMIT 100000, 20) t2 ON t1.id t2.id ORDER BY t1.create_time DESC;方案三MyBatis-Plus的optimizeCountSql。如果使用MP开启此配置默认已开启它会在执行count查询时进行优化移除不必要的order by和join对于复杂SQL的count查询有提升。3.3 批量操作的性能优化秘籍批量插入或更新是提升性能的关键但用不对反而会出问题。1. 批量插入Batch Insert原生MyBatisExecutorType.BATCH 在获取SqlSession时指定执行器类型为BATCH。注意BATCH模式不会在每次insert方法执行时立即提交到数据库而是将SQL预编译并加入批处理队列。你需要在最后调用sqlSession.commit()才会一次性执行。此外BATCH模式下无法获取自动生成的主键useGeneratedKeys会失效这是一个很大的限制。SqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper sqlSession.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } sqlSession.commit(); // 一次性提交 } finally { sqlSession.close(); }MyBatis-Plus的saveBatchMP的Service层提供的saveBatch方法默认并不是真正的JDBC批处理。它本质上是循环单条插入。要启用真正的批处理需要做两件事在配置文件中开启批处理模式mybatis-plus.global-config.db-config.insert-strategybatched此配置名可能随版本变化请以官方文档为准。在数据源配置中JDBC URL需要加上重写批处理语句的参数rewriteBatchedStatementstrueMySQL驱动参数。这个参数至关重要它会让MySQL驱动将多条插入语句重写为一条多值插入的SQL性能提升数十倍。2. 批量更新Batch Update批量更新比插入更复杂因为每条更新的SQL可能不同SET的值不同。原生foreach标签在XML中使用foreach标签拼接成一条包含多个WHEN...THEN...的CASE语句的SQL。这是最高效的方式但SQL会很长。update id“updateBatch” UPDATE user SET name CASE id foreach collection“list” item“item” WHEN #{item.id} THEN #{item.name} /foreach END, age CASE id foreach collection“list” item“item” WHEN #{item.id} THEN #{item.age} /foreach END WHERE id IN foreach collection“list” item“item” open“(” separator“,” close“)” #{item.id} /foreach /updateExecutorType.BATCH同样可以使用BATCH执行器。将多条update语句加入批处理然后一次性提交。性能尚可但不如单条复杂SQL高效。同样需要注意在BATCH模式下返回的int是-2BatchExecutor.BATCH_UPDATE_RETURN_VALUE不代表实际更新行数。重要提醒无论是批量插入还是更新当数据量巨大例如超过10万条时都不建议一次性操作。应该进行分片比如每1000条提交一次避免事务过大、数据库连接超时、内存溢出OOM等问题。3.4 动态SQL的进阶写法与“空值”陷阱MyBatis的动态SQL标签if,choose,trim,foreach等非常强大但使用不当会导致SQL错误或性能问题。陷阱if标签判断空字符串if test“username ! null and username ! ‘’“ AND username #{username} /if注意在OGNL表达式中判断空字符串是! ‘’两个单引号。很多人写成! “”双引号在某些版本下也能工作但单引号是更标准的写法。高级技巧trim、set、where的妙用where标签会智能地处理WHERE关键字。如果标签内无内容则不会生成WHERE如果内容以AND或OR开头会自动将其去除。set标签用于UPDATE语句会智能地处理SET关键字并去除末尾多余的逗号。trim标签功能最强大可以自定义前缀、后缀以及要覆盖去除的前缀/后缀字符串。可以用来实现非常复杂的动态片段。trim prefix“WHERE” prefixOverrides“AND |OR “ if test“id ! null”AND id #{id}/if if test“name ! null”AND name like #{name}/if /trim“空值”更新问题MyBatis-Plus的updateById不更新null字段这是MP的一个特性而非Bug。MP的updateById方法默认使用了“空值忽略”策略。即当你传入的实体对象中某个字段为null时MP认为你不想更新这个字段因此在生成的UPDATE语句中会排除该字段。解决方案使用UpdateWrapper通过UpdateWrapper可以明确指定要更新的字段和值即使值为null。UpdateWrapperUser updateWrapper new UpdateWrapper(); updateWrapper.eq(“id”, user.getId()) .set(“email”, null); // 明确将email字段更新为null userMapper.update(null, updateWrapper);修改全局策略不推荐在MP配置中可以设置field-strategy为ignored这样所有null值都会参与更新。但这可能导致你无意中将字段更新为null破坏数据风险很高。在字段上使用TableField注解TableField(updateStrategy FieldStrategy.IGNORED)仅针对特定字段忽略更新策略。3.5 缓存机制与“脏数据”防范MyBatis有一级缓存和二级缓存。一级缓存是SqlSession级别的默认开启。二级缓存是Mapper级别的需要手动配置开启。一级缓存本地缓存的坑 在同一个SqlSession中执行两次相同的查询第二次会直接从缓存返回结果不会访问数据库。这听起来不错但在Web应用中如果SqlSession的生命周期过长比如和请求绑定可能导致读到“脏数据”。例如你在一个事务中先查询了用户A然后另一个方法更新了用户A并提交你又在同一个SqlSession里查询用户A拿到的还是旧数据。规避方法在查询方法上添加flushCache“true”选项强制清空缓存。或者更根本的方法是确保在Web环境中如整合Spring后SqlSession的生命周期与每个数据库操作绑定即每次操作都创建新的SqlSessionSpring的SqlSessionTemplate就是这么做的它实际上禁用了一级缓存的效果。二级缓存Mapper缓存的使用与风险 二级缓存是跨SqlSession的数据存储在更广的范围内如应用进程内。启用需在XML中配置cache/。风险1脏读。多个SqlSession操作同一数据一个更新了另一个可能还读到缓存中的旧值。风险2序列化。缓存的对象需要实现Serializable接口。如果缓存了复杂的、关联了其他未序列化对象的POJO会导致错误。使用建议对于读远多于写、且数据实时性要求不高的配置表、字典表可以考虑开启二级缓存。对于核心业务数据、频繁更新的数据强烈建议关闭二级缓存。在分布式环境下默认的PerpetualCache本地内存缓存会导致各节点数据不一致需要集成Redis等分布式缓存配置复杂。4. 与Spring Boot集成的深度配置与优化现在大部分项目都是Spring Boot MyBatis/MyBatis-Plus集成时的配置细节决定了应用的稳定性和性能。4.1 多数据源与动态数据源配置中大型项目常需要连接多个数据库。配置多数据源的核心是创建多个DataSourceBean并配合Primary注解指定默认数据源。然后你需要自定义一个AbstractRoutingDataSource它根据当前线程上下文如通过ThreadLocal存储的key来动态路由到具体的DataSource。一个常见的做法是使用AOP在Service层方法上根据注解如DS(“slave”)来切换数据源。MyBatis-Plus提供了开箱即用的DS注解和动态数据源组件大大简化了这项工作。但需要注意事务管理的问题在带有Transactional的方法内部切换数据源可能会失效因为事务管理器通常在方法开始时就已经确定了Connection。因此数据源切换的注解最好放在Service类或方法上并且要早于Transactional生效可以通过AOP的Order注解调整切面顺序。4.2 事务管理与Transactional的注意事项Spring的事务管理和MyBatis的SqlSession生命周期紧密相关。默认情况下Spring的Transactional注解会在方法开始时开启一个事务并绑定一个SqlSession到当前线程。方法内的所有MyBatis操作都使用这个SqlSession方法结束时根据是否异常决定提交或回滚。关键点事务失效场景在同一个类中一个非事务方法A调用同一个类中的事务方法BB的事务会失效。这是因为Spring的AOP代理机制导致的。解决方法是将方法B放到另一个Bean中或使用AopContext.currentProxy()。只读事务对于纯查询方法使用Transactional(readOnly true)。这会给数据库一个提示数据库可能会做一些优化如启用只读模式。同时MyBatis结合某些数据源如HikariCP时只读事务可能会被路由到从库如果配置了读写分离。事务传播行为理解PROPAGATION_REQUIRED默认有则加入无则新建、PROPAGATION_REQUIRES_NEW新建事务挂起当前事务等传播行为的区别是处理复杂业务逻辑的基础。4.3 配置文件application.yml的“灵魂”参数除了常见的mapper-locations和type-aliases-package下面这些配置值得关注mybatis-plus: configuration: # 是否开启驼峰命名自动映射。数据库字段 user_name 自动映射到实体属性 userName。默认true建议保持。 map-underscore-to-camel-case: true # 默认的执行器类型。SIMPLE, REUSE, BATCH。根据业务场景调整。 default-executor-type: simple # 日志实现。StdOutImpl直接打印到控制台比较吵。通常用Slf4jImpl再配合日志框架控制输出。 log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl # 缓存配置。本地缓存一级缓存作用域。SESSION默认是SqlSession级别STATEMENT是语句级别相当于关闭一级缓存。 local-cache-scope: session # 当结果集中有为空的列时是否调用 setter 方法。默认false不调用。设为true可以用于触发某些字段的初始化逻辑。 call-setters-on-nulls: false # 配置JdbcTypeForNull。当参数为null时默认的JDBC类型是OTHER。某些数据库如Oracle可能需要指定为NULL。 jdbc-type-for-null: null global-config: db-config: # 逻辑删除配置如果使用 logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 # 插入时主键生成策略。ASSIGN_ID雪花算法 ASSIGN_UUID INPUT手动输入等。 id-type: ASSIGN_ID5. 高级特性与插件开发入门当你需要定制化MyBatis的行为时插件Interceptor是终极武器。分页插件、性能分析插件、SQL打印插件都是基于此实现的。5.1 自定义插件开发步骤MyBatis插件通过实现org.apache.ibatis.plugin.Interceptor接口来编写。它可以拦截四大核心对象的方法Executor,ParameterHandler,ResultSetHandler,StatementHandler。开发一个简单的SQL执行时间监控插件实现Interceptor接口Intercepts({ Signature(type StatementHandler.class, method “query”, args {Statement.class, ResultHandler.class}), Signature(type StatementHandler.class, method “update”, args {Statement.class}) }) Component public class SqlCostInterceptor implements Interceptor { private static final Logger log LoggerFactory.getLogger(SqlCostInterceptor.class); Override public Object intercept(Invocation invocation) throws Throwable { long startTime System.currentTimeMillis(); Object result invocation.proceed(); // 执行原方法 long endTime System.currentTimeMillis(); long cost endTime - startTime; StatementHandler statementHandler (StatementHandler) invocation.getTarget(); BoundSql boundSql statementHandler.getBoundSql(); String sql boundSql.getSql(); // 简化SQL便于日志查看 sql sql.replaceAll(“\\s“, “ “); log.warn(“SQL执行耗时[{}ms] - {}“, cost, sql); // 可以在这里添加慢SQL告警逻辑比如cost 1000ms时发送通知 if (cost 1000) { log.error(“慢SQL detected: {}“, sql); } return result; } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { // 可以接收配置文件中的参数 } }使用Intercepts和Signature注解指定你要拦截哪个类的哪个方法。args属性指定方法参数类型用于精确匹配重载方法。获取SQL信息通过StatementHandler.getBoundSql()可以拿到BoundSql对象里面包含了最终的SQL语句和参数。注册插件如果使用Spring Boot像上面一样加上Component注解即可自动注册。也可以通过在MybatisConfiguration中创建Bean的方式注册。5.2 类型处理器TypeHandler的妙用TypeHandler用于处理Java类型和JDBC类型之间的转换。常见的用途有枚举类型处理默认情况下MyBatis将枚举按字符串枚举名存储。如果你想按枚举的code整数存储可以自定义TypeHandler。复杂对象存储为JSON字符串将实体类中的一个MapString, Object属性在存入数据库时自动转为JSON字符串查询时再自动转回Map。自定义加密/解密在数据进出数据库时对敏感字段如手机号、身份证号进行加解密。实现一个将ListString存储为逗号分隔字符串的TypeHandler示例MappedJdbcTypes(JdbcType.VARCHAR) MappedTypes(List.class) public class StringListTypeHandler extends BaseTypeHandlerListString { Override public void setNonNullParameter(PreparedStatement ps, int i, ListString parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, String.join(“,”, parameter)); } Override public ListString getNullableResult(ResultSet rs, String columnName) throws SQLException { String value rs.getString(columnName); return value null ? null : Arrays.asList(value.split(“,”)); } // 另外两个getNullableResult重载方法也需要实现... }然后在Mapper XML中或者直接在配置文件中注册这个TypeHandler并在resultMap或参数中指定typeHandler属性即可使用。6. 生产环境问题排查与性能调优最后分享一些线上环境排查MyBatis相关问题的思路。问题一查询结果List的size()为1但里面元素是null这种情况通常发生在你的查询预期返回多条记录但实际只匹配到一条并且MyBatis无法将这一条记录正确映射到你的结果对象。例如你的resultMap配置有误或者数据库返回的列名与resultMap中的property不匹配。MyBatis在构建结果列表时会先创建一个List然后尝试为每一行数据创建一个结果对象并放入列表。如果创建对象失败比如构造器异常、映射失败它可能会放一个null进去。所以列表大小是结果集的行数但元素是null。解决方案检查SQL在数据库执行的真实结果并仔细核对resultMap的映射配置。开启详细的日志包括ResultSetHandler的日志有助于定位。问题二IN查询条件数量巨大导致SQL超长或性能差使用foreach做IN查询时如果传入的列表有几千上万条生成的SQL会非常长可能超出数据库限制且解析性能差。解决方案分片查询将大列表拆分成多个小列表比如每500条一组分批执行IN查询在内存中合并结果。临时表/Join查询如果数据源允许可以将ID列表插入到一张临时表然后使用JOIN查询。这在处理非常大量的数据时更高效。使用内存计算如果数据量不是特别大且业务允许可以一次性查出所有可能的数据在应用层用Java进行过滤使用Stream.filter。问题三MyBatis启动慢特别是XML文件很多时MyBatis在启动时需要解析所有的XML映射文件。当XML文件成百上千时这个过程会变慢。优化方案使用MyBatis-Spring-Boot-Starter它默认支持XML文件的热加载在开发环境但在生产环境是缓存的。考虑将一些简单的、稳定的SQL注解化使用Select、Update等注解减少XML文件数量。但复杂的动态SQL还是XML更易维护。确保mapper-locations配置的路径精确不要使用过于宽泛的通配符如classpath*:mapper/**/*.xml这会导致Classpath扫描范围过大拖慢启动速度。尽量指定到具体的目录。问题四数据库连接池配置与MyBatis的协作MyBatis本身不管理连接连接池如HikariCP的配置直接影响性能。关键参数maximum-pool-size连接池最大大小。不是越大越好需要根据数据库和应用的负载调整。connection-timeout获取连接的超时时间。设置过短在高并发时可能大量获取连接失败。idle-timeout/max-lifetime连接空闲时间和最大生命周期用于防止连接僵死。 有一个隐藏知识点MyBatis的一级缓存SqlSession级别在结合连接池时效果被大大削弱了。因为Web应用中通常一个请求一个SqlSession请求结束连接就还回连接池下一个请求拿到的是另一个物理连接缓存自然失效。所以一级缓存更多体现在单个事务同一个SqlSession内的重复查询优化上。