Spring Boot + MyBatis这套组合,几乎已经成了国内Java后端的“默认选项”。我最近这大半年,把手上一个老项目从SSH架构整体迁移到了Spring Boot 2.7 + MyBatis 3.5,中途翻了小半套源码,也踩了不少文档没写明白的坑。群里问到最多的几个高频问题,翻来覆去就是分页插件怎么配、二级缓存什么时候生效、@Update执行慢该从哪查、Oracle时间字段映射又是一堆幺蛾子。索性把从项目初始化到故障排查的完整过程整理成文,尽量把每一步的“为什么”也讲透,而不是只甩一段能跑的配置。
这套东西看起来简单,本质上却是两套成熟框架的深度整合。Spring Boot负责自动装配和生命周期管理,MyBatis负责SQL与Java对象的映射和持久化。真正的问题几乎都出在“边界”上——配置项谁生效、插件按什么顺序拦截、缓存作用域到底到哪一层。搞懂这些边界,才算真正会用。
1. 项目选型与整体设计:为什么是MyBatis + Spring Boot
1.1 Spring Boot为什么是MyBatis的最佳宿主
很多人觉得用Spring Boot无非是省了写XML配置的时间,其实它对MyBatis的价值远不止这一点。MyBatis自己是个纯粹的持久层框架,它需要外部容器来管理数据源、事务、Mapper接口的注册和生命周期。在没有Spring Boot的时代,你得手动创建SqlSessionFactory,写一堆DataSource配置,还要处理Mapper扫描的细节。
Spring Boot用自动配置机制(AutoConfiguration)把这一步完全接管了。mybatis-spring-boot-starter在启动时会根据classpath里的依赖自动创建SqlSessionFactory和SqlSessionTemplate,你只需要提供一个DataSource和一些基础配置。这意味着团队里任何一个新人都能在五分钟内跑通一个可用的数据库访问链路,不需要理解底层全貌。
这背后其实是Spring Boot“约定优于配置”的思想在起作用。它把复杂的Bean装配过程变成了“依赖引入+少量配置”两步走,让MyBatis的接入成本指数级下降。我见过不少老项目,迁移到Spring Boot后,把原来几百行的XML配置文件压缩成了几十行,维护成本肉眼可见地下降。
1.2 MyBatis与JPA、MyBatis Plus的选型对比
选型的时候绕不开一个问题:同样做持久层,为什么选原生MyBatis而不是Spring Data JPA或者MyBatis Plus?我经历过至少三个项目在这条路上来回摇摆,说说实际感受。
JPA的特点是面向对象建模,对简单CRUD非常友好,尤其是单表操作几乎不用写SQL。但它一遇到复杂查询、多表联查、动态条件这种“中国特色需求”,就会逼你写JPQL或者原生SQL,而这时候MyBatis的优势就出来了——SQL完全可控,动态SQL是原生能力,性能调优时可以精确控制每一条语句。
MyBatis Plus是在原生MyBatis之上做增强的,内置了通用CRUD、条件构造器、分页插件等能力,开发效率确实高。但我的建议是,如果你刚接触这套技术栈,先从原生MyBatis入手把原理吃透,再引入Plus就非常顺畅。直接上手Plus容易产生“什么都能调用但不知道为什么”的空洞感,一旦遇到Plus解决不了的场景,回退到原生写法就会很被动。
从项目长期维护的角度看,团队如果以老练的SQL工程师为主,原生MyBatis会让代码审查非常舒服,因为每条SQL都是透明的。这也是我们最终选择原生MyBatis的原因。
1.3 从零初始化项目的两条路径
创建Spring Boot项目,我推荐两条路线。第一条是IDEA自带初始化器,直接选择Spring Initializr,勾选依赖时添加Spring Web、MySQL Driver,生成后自己加一个mybatis-spring-boot-starter。第二条是直接在Spring官网的start.spring.io页面生成,导入IDEA后补齐MyBatis相关依赖。
注意:如果使用了IDEA的Spring Initializr,生成的项目默认构建工具是Maven,建议保留默认。Gradle虽然更灵活,但国内团队Maven的普及度更高,遇到依赖冲突时查询资料也更方便。
实际搭建时,我的标准动作还会额外做三件事:设置Java版本为项目团队统一版本、把groupId和artifactId改成公司规范、在pom里预置一个统一的dependencyManagement。这些小细节在新项目刚起步时不显眼,但等依赖多了再回头改就很麻烦。
2. 环境搭建与基础配置拆解
2.1 Maven依赖引入:starter、驱动与连接池
我当前的稳定组合是Spring Boot 2.7.18 + MyBatis 3.5.x + mybatis-spring-boot-starter 2.3.x。为什么要强调组合版本匹配?因为我见过太多人直接把starter升到最新版,结果Spring Boot版本没跟上,启动直接报映射器绑定异常。
<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>注意一个细节:新版MySQL驱动坐标已经从mysql-connector-java改成了mysql-connector-j,Spring Boot 2.7.18里你不需要手动指定版本,它会被Spring Boot的依赖管理自动锁定。如果你用的是旧坐标,也建议统一升级到新坐标,避免后续出现驱动兼容问题。
连接池我直接用HikariCP,它是Spring Boot的默认选型,没有额外引入的必要。实际压测中,HikariCP在低延迟场景的表现确实比DBCP2和C3P0好,而且配置简单,几乎不需要调优就能满足绝大多数业务场景。
2.2 application.yml配置项逐行解析
这是最容易被“复制粘贴”但完全没搞懂的一部分。我按照实际项目配置逐行说明,包括经常被忽略的随机端口配置和优雅关闭。
server: port: ${PORT:8080} shutdown: graceful spring: application: name: demo-boot datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 15 idle-timeout: 30000 connection-timeout: 30000 pool-name: DemoHikariPool mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: true cache-enabled: trueURL里的参数每项都有讲究:useUnicode和characterEncoding保证UTF-8存取;serverTimezone解决MySQL 8.0以上版本的时间时区问题,不设这个参数很容易出现时间比实际早8小时;allowPublicKeyRetrieval用在了非SSL连接时MySQL 8的认证流程中,Google一圈抱怨无法连接的问题里,有一半是这个参数缺失造成的。
mapper-locations指定XML文件位置,这是Mapper接口和XML映射文件关联的物理基础。map-underscore-to-camel-case: true允许你直接把数据库的下划线字段自动映射成驼峰Java属性,比如user_name直接映射为userName,省去大量resultMap手写量。cache-enabled先留个开关,后面聊缓存会展开。
还有一个容易被忽略的场景:开发环境多实例调试时,需要随机端口启动避免冲突。可以在配置里加server.port: ${PORT:0},启动时让Spring随机分配一个可用端口。但注意,这要求你的应用逻辑里没有写死端口的地方,否则会出现拿着随机端口去连接固定地址的诡异Bug。
2.3 Mapper接口扫描与XML绑定机制
MyBatis与Spring整合后,Mapper接口本身没有实现类,它是通过JDK动态代理生成代理对象然后注册到Spring容器的。你唯一要做的事就是在启动类或配置类上声明扫描范围:
@SpringBootApplication @MapperScan("com.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }@MapperScan的背后逻辑是:Spring在启动时会扫描指定包下所有接口,然后为每个接口生成一个MapperFactoryBean,交给MyBatis的SqlSessionTemplate管理。这个过程有一个容易踩的坑——如果Mapper接口的包路径和XML文件的namespace没有严格对应,启动时并不会立刻报错,只有第一次调用这个Mapper方法时才会抛出“Invalid bound statement (not found)”异常。
我排查这种问题时的固定动作是看三点:第一,mapper-locations路径是否真的能匹配到XML文件存储位置;第二,XML文件的namespace是否和接口全限定名完全一致;第三,接口方法名与XML中语句的id是否一致、参数类型是否匹配。这三处任何一处不一致都会出现运行时才发现的问题。
2.4 打印SQL与配置文件实战
开发阶段不打印SQL,排查问题就跟瞎子摸象一样。MyBatis的SQL打印并不在MyBatis自身配置里,而是通过日志框架实现。Spring Boot 2.x默认的日志门面是SLF4J,底层实现是Logback,所以最直接的方式是在application.yml里这样配:
logging: level: com.demo.mapper: debugcom.demo.mapper是你的Mapper接口所在包路径。一旦配置成debug级别,MyBatis会通过该接口的日志对象打印完整的Preparing、Parameters和Rows,这对排查参数绑定问题和SQL语法错误极其有用。
补充一个进阶用法:生产环境如果不想在配置文件中暴露SQL,可以利用MyBatis拦截器单独实现SQL日志输出,把参数用占位符替换后打印完整SQL。我写过一个小拦截器,核心逻辑是在Executor的query/update方法上切入,在preHandle处取出BoundSql,然后遍历ParameterMapping拼接真实参数。这套做法的好处是可以控制开关、脱敏和格式化,比纯日志级别的方案更灵活。
3. 核心功能实战:CURD、分页与缓存
3.1 动态SQL是MyBatis的灵魂
真正拉开MyBatis和其他ORM差距的时刻,就是遇到复杂的动态查询条件时。比如一个用户列表查询,姓名可能为空,部门可能为null,状态可能有值也可能要求查全部。用MyBatis的<where>加<if>几乎是标配解决方案:
<select id="listUsersByCondition" parameterType="map" resultType="UserEntity"> SELECT * FROM t_user <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND dept_id = #{deptId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select><where>标签很聪明,它会在子标签有内容时自动生成WHERE关键词,并且自动移除第一个AND/OR,避免写出WHERE AND name = ?这种非法SQL。同理<set>标签用于update语句,自动处理SET后多余逗号的问题。
动态SQL还包括<choose>、<trim>、<foreach>等标签。<foreach>用于批量操作最容易出错,集合为null或空时如果不做保护,生成出来的SQL会是IN ()直接报语法错误。我的习惯是在<foreach>外层套一个<if test="ids != null and ids.size() > 0">。
3.2 分页插件PageHelper的配置与用法
分页这个问题,网上一下子就能搜到一堆文章,但能说清原理的很少。PageHelper是一个基于MyBatis拦截器实现的分页插件,它的核心逻辑是通过拦截Executor.query()方法,在原有SQL上动态拼接LIMIT ? OFFSET ?。我用的是PageHelper 5.3.x,配合Spring Boot的配置如下:
pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true在代码中的用法分两种。传统方式是:
PageHelper.startPage(pageNum, pageSize); List<UserEntity> list = userMapper.selectUserList(); PageInfo<UserEntity> pageInfo = new PageInfo<>(list);startPage之后的第一查询会被拦截并自动做分页处理,注意必须是紧接着的第一条查询。这个“紧接着”是PageHelper使用中最容易犯错的地方,中间但凡多一次查询,分页就会作用到错误的SQL上。设计上也有点建议:更推荐把startPage放在和查询方法紧邻的上一行,中间不要穿插任何逻辑代码。
另一种是MyBatis Plus风格的分页,使用Page对象作为参数传入Mapper方法。两者的差别在于PageHelper是线程本地变量保存分页参数,而Plus的分页是把Page实体直接作为参数传递,后者在可读性和方法重入方面明显更优。如果团队规范允许引入MyBatis Plus,建议用Plus的分页方式替代PageHelper,能少很多由于调用时序引发的诡异问题。
分页还有一个隐藏性能点:count语句的生成。PageHelper默认会执行一条count查询统计总数,如果你的主查询很复杂(多表join、子查询),count的效率可能很低。我的做法是给count查询单独写一个轻量统计SQL,用PageHelper的countSuffix或手动改写,尽量避开大表join。
3.3 MyBatis缓存机制:一级缓存与二级缓存
MyBatis的缓存是面试高频点,也是线上问题容易踩雷的地方。先说一级缓存,它是SqlSession级别的,默认开启且无法关闭。在一次数据库会话中,如果连续执行两条完全相同的查询,第二次会直接从缓存返回,不经过数据库。
这个机制在Spring整合环境下有一个容易被忽视的问题:Spring管理的SqlSession默认每执行一次Mapper方法就会关闭并重建,所以一级缓存实际作用域通常局限在单次方法调用内。只有在同一个方法中连续调两次相同查询才可能命中的一级缓存,跨方法几乎不可能命中。因此想靠一级缓存提升性能,效果微乎其微,反而是长会话场景(比如手动控制SqlSession的批处理)需要注意缓存带来的数据一致性问题。
二级缓存是Mapper级别的,跨SqlSession共享,默认关闭。开启方式是在Mapper XML中声明:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="false"/>这里我想聊聊readOnly参数。如果设置成true,MyBatis会直接返回缓存对象的引用,性能好但没有隔离;设置成false,MyBatis会通过序列化复制对象后返回,安全性好但有序列化性能开销。如果你的实体类没实现Serializable,开启readOnly=false会直接报序列化错误。
二级缓存最大的雷区是关联查询。一个多表join查询的结果缓存在一个Mapper里,但另一张表的Mapper做了更新,这个缓存不会自动失效。这就是为什么很多团队明令禁止在分布式环境下开启二级缓存——数据一致性问题很难保证。我的经验是:单机、低频更新、不涉及join的表可以考虑二级缓存;其他情况一律关闭。默认配置里我保持了cache-enabled: true,但没有任何Mapper声明<cache>,等同于所有Mapper都不启用二级缓存。
3.4 XML文件的编写体验优化:高亮、跳转与校验
写MyBatis的XML映射文件,很多人还停留在裸写XML的状态。IDEA里只要引入了MyBatis插件,XML文件就能获得三个关键能力:一是Mapper方法名和XML id之间的跳转,二是写了错误SQL时的实时校验,三是SQL语言高亮。
XML高亮这件事,看起来是锦上添花,实际操作中被低估了。没有高亮时,一个多条件嵌套的查询SQL写错一个标签,排查可能花掉几十分钟。有了IDE支持,标签不闭合、属性名拼错这类低级错误在保存瞬间就能看到。
另外一个提升效率的技巧是:把常用SQL片段拆成<sql>标签。比如所有查询都要用到的字段列表和关联条件,定义成公共片段后用<include>引用,既保证一致性,又减少重复编写。对多表join的查询,我会把连接条件单独抽出来,这样后续新增筛选条件时不会动到join骨架。
4. 常见问题与性能排查实录
4.1 @Update执行慢的场景与排查思路
群里问“MyBatis @Update 执行慢”的频率非常高。这个问题不能一概而论,我拆成几类场景逐一说排查路径。
第一类是单条update慢,每次执行都在一秒以上甚至几秒。这种我优先怀疑锁竞争。用SHOW ENGINE INNODB STATUS查看当前事务锁等待,大概率会看到某个行锁或者间隙锁卡住了。常见原因是事务A更新某行未提交,事务B尝试更新同一行,MyBatis本身只是执行SQL,锁问题完全是数据库层的事,先排查索引是否有匹配的更新条件。比如更新时where条件没有走索引,就会导致行锁升级为表锁,并发场景下极其致命。
第二类是批量update慢。这类问题常见原因是批量语句在循环里逐条提交,MyBatis默认的ExecutorType为SIMPLE,每次执行都自动提交。我建议手动配置批量ExecutorType:
mybatis: executor-type: batch或者使用SqlSessionTemplate的batch模式,能减少网络往返和日志刷盘。实测一个5万条数据的批量更新,从逐条执行的三四分钟压缩到二三十秒,效果肉眼可见。
第三类是update执行速度正常,但整个接口响应慢。这种往往是事务过大导致连接池被占满,比如一个事务里包含了大量查询和update,持锁时间过长。排查方法是打印事务执行时间,拆解每个步骤的耗时占比。我处理过一个案例,update本身2ms,但它所在的事务前面有个远程调用耗时800ms,把事务边界一缩,整体从900ms降到15ms。
4.2 Oracle查询时间字段映射问题
MySQL迁移到Oracle,或者在两边同时使用的项目,最容易出的问题就是时间字段映射。Oracle的DATE类型包含时分秒,但JDBC驱动映射到Java的java.sql.Timestamp和java.util.Date时,如果不显式处理,容易丢失秒级精度或者格式错乱。
我在配置里的处理方式是,对时间字段在resultMap中显式指定jdbcType和javaType:
<result column="CREATE_TIME" property="createTime" jdbcType="TIMESTAMP" javaType="java.util.Date"/>查询参数传时间时,同样要在参数注解里指定:
List<OrderEntity> listByTime(@Param("startTime") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime);还有一个细节,Oracle的SYSDATE返回精度很高的时间,存入的是秒级。如果你用#{createTime}传入一个LocalDateTime对象,JDBC 4.2后游标映射问题会出现版本差异。稳妥做法是entity里统一用LocalDateTime,在XML里避免直接比较字符串形式的日期,而是比较Date类型参数。
4.3 Spring Boot版本过高引发的兼容问题
Spring Boot的版本节奏这两年很快,每次大版本升级都伴随一堆兼容问题。我踩过一次最典型的:项目用Spring Boot 3.0 + MyBatis时,发现mybatis-spring-boot-starter 2.x版本不兼容,因为Spring Boot 3基于Jakarta EE 9,包名从javax变成了jakarta,老版本starter依赖的javax.sql完全失效。
Spring Boot 3.0下必须使用mybatis-spring-boot-starter 3.0.3以上版本,同时MyBatis本身也要求3.5.13以上。如果你还在用Spring Boot 2.3.x又要升到2.7.x,同样要注意xstream、hikari等依赖的版本变化。我的升级策略是:先升级Spring Boot小版本,跑通基础用例之后再升MyBatis相关依赖,最后再处理断崖式变更(比如Java 17记录类替换Getter/Setter)。
还有一个常见情况是springboot版本太高导致配置文件失效。比如Spring Boot 2.4之后,spring.profiles.active的写法被废掉了,得改成spring.config.activate.on-profile。很多老配置直接搬过来会静默失效,尤其要注意spring.datasource和mybatis这些配置项在新版本中是否有别名或被重新归类。
4.4 数据库驱动连接参数与占位符的坑
连接串里的每个参数都可能是性能瓶颈。有一个很隐蔽的参数是rewriteBatchedStatements=true。如果不加这个参数,MySQL JDBC驱动并不会真正把多条insert批量提交,虽然代码里用的batch API,但驱动还是在循环逐条执行。加上之后,驱动会把批量SQL重写成多值insert,性能差异可以达到十倍以上。我们接口从“慢到超时”优化到“毫秒级返回”,关键就在这一行参数。
另一个坑是useSSL=false在一些高版本驱动上会导致警告刷屏,但无碍功能。真正的坑在于连接串里的characterEncoding=utf8,MySQL 8.0默认使用utf8mb4,如果你明写utf8,表情符号存储时会报错或截断。改成utf8mb4同时确认数据库表字符集也是utf8mb4,才敢保证内容安全。
MyBatis的#{}和${}占位符区别就不用多说了,${}直接字符串拼接存在SQL注入风险。但很多人忽略了一个点:分页插件动态拼接SQL时,有些场景会用到${},比如动态表名、动态排序列名。这种地方如果能用白名单校验,就不要直接把用户参数放进去,我习惯把所有排序列名在代码层做一次映射校验,不匹配直接就拒绝请求。
5. 面试题视角:高频考点评析
5.1 MyBatis核心原理与生命周期问题
MyBatis的原理是面试必问的,核心要能说清从Mapper接口到数据库执行的全链路。我的标准表述是:Mapper接口通过MapperProxy动态代理生成代理对象,调用方法时解析MapperMethod;MapperMethod会根据SQL类型调用SqlSession的对应方法;SqlSession内部通过Executor执行SQL,Executor负责一级缓存和二级缓存的逻辑,最后通过StatementHandler和ParameterHandler完成JDBC层面的交互。
一级缓存为什么是SqlSession级别、二级缓存为什么是Mapper级别、使用二级缓存时为什么需要序列化,这几个问题都是高频考点。答案对应本文3.3节的内容。面试官如果继续追问“分布式环境下二级缓存有哪些隐患”,你就可以展开缓存一致性和脏数据模型的问题,把场景具体到实际项目。
生命周期相关的问题也一样:SqlSessionFactory是全局单例,SqlSession是线程不安全的、每次请求都应新建,Mapper是绑定在SqlSession基础上的线程安全代理。这三个对象的生命周期搞清楚了,才不会被问倒。
5.2 Spring Boot自动配置与MyBatis的联动
Spring Boot面试题的经典套路是“说说自动配置原理”,落地到MyBatis就是:mybatis-spring-boot-starter在META-INF/spring.factories里声明了MybatisAutoConfiguration,这个类用@ConditionalOnClass和@ConditionalOnBean判断是否需要执行,然后通过SqlSessionFactoryBean创建SqlSessionFactory,并注册MapperScannerConfigurer完成Mapper扫描。
追问环节还会涉及“自定义starter怎么做”,这时候要能说清楚自动配置类、条件注解、配置绑定(@ConfigurationProperties)三者的协作关系。我建议直接看mybatis-spring-boot-starter源码,它一千多行代码,逻辑清晰,是理解Spring Boot自动配置机制的最佳教材。
还有一个接口逻辑需要注意:Spring Boot 2.7里MybatisAutoConfiguration上标记了@EnableConfigurationProperties(MybatisProperties.class),配置前缀是mybatis。我的yml里所有带mybatis.前缀的配置都会绑定到这个类上,理解这一点,就能解释为什么mybatis.configuration.*下的属性能直接映射到MyBatis全局Config。
5.3 综合场景设计题的答题套路
面试官喜欢给一个“高并发订单系统,请设计数据访问层”的场景题。我的回答框架分三层:第一层讲CRUD和事务边界,如何处理批量插入和唯一索引冲突;第二层讲缓存层级,何时用Redis、何时用MyBatis二级缓存,阐述各自的风险;第三层讲分页和大数据量查询,说明主键分页和LIMIT深分页的差异。
直角深分页的问题一定要答出来:LIMIT 100000, 20这种写法会先扫描前面十万行再丢弃,性能极差。替代方案是记录上一页的最大主键,用WHERE id > ?加上ORDER BY id LIMIT 20。这套思路在MyBatis里实现特别自然,把上一页游标作为参数传进来就行。能答到这一层,说明你确实做过大规模数据的项目。
6. 源码复盘与真实项目落地经验
6.1 核心调用链:SqlSession、Executor与MapperProxy
调试MyBatis问题时,花些时间跟一遍源码会有长远回报。我推荐的阅读路径是:先看Configuration类里注册的MapperRegistry和InterceptorChain,然后看MapperProxy的invoke方法,接着跟到MapperMethod.execute。这层能搞清接口方法名和SQL语句怎么对应上。
继续往下就是Executor体系。BaseExecutor负责一级缓存和获取连接,SimpleExecutor/BatchExecutor/ReuseExecutor分别对应三种执行模式。一级缓存的key是CacheKey,由statementId、SQL、参数和分页信息组成的。我记忆里踩过的一个坑就发生在这里:跨Mapper执行同样SQL但statementId不同,一级缓存不会共享,这是很多人误以为一级缓存有问题的原因。
二级缓存在CachingExecutor这一层,它包裹着BaseExecutor。执行的顺序是先查CachingExecutor的二级缓存,未命中再进入BaseExecutor查一级缓存。每次update操作都会清空相关Mapper的二级缓存,但一级缓存是否清空取决于配置localCacheScope,默认在同一个SqlSession下可能会保留旧值,这在长事务里需要注意。
6.2 实践中的项目部署与自动化
项目开发完还要能稳定部署,我目前的标准流程是Docker Compose编排Spring Boot容器和MySQL容器,MyBatis配置通过环境变量动态注入。这时配置里的${PORT:8080}、${DB_PASSWORD}这类占位符的价值就体现出来了。同一个镜像,在开发、测试、生产各环境通过环境变量覆盖配置,完全不改代码。
我维护过的一个迁移项目,Dockerfile里的基础镜像是eclipse-temurin:17-jdk-alpine,构建阶段用Maven先打包再拷贝jar包,能有效缩小镜像体积。启动命令用了优雅关闭配置:
ENTRYPOINT ["java","-Xms256m","-Xmx512m","-jar","/app/demo.jar"]这里注意一点:Java容器的内存参数不要只设置堆,最好把容器总内存限制也交给Docker控制,否则容易出现容器内Java进程不受控的情况。我在实际项目中用-XX:MaxRAMPercentage=75.0替代固定堆内存,让JVM根据容器限制自动调整堆大小,线上表现稳定很多。
6.3 团队协作与代码规范
最后分享一个从经验里总结的团队协作建议。MyBatis最容易被忽视的是XML文件和Mapper接口的命名规范。我要求团队所有XML文件以实体名命名,Mapper接口必须在文件名上就能看出对应关系,比如UserMapper与UserMapper.xml必须一一对应。同时,每个Mapper接口的类头注释里标明业务场景,XML的SQL里对复杂语句加上注释,说明业务意图和涉及的表。
这样做的目的很朴素:三个月后回来看代码的同事(或者自己),不用靠猜就能重建上下文。MyBatis的SQL是透明的,这层透明不利用起来就太可惜了。
还有一个小技巧:如果项目长期由多人维护,建议在CI流程里加入MyBatis SQL的静态检查。网上有现成的MyBatis SQL拦截插件,在测试阶段做一次真实执行,用EXPLAIN验证关键查询是否走了索引。我用这个方式拦截过好几条线上才会暴露的大表全扫描SQL,省下的运维成本远超引入这层检查的成本。
回到开头说的那个老项目,迁移完成到现在跑了快一年,最深的体会是:MyBatis + Spring Boot这套组合,能力上限很高,但大多数问题的根源不在框架本身,而在使用者是否理解边界。缓存边界、分页插件时序边界、事务边界、版本兼容边界,每一条边界都是一道关卡。把边界理清楚,你会觉得这套工具是真的顺手,而不是在网上搜一段配置粘贴了事。
最后补一个我自己常用的辅助手段:在本地开发时,把logging.level设置成trace,然后完整刷一遍查询接口,观察SQL的执行次数。如果发现同一数据被查询了多次,多半就是N+1问题或者缓存没有正确利用,这种肉眼排查的方式比任何工具都直接。做完这一步再去设计缓存策略,方向基本上不会偏。