ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MyBatis核心配置文件environments节点深度解析:多环境切换与连接池配置实战

MyBatis核心配置文件environments节点深度解析:多环境切换与连接池配置实战 在项目里折腾 MyBatis 的时间也不短了从最早照着博客抄 mybatis-config.xml到后来给团队做框架封装我发现很多人对这个核心配置文件的理解都停留在“能跑就行”。尤其是 environments 这个节点不少新手觉得它不就是写个数据库地址嘛甚至有人直接把整个节点删掉也能跑起来于是更觉得它可有可无。但真正等你要做多环境切换、要接连接池、要排查诡异的连接问题时才会发现这个节点里藏的细节比你想象的多得多。这篇文章就专门把 MyBatis 核心配置文件里的 environment 讲透它究竟在解决什么问题、节点内部每个标签的含义、配置加载时的解析逻辑再给出一套可以直接抄作业的多环境配置方案最后整理几个我实际踩过的坑。无论你是刚入门想搞懂基础配置还是工作几年想补一补源码层面的原理这篇文章应该都能给你一些参考。1. environments 标签到底在解决什么问题1.1 environments 在配置文件里的位置和使命先看一个最基础的 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 properties resourcedb.properties/ environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${db.driver}/ property nameurl value${db.url}/ property nameusername value${db.username}/ property namepassword value${db.password}/ /dataSource /environment /environments mappers mapper resourcecom/example/mapper/UserMapper.xml/ /mappers /configuration在这份配置里environments是configuration的子节点它本身可以包含多个environment每个environment就是一个独立的数据库连接环境。比如你可以在同一个配置文件里定义 development、test、production 三套环境然后通过defaultdevelopment指定当前默认激活哪一套。很多人会问既然代码里用的连接信息大多来自 Spring 的 datasource为什么 MyBatis 还要自己维护一套 environment这个问题的答案要回到 MyBatis 的设计初衷上去。MyBatis 是 SQL 映射框架不是 Spring 的附属品它需要保证在没有 Spring 的场景下也能独立工作。而 environment 就是 MyBatis 自己管理数据库连接的最小单元它让框架有了“环境”的概念这也是后面 MyBatis 能支持多数据源、能动态切换环境的基础。1.2 为什么连接信息不直接写死在代码里早期写 JDBC 的时候很多人是这么干的Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/test, root, 123456);这种写法的问题很明显连接信息散落在业务代码里换环境要改代码重新编译维护起来简直是一场灾难。而通过 environment 把连接信息集中到配置文件中配合 properties 参数外部化就能做到“一处配置、多处使用、环境隔离”。我见过一些项目明明用了 MyBatis却依然把数据库配置写在 DAO 类里这种半吊子做法比纯 JDBC 还难维护。正确的思路是连接信息交给 environment 管理业务代码只关心 SqlSessionFactory 和 Mapper 接口至于连的是哪套库、用的是连接池还是直连业务层完全不需要知道。1.3 多环境切换的具体使用场景environment 的多环境能力在实际开发里最有价值的场景有三个。第一个是本地开发与生产环境隔离。开发机连本地 MySQL测试环境连测试库生产环境连线上库。以前的做法是每次部署前手动改配置文件现在只需要通过default属性或者动态指定 environment id就能实现无缝切换。第二个是同一个应用需要访问多个业务库。比如订单库和用户库不在同一个实例上你可以配置两个environment再配合 MyBatis 的多数据源路由方案通常是继承 Spring 的 AbstractRoutingDataSource 或者用 MyBatis 的 MapperScannerConfigurer 按包扫描实现一套代码操作多个库。第三个是承载 SqlSessionFactory 级别的连接策略。在 MyBatis 的源码里Configuration对象持有一个Environment字段里面封装了DataSource和TransactionFactory。如果你有特殊需求比如想给连接池做动态切换完全可以替换这个 Environment 对象。这个能力在二次开发或者框架封装时非常有用。我自己的实践是哪怕项目最终用 Spring 管理数据源我也习惯在 mybatis-config.xml 里留一份完整的 environment 配置不为别的就为了本地调试和工具类直接读取时能少踩坑。2. environment 节点拆解事务管理器与数据源2.1 transactionManager 的 JDBC 与 MANAGED 之争一个environment内部有两个必填子节点transactionManager和dataSource。先看事务管理器。transactionManager的type属性有两个内置值JDBC和MANAGED。JDBC表示事务由 MyBatis 自己管理也就是它直接使用 JDBC 的 Connection 来做 commit、rollback、close 操作。这个类型适合在非容器环境下使用你可以在代码里通过 SqlSession 手动控制事务SqlSession session sqlSessionFactory.openSession(false); try { // do something session.commit(); } catch (Exception e) { session.rollback(); } finally { session.close(); }MANAGED表示事务由外部容器管理MyBatis 不主动提交和回滚只负责把 connection 交给外部容器处理。这个类型在 Web 容器中与 Spring 整合时比较常见。需要提醒的是如果用了 MANAGED 但外部没有真正管理事务那连接上的提交和回滚就会失效这是很多奇怪事务问题的来源之一。从源码角度看type对应的解析逻辑在XMLConfigBuilder里。它会根据 type 字符串构造一个TransactionFactory实例JDBC 对应JdbcTransactionFactoryMANAGED 对应ManagedTransactionFactory。如果你有自定义需求也可以实现TransactionFactory接口并在这里写全限定类名。这里我的建议是单独使用 MyBatis 的时候事务管理器用JDBC语义清楚、行为可控和 Spring 整合的时候其实这个配置大概率会被覆盖但保留JDBC也无妨因为真正的事务控制权在 Spring 的DataSourceTransactionManager手里。2.2 dataSource 三种内置类型UNPOOLED、POOLED、JNDIdataSource比事务管理器更关键因为它直接决定了 MyBatis 怎么获取数据库连接。内置类型有三种UNPOOLED、POOLED、JNDI。UNPOOLED的含义是每次请求都新建一个 Connection用完之后直接关闭。没有连接池的管理也没有复用性能自然比较差。但它的优点是实现简单、占用资源少适合一些低并发的工具类场景或者测试环境。POOLED是使用频率最高的配置。它利用连接池复用 Connection减少频繁创建和销毁连接的开销。MyBatis 自带的连接池实现是PooledDataSource它内部维护了一个空闲连接列表和一个活动连接列表。当请求连接时优先从空闲列表中取如果没有空闲连接就检查活动连接是否达到最大数量如果没达到就新建一个如果达到了就等待其他连接归还。JNDI是为了让 MyBatis 在 Java EE 容器中使用容器提供的连接池比如 Tomcat 的 JNDI 数据源。这个类型在 Spring Boot 时代基本很少用了因为 Spring Boot 自己管理数据源的方式更直接。还有一个没有写在文档里的细节dataSource的 type 属性同样支持自定义类。你可以实现DataSourceFactory接口然后写全限定类名比如接 Druid 或者 HikariCP 的时候有人就是通过这种方式让 MyBatis 原生使用第三方连接池的dataSource typecom.example.MyDruidDataSourceFactory property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/test/ property nameusername valueroot/ property namepassword value123456/ /dataSource不过现实中MyBatis 与 Spring Boot 整合时数据源通常直接由 Spring Boot 的自动配置注入这里的 dataSource 配置往往不被使用。但理解它的三种内置类型对排查连接问题依然有很大帮助。2.3 连接池参数细节POOLED 的核心属性一览如果你使用了POOLED那下面这些属性值得认真看一眼属性默认值作用说明poolMaximumActiveConnections10任意时间的最大活动连接数poolMaximumIdleConnections5最大空闲连接数超过则归还时直接关闭poolMaximumCheckoutTime20000连接被强制归还前的最大时长单位毫秒poolTimeToWait20000请求连接时的最长等待时间超时抛异常poolPingEnabledfalse是否开启连接检测poolPingQueryNO PING QUERY SET检测连接是否有效的 SQL比如 SELECT 1poolPingConnectionsNotUsedFor0连接空闲多久后执行一次检测单位毫秒我最早用POOLED的时候曾经因为连接泄漏导致应用在高峰期假死。查了半天发现是poolMaximumCheckoutTime默认 20 秒某个慢 SQL 占着连接不放后面的请求一直在等。后来把poolMaximumActiveConnections从默认的 10 调到了 20并且把poolPingEnabled设置为 true 加上poolPingQuery为SELECT 1类似的问题就少了很多。还有个小细节poolPingConnectionsNotUsedFor的值如果设置得过小比如 0那 MyBatis 会对每个连接在获取时都执行一次 ping 查询这会额外增加一次网络往返。合理的做法是设置成一个可接受的值比如 30000也就是连接空闲超过 30 秒才做检测。3. 配置加载背后从 XMLConfigBuilder 到 Configuration3.1 MyBatis 初始化流程里的 environment 解析节点如果你想在面试里把 MyBatis 讲出深度那 environment 的加载过程是个很好的切入口。很多面试题会问“MyBatis 初始化流程是怎样的”其实核心就是XMLConfigBuilder解析 mybatis-config.xml 并构建Configuration对象的过程。XMLConfigBuilder是SqlSessionFactoryBuilder用来解析 XML 配置的解析器。它解析配置文件的入口方法是parse()在这个方法里会依次调用一系列parseXxx()方法其中就有一个environmentsElement()方法专门处理 environments 节点。environmentsElement的解析逻辑大致是读取environments节点的default属性得到默认环境 id遍历所有environment子节点逐个解析对每个 environment先解析transactionManager构造TransactionFactory再解析dataSource通过DataSourceFactory创建DataSource最后用环境 id、TransactionFactory、DataSource 构建一个Environment对象将Environment设置到Configuration对象上。在源码层面Environment是个很简单的类只有三个字段id、DataSource、TransactionFactory。但就是这三个字段构成了 MyBatis 运行时的数据库连接策略。3.2 default 属性的解析与匹配逻辑default属性的作用是设置当前默认启用的环境。MyBatis 拿到这个值后会把它和Configuration里的环境映射做对比。配置了多个environment时每个环境的 id 都会存起来default指向哪个哪个就生效。有一点容易踩坑如果default指定的 id 在environment里不存在MyBatis 不会报编译期错误而是在运行期构建Environment的时候直接抛异常大概长这样Exception in thread main org.apache.ibatis.exceptions.PersistenceException: ### Error building SqlSession. ### The error may exist in com/example/mybatis-config.xml ### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. ### Cause: java.lang.IllegalArgumentException: Cannot load environment: xxx这类问题在配置复杂、多人协作的项目里不算罕见尤其是有人把自己的 environment id 写错又恰好把 default 指过去的时候。排查思路很简单打开配置文件确认default的值和某个environment的id完全一致。还有一个细节default并不是必须的。如果不写defaultSqlSessionFactoryBuilder 在构建时也不会立即报错但调用openSession()时如果没有通过参数指定 environmentMyBatis 就无从知道该用哪个环境后续拿连接的时候就会出问题。所以建议无论什么情况都把default写上。3.3 SqlSessionFactory 构建时 environment 如何进入 Configuration继续往下走XMLConfigBuilder.parse()执行完毕后Configuration对象就构建好了。接下来SqlSessionFactoryBuilder会拿这个Configuration去创建SqlSessionFactory。默认实现是DefaultSqlSessionFactory它在构造时把Configuration保存下来后续每次openSession()都是从这个 Configuration 里拿环境信息。打开DefaultSqlSessionFactory的openSession()源码你会发现它有一个从configuration.getEnvironment()获取DataSource的逻辑private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx null; try { final Environment environment configuration.getEnvironment(); final TransactionFactory transactionFactory getTransactionFactoryFromEnvironment(environment); tx transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 构建执行器和 SqlSession } catch (Exception e) { closeTransaction(tx); throw ExceptionFactory.wrapException(e); } }这段代码把 environment 的重要性体现得很清楚Configuration里的Environment直接决定了 SqlSession 是从哪个数据源拿连接、用什么样的事务工厂。如果你在代码里动态替换Configuration的Environment那后续 openSession 的行为也会跟着变。这也是为什么有人能利用configuration.setEnvironment(...)做动态数据源切换的原因。4. 实操一套能直接跑起来的多环境配置4.1 配置文件的完整示例与逐段说明理论讲完到了动手环节。下面是我在实际项目里经常用的一套多环境配置你几乎可以直接复制过去改改参数就能用。首先是db.properties把连接信息独立出来dev.drivercom.mysql.cj.jdbc.Driver dev.urljdbc:mysql://127.0.0.1:3306/dev_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai dev.usernamedev_user dev.passworddev_pass test.drivercom.mysql.cj.jdbc.Driver test.urljdbc:mysql://192.168.1.100:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai test.usernametest_user test.passwordtest_pass然后是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 properties resourcedb.properties/ environments default${dev.environment} environment iddev transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${dev.driver}/ property nameurl value${dev.url}/ property nameusername value${dev.username}/ property namepassword value${dev.password}/ /dataSource /environment environment idtest transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${test.driver}/ property nameurl value${test.url}/ property nameusername value${test.username}/ property namepassword value${test.password}/ /dataSource /environment /environments mappers mapper resourcecom/example/mapper/UserMapper.xml/ /mappers /configuration注意看default属性我这里写的是${dev.environment}这是利用 MyBatis 的 properties 机制做了一层动态化。你可以在db.properties里定义dev.environmentdev换环境的时候只需要改这个值。这个做法比直接写死 default 要灵活一些。4.2 构建 SqlSessionFactory 时动态指定 environment除了靠default属性MyBatis 还允许在构建SqlSessionFactory时通过参数动态指定 environment。SqlSessionFactoryBuilder有一个重载方法InputStream inputStream Resources.getResourceAsStream(mybatis-config.xml); SqlSessionFactory factory new SqlSessionFactoryBuilder().build(inputStream, test);第二个参数就是 environment id。这个方式在代码里做环境切换非常方便比如你可以在启动脚本里通过 JVM 参数指定使用哪套环境String env System.getProperty(mybatis.env, dev); SqlSessionFactory factory new SqlSessionFactoryBuilder().build(inputStream, env);这种方式比修改 XML 里的default更彻底因为它直接改变了构建结果。如果你在写一些批量任务、定时任务、独立工具类这个方法值得一试。4.3 与 Spring 整合时 environment 去哪了到了 Spring Boot 项目里很多人会发现 mybatis-config.xml 里的 environments 似乎不起作用了。原因是 MyBatis Spring Boot Starter 在自动配置时会用自己的方式创建SqlSessionFactory。具体来说在整合 Spring 的场景下DataSource的配置优先来自 Spring 容器中的DataSourceBean比如spring.datasource.url配置的 HikariCP 数据源而mybatis-config.xml里的 environments 节点往往会被忽略或覆盖。这一点值得特别注意如果你辛辛苦苦在 XML 里配好了连接池参数结果发现项目实际用的是 Spring 的 DataSource不要太意外。这不是 MyBatis 的 bug而是整合场景下的设计如此。如果你确实想让 MyBatis 使用 XML 里的 environment可以考虑放弃 Spring Boot 对数据源的自动配置手动构建SqlSessionFactoryBean并指定configuration但一般没人这么干因为 Spring 管理数据源的方式显然更主流、更好维护。5. 常见问题与排查技巧实录5.1 环境找不到Cannot load environment 异常这个异常前面已经提到过场景很典型配置了多个 environment但default指错了 id或者代码里build(inputStream, test)指定的环境 id 在 XML 里根本不存在。我的排查步骤一般是第一确认SqlSessionFactoryBuilder.build()是否传入了自定义 environment id如果传了检查它的值和 XML 中的environment id...是否一致第二如果没有传检查environments default...的值是否和某个environment id...完全一致第三注意 id 的拼写包括大小写、前后空格这些细节在我踩过的坑里出现过不止一次。5.2 事务管理器的配置导致连接无法自动提交有朋友遇到过这样的问题MyBatis 单独使用时每次执行完 SQL 数据没有生效甚至查询都正常但插入就是没数据。排查到最后发现transactionManager被配成了MANAGED而代码里又没有外部容器来提交事务结果连接上的一切操作都没有真正落库。如果你用的是MANAGED类型务必确认确实有外部容器管理事务。否则请换回JDBC并且在代码里调用session.commit()。或者简单一点用openSession(true)开启自动提交模式。这里要重点提醒自动提交模式下session.commit()也可以调用但它是无意义的空操作不要指望它来兜底。5.3 连接池配置参数导致的高并发连接耗尽这个问题的经典表现是应用跑一段时间后所有请求都卡在获取数据库连接上日志里打出一堆连接超时或者Cannot get a connection, pool exhausted之类的错误。原因往往是poolMaximumActiveConnections太小或者某个连接被长时间占用没释放。实际操作中我会做几件事把poolMaximumActiveConnections从默认的 10 调到 20~50看业务并发量而定把poolMaximumCheckoutTime从 20000 缩短到 10000 左右让长时间占用的连接尽快被强制回收开启poolPingEnabled配置poolPingQuery为SELECT 1并且把poolPingConnectionsNotUsedFor设置为 30000同时检查代码里是否存在 SqlSession 或 Connection 没关闭的情况这是连接无法归还的根本原因。5.4 时区与驱动导致的连接报错MySQL 8.x 和旧驱动引发的时区问题也常和 environment 配置有关。报错一般是The server time zone value Öйú±ê׼ʱ¼ä is unrecognized or represents more than one time zone.这本质上不是 MyBatis 的问题而是 JDBC URL 里缺少serverTimezone参数。解决办法是在property nameurl value.../中加上serverTimezoneAsia/Shanghai并且确认驱动是com.mysql.cj.jdbc.Driver。这种问题只在环境切换时容易冒出来因为不同环境的 MySQL 版本或默认时区可能不一样建议一开始就把 URL 参数写全。最后分享一点个人习惯关于 environment 我还有一个使用习惯在团队内部的基础框架里我会封装一个EnvironmentIdHolder把当前线程应该使用的 environment id 存在ThreadLocal里然后自定义一个DataSource的实现在getConnection()时根据当前线程的 environment id 切换连接。这样上层业务代码完全无感知底层却能实现读写分离或者多租户隔离。这个玩法算是 environment 机制的一个进阶用法虽然复杂度高一些但理解了 MyBatis 对 environment 的解析和管理机制之后你会发现一切源码都在那里改起来并不神秘。每次帮同事排查环境相关的问题最后多半都落在“配置和代码默认值不一致”这类基础原因上。真正理解了 environments 和 environment 的设计意图很多问题不用查都能提前想到。希望你读完这篇再去翻翻自己项目里的 mybatis-config.xml能把每一行配置都看懂、看透。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进