news 2026/9/30 7:44:50

SpringBoot3整合Mybatis避坑指南:版本选型与配置详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot3整合Mybatis避坑指南:版本选型与配置详解

前两天把一个内部项目从SpringBoot 2.7往SpringBoot 3.2上迁移,代码层面倒是没费多大劲,反而是Mybatis这一块的整合让我踩了几个意想不到的坑。网上讲SpringBoot2整合Mybatis的教程一抓一大把,但到了SpringBoot3,情况确实变了不少——JDK版本要求、依赖坐标、自动配置的行为都有变化,照着旧教程抄很容易一上来就编译报错或者启动失败。这篇文章把我这次整合的全过程整理出来,包括版本选型、配置细节、报错排查和一些进阶玩法。新手可以直接照做,老手也能对应查漏补缺,少走点弯路。

1. 项目基础与版本选型:先别急着写代码

1.1 SpringBoot3带来的两个关键变化

SpringBoot 3.0发布后,底层有两件事直接影响了Mybatis整合方式:JDK17基线、javax到jakarta的命名空间迁移。

先说JDK17。SpringBoot3是基于Spring Framework 6构建的,官方最低要求就是JDK17。这意味着你在SpringBoot2时代常用的那些依赖,如果没有同步升级,很有可能会出现类加载、自动配置时机不兼容的问题。Mybatis官方给出的适配方案也很明确:想要在SpringBoot3上用Mybatis,必须使用mybatis-spring-boot-starter的3.x分支,而不是老的2.x分支。2.3.x系列是给SpringBoot 2.x准备的,强行走Boot3会出现类似Invalid value type for attribute 'factoryBeanObjectType'的启动报错,原因就是Mybatis-Spring 2.x里的FactoryBean与Spring6的新机制不兼容。

然后是javax到jakarta的迁移。SpringBoot3全面采用了Jakarta EE 9+规范,之前常见的javax.annotation.Resource、javax.validation.constraints都改成了jakarta.*。如果你的实体类、公共模块里有这些旧路径的依赖,编译期就会直接挂掉。不过对于Mybatis本身来说,这个迁移的影响相对小,因为Mapper接口、XML映射文件用的都是Mybatis自己的类型和注解,和Servlet、Validation这类规范没有直接关系。改动主要集中在项目里的其他依赖上,比如要换jakarta.validation-api、spring-boot-starter-validation这类坐标。

很多人一上来就纠结“为什么我的代码和旧教程一模一样,却起不来”,其实根因往往就在这两个底层变化上。SpringBoot3不是简单的版本号变大,它的自动配置机制和Spring6的AOT、原生镜像支持都绑在一起,依赖选型必须跟着新生态走。

1.2 Mybatis官方适配版本与依赖坐标

我实测下来比较稳妥的版本组合是这样:

组件版本说明
SpringBoot3.2.x3.2比较成熟,JDK17+
mybatis-spring-boot-starter3.0.3适配Boot 3.0/3.1/3.2
mybatis3.5.16starter 3.0.3内置
mybatis-spring3.0.4starter 3.0.3内置
mysql-connector-j8.xSpringBoot3.2默认管理的版本

一个非常常见的坑是:很多老资料里写的是org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.2,如果你在SpringBoot3项目里照抄,大概率启动直接抛异常。这时候别怀疑自己代码有问题,先看版本。

另外从MySQL驱动角度,SpringBoot3的依赖管理里已经默认使用com.mysql:mysql-connector-j,旧坐标mysql:mysql-connector-java已废弃。如果你是Oracle数据库,对应要引入com.oracle.database.jdbc:ojdbc11,并且注意不同版本Oracle驱动对应不同的JDK要求。

依赖坐标选对之后,Mybatis的自动配置基本不需要你写一行@Configuration,因为mybatis-spring-boot-autoconfigure会在DataSource存在的前提下,自动注册SqlSessionFactory和SqlSessionTemplate。这也是SpringBoot3整合Mybatis比老式SSM整合舒服的地方,框架已经帮你把大部分样板代码封装完了。

2. 完整整合实操:从空项目到第一条SQL跑通

2.1 初始化工程与依赖导入

我习惯先用Spring Initializr生成一个空工程,选好SpringBoot版本和JDK17,然后手动往pom.xml里加Mybatis相关依赖。最终核心的依赖大概是这样的:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <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>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

注意mybatis-spring-boot-starter这个坐标要带版本号,因为SpringBoot的BOM并不管理它。很多初学者在这里漏写版本,然后Maven直接报错找不到依赖,或者静默拉到了一个旧版本。

如果你不需要Web能力,也可以不加spring-boot-starter-web,单纯做一个服务端任务。但大部分业务场景都需要HTTP接口,所以我一般保留web starter。后续如果要用分页、通用Mapper这些增强工具,再在这个基础上追加对应依赖,这一点放到后面进阶章节细说。

2.2 编写启动类、实体、Mapper与XML

工程结构我通常这样组织,简单清晰:

com.example.demo ├── DemoApplication.java ├── controller ├── service ├── mapper ├── entity └── resources └── mapper └── UserMapper.xml

启动类上需要加@MapperScan,把Mapper接口所在的包告诉Mybatis:

package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

有人会问:那Mapper接口上还要不要加@Mapper?如果你已经用了@MapperScan,接口上可以不加;如果你没加@MapperScan,就必须在每个Mapper接口上标@Mapper。这两种方式本质上一个靠包扫描,一个靠逐一声明,我建议直接统一用@MapperScan,省得漏标。

实体类就是一个普通的POJO:

package com.example.demo.entity; import java.time.LocalDateTime; public class User { private Long id; private String name; private String email; private LocalDateTime createTime; // getter / setter }

Mapper接口:

package com.example.demo.mapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Param; import java.util.List; public interface UserMapper { User selectById(@Param("id") Long id); List<User> selectAll(); int insert(User user); }

对应的UserMapper.xml放在src/main/resources/mapper目录下:

<?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="com.example.demo.entity.User"> select id, name, email, create_time from user where id = #{id} </select> <select id="selectAll" resultType="com.example.demo.entity.User"> select id, name, email, create_time from user </select> <insert id="insert" parameterType="com.example.demo.entity.User" useGeneratedKeys="true" keyProperty="id"> insert into user (name, email, create_time) values (#{name}, #{email}, #{createTime}) </insert> </mapper>

这里有几个点要说一下。resultType我习惯写全限定类名,这样最稳。虽然可以通过type-aliases-package配置别名,但全限定名在IDE里能直接跳转,重构时也更安全,不会因为某个别名没改过来导致运行时才报错。useGeneratedKeys="true"配合keyProperty="id"是MySQL自增主键回填的标准写法,插入后实体的id会被自动填充,这个我强烈推荐保留。

2.3 核心配置参数详解

接下来是application.yml,这里集中了Mybatis整合的几乎所有关键配置:

spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl logging: level: com.example.demo.mapper: debug

逐行看几个重点。

mapper-locations指定XML文件位置,这里用通配符classpath:mapper/*.xml,它决定Mybatis去哪里加载SQL映射文件。如果这个配置忘写,而你的Mapper方法又依赖XML里的SQL,运行时就会报Invalid bound statement (not found)。

type-aliases-package是类型别名扫描包。配了它之后,在XML里可以把com.example.demo.entity.User简写成User或user。不过还是那句话,为了可读性和可追踪性,我一般不在XML里用别名,保持全限定名。

configuration.map-underscore-to-camel-case是下划线转驼峰映射开关。数据库字段create_time默认不会自动映射到Java属性createTime,打开这个开关后,Mybatis会在结果集映射时自动把下划线风格转成驼峰风格。这一步能省掉大量resultMap和as别名,属于必开项。

log-impl使用Slf4jImpl而不是网上常见的StdOutImpl,原因我在下一章详细说。

3. 核心配置背后为什么这样写:关键参数拆解

3.1 驼峰映射:省掉90%的字段映射配置

map-underscore-to-camel-case: true这个配置看起来简单,实际是Mybatis日常开发中最实用的一个开关。

它的原理是:Mybatis在结果集映射阶段,默认把数据库返回的列名直接作为Java属性的匹配值。如果你的表列叫create_time,Java属性叫createTime,两者对不上,最终结果就是该字段为null。很多人排查半天发现查出来的数据明明是有的,但对象里这个字段就是空,往往就是这个开关没打开。

打开之后,Mybatis内部会通过MapUnderscoreToCamelCase把下划线风格的名字转换成驼峰风格,再和Java属性匹配。这个转换只影响结果集映射,不会影响你SQL里写的别名。比如你SQL里写select create_time as ct from user,那映射匹配的就是ct这个别名,而不是把create_time转成createTime。

所以在建表规范里,我建议数据库字段统一用下划线风格,Java属性统一用驼峰风格,然后打开这个开关。这样90%的场景都不需要手写resultMap,写起来很痛快。

3.2 SQL日志打印实测(log-impl搭配logback)

日志打印配置是排查SQL问题最有效的工具,但很多人配置完发现日志没打出来,或者打印格式很乱,问题往往出在log-impl和日志框架搭配上。

StdOutImpl是Mybatis自带的输出实现,直接把SQL日志打到标准输出。它简单粗暴,不需要额外配置,但不走日志框架,不方便做级别控制,也不方便按包名过滤。所以我更推荐Slf4jImpl,配合SpringBoot默认的Logback使用,这样logging.level.com.example.demo.mapper=debug就能精细控制只打印Mapper包的SQL。

完整一点的logback-spring.xml长这样:

<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="com.example.demo.mapper" level="debug" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <root level="info"> <appender-ref ref="CONSOLE"/> </root> </configuration>

注意Mybatis打印SQL日志的logger名是Mapper接口的全限定名,所以logger name="com.example.demo.mapper"可以覆盖所有Mapper接口。如果你的Mapper包很深,也可以直接用com.example.demo或者更上层的包名。

打印出来的日志不是一条完整的SQL,而是两部分:Preparing:显示的是预编译后的SQL模板,Parameters:显示的是实际绑定的参数。比如:

Preparing: select id, name, email, create_time from user where id = ? Parameters: 1(Long)

这个设计是因为JDBC预编译机制本身就不拼接字符串,参数是单独设置到PreparedStatement上的。很多初看日志的人会疑惑“为什么SQL里不是直接拼好的id=1”,这是正常现象,不是Bug。

3.3 类型别名与包扫描路径的取舍

type-aliases-package和@MapperScan这两个配置都涉及包路径,但它们扫描的目标完全不同。type-aliases-package扫描的是实体类,用于生成类型别名;@MapperScan扫描的是Mapper接口,用于注册映射器。很多人把它们混在一起,导致报错时思路混乱。

对于type-aliases-package,我建议如果你项目里有大量实体类,并且XML里想少写几个字,可以配置它。但要注意别把包路径写得太宽,比如写成com.example,然后包下有很多DTO、VO、请求参数类,它们也会被扫描进来生成别名,一是启动耗时增加,二是可能出现同名类冲突。Mybatis遇到同名的不同类,启动时会直接报TypeAliasRegistry冲突异常,这时候大多数人会一脸懵,其实只要把扫描范围缩小到真正需要起别名的实体包就行。

对于@MapperScan,它会导致Mapper接口被Spring代理并注册到容器。如果你同时还有@Mapper注解,扫描范围交叉了也没关系,Mybatis会自动去重。但要注意,如果你有多个数据源,@MapperScan一定要指定对应的sqlSessionFactoryRef,否则所有包都用同一个SqlSessionFactory,数据源就串了。

4. 常见报错排查与实测记录

4.1 Invalid bound statement (not found)

这是整合Mybatis时最高频的报错,没有之一。报错信息长这样:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.demo.mapper.UserMapper.selectById

意思是Mybatis在启动时虽然注册了UserMapper接口的代理,但在解析SQL映射时没有找到selectById这个statement。排查顺序我整理成一个清单:

  1. UserMapper.xml的namespace是否等于com.example.demo.mapper.UserMapper,一个字母都不能差。
  2. XML里<select>的id是否等于Mapper接口的方法名。
  3. application.yml里的mapper-locations是否覆盖了XML文件所在的目录。最常见的是classpath:mapper/*.xml,如果你的XML放在resources/mapper下,这个配置没问题。
  4. target/classes下有没有这个XML文件?如果XML放在src/main/java目录里,Maven默认不会把它打包到classes,所以会找不到。

针对第4条,如果非要XML和Mapper接口放同一个包,你需要改Maven构建配置:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>

不过我还是要劝一句:XML统一放src/main/resources/mapper是更省心的方案,不需要额外配置Maven资源过滤,目录职责也更清晰。

4.2 启动报错 Failed to configure a DataSource

这个报错出现得非常无厘头,因为它经常在你什么都没写错的情况下出现:

*************************** APPLICATION FAILED TO START *************************** Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

看到这个报错,先别急着检查application.yml里的url写没写,大概率是缺数据库驱动依赖。SpringBoot的DataSource自动配置需要根据classpath上的驱动类型来创建连接池,如果mysql-connector-j没加进去,或者scope是provided导致运行时不可见,它就无法从url推断出连接类型,最终抛这个错。

另一种可能:你确实改了配置,但配置文件名不对。SpringBoot只能识别application.yml或application.yaml,如果你写了app.yml,那就不生效。

4.3 日期时间类型映射错误

新老项目切换时,日期时间类型最容易出问题。典型报错:

org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='createTime', jdbcType=OTHER ...}

这个问题的关键在于Java类型和数据库类型不匹配。比如Java用了LocalDateTime,数据库是MySQL的datetime,一般没问题;但如果是Oracle的DATE类型,或者某些特殊的驱动实现,Mybatis在调用PreparedStatement.setObject时无法确定该用哪种JdbcType,就会默认用OTHER,然后抛出异常。

常见的解决办法有两个。一是在SQL参数上显式指定jdbcType:

insert into user (create_time) values (#{createTime, jdbcType=TIMESTAMP})

二是注册自定义TypeHandler。实际项目中建议用TypeHandler统一解决,不要在几百条SQL里到处写jdbcType。这个我在第5章专门展开。

4.4 事务不生效排查

SpringBoot + Mybatis的事务不生效,排查顺序也很固定。

先看@Transactional是不是加在public方法上,而且是不是从外部调用。同类内部this.method()调用不会走Spring代理,事务直接失效,这是最隐蔽的情况。

再看异常是不是被try/catch吃掉了。Spring默认只对RuntimeException回滚,如果你catch掉异常不重新抛出,框架根本感知不到,事务自然不会回滚。如果你希望检查型异常也回滚,要配置@Transactional(rollbackFor = Exception.class)。

还要注意Mybatis和Spring事务的配合原理。单独使用Mybatis时,SqlSession的提交是我们手动调用sqlSession.commit();整合Spring后,Spring的DataSourceTransactionManager会接管SqlSession的提交、回滚和关闭。如果你的Mapper方法没有在事务中运行,SpringBoot使用SqlSessionTemplate每次都会获取新的SqlSession,操作完成后立即关闭。所以在没有事务的方法里多次调用Mybatis,一级缓存基本没有意义。想让一级缓存发挥效果,就得保证多次查询发生在同一个Spring事务里。

5. 进阶:缓存、TypeHandler与批量写入

5.1 一级缓存与二级缓存的真实使用时机

Mybatis缓存这个话题,很多人面试时会背概念,但实际项目里不见得用对。

一级缓存是SqlSession级别的,默认开启。同一个SqlSession里执行两次相同的查询,第二次会直接命中缓存。但在Spring整合环境下,如果方法没有事务,每次Mapper调用都会开一个新的SqlSession,一级缓存几乎等于没有。只有在同一个事务里,Spring会让多次Mapper调用共享同一个SqlSession,一级缓存才会显现效果。举个例子,事务A里先查了一次用户,事务B更新了用户,事务A又查一次用户,如果没有缓存刷新机制,还是可能读到旧值。所以不要把一级缓存当成可靠的业务缓存。

二级缓存是Mapper Namespace级别的,跨SqlSession共享。开启方式很简单,在XML里加一行:

<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

配置项的含义:eviction是回收策略,LRU表示最近最少使用;flushInterval是刷新间隔毫秒数;size是缓存对象个数;readOnly为true时缓存返回同一实例,性能更好,为false时需要实体可序列化并做拷贝。

但我要泼一盆冷水:二级缓存默认关闭是有道理的,因为它很容易踩“脏数据”的坑。比如A Mapper的某个查询关联了user表,B Mapper的某个update也改了user表,但B Mapper的namespace里没有配置和A Mapper同名的缓存刷新机制,A Mapper继续读二级缓存,就会读到旧数据。多表关联查询越多,这个风险越大。我的经验是:只对极少更新、字典类、配置类的Mapper开启二级缓存,核心业务表一律不碰,除非你能保证该namespace下的所有写操作都只会通过这个Mapper完成。

5.2 自定义TypeHandler处理特殊类型

TypeHandler的作用是在JDBC的ResultSet/PreparedStatement和Java对象之间做类型转换。如果你有“数据库存了一个JSON字符串,Java端希望直接拿到List”这类需求,自定义TypeHandler是比每次手动JSON序列化/反序列化优雅得多的方案。

写一个处理JSON数组的TypeHandler:

package com.example.demo.handler; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @MappedTypes(List.class) public class JsonListTypeHandler extends BaseTypeHandler<List<String>> { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException { try { ps.setString(i, objectMapper.writeValueAsString(parameter)); } catch (Exception e) { throw new SQLException(e); } } @Override public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException { String value = rs.getString(columnName); return parse(value); } @Override public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String value = rs.getString(columnIndex); return parse(value); } @Override public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String value = cs.getString(columnIndex); return parse(value); } private List<String> parse(String value) { if (value == null) { return null; } try { return objectMapper.readValue(value, new TypeReference<List<String>>() {}); } catch (Exception e) { throw new RuntimeException(e); } } }

注册方式有两种。一种是在application.yml里配置:

mybatis: type-handlers-package: com.example.demo.handler

另一种是在XML的resultMap或者SQL参数中指定。比如你需要针对某个字段使用:

<result column="tags" property="tags" typeHandler="com.example.demo.handler.JsonListTypeHandler"/>

TypeHandler还有一个小用途:数据库字段是加密密文,Java拿到的应该是明文。你可以把解密逻辑放在TypeHandler的getNullableResult里,把加密逻辑放在setNonNullParameter里,调用方无感知。这样业务代码不会到处都是加解密的散弹式逻辑。

5.3 批量插入的三种方式与性能对比

批量插入是实际开发中绕不开的需求,三种方式我都试过,各有利弊。

第一种是XML里的foreach拼接,最常见:

<insert id="batchInsert"> insert into user (name, email, create_time) values <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.email}, #{item.createTime}) </foreach> </insert>

这种方式本质是一条多值SQL,性能不错,但要注意SQL长度会随记录数增长,容易撞上MySQL的max_allowed_packet限制。实测下来,单批500条左右比较稳,太多就要拆批。

第二种是ExecutorType.BATCH。通过SqlSession手动控制:

SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { UserMapper mapper = sqlSession.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); } sqlSession.commit(); } finally { sqlSession.close(); }

这种方式会把多条insert语句缓存到JDBC批处理中,一次性发送。它的效率在某些场景下高于foreach拼接,但要手动管理SqlSession,而且批量模式下最好不要混用查询操作,否则可能出现结果未刷出的误解。MySQL下还可以在连接URL里加rewriteBatchedStatements=true优化。

第三种是用Mybatis-Plus或者Spring Batch提供的批处理组件。如果项目本身用了Mybatis-Plus,直接用内置的批量能力,代码量最少。不过如果你只引了原生Mybatis,手动开ExecutorType.BATCH也完全够用。

三种方式我简单对比一下:

方式代码量性能坑点
foreach拼接低一般SQL长度受限,需拆批
ExecutorType.BATCH中较好手动管理SqlSession,不能混查
框架批处理低好引入额外依赖

5.4 分页插件PageHelper整合

分页插件是Mybatis生态里用的最多的增强组件,SpringBoot3下的版本要特别注意。老写法是pagehelper-spring-boot-starter:1.4.7,那个版本是为SpringBoot2准备的,在Boot3下会有兼容问题。新版本用2.1.0:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>

用法没有变化:

PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectAll(); PageInfo<User> pageInfo = new PageInfo<>(list);

核心注意点有三个。

第一,startPage只对紧随其后的第一条SQL生效。它底层是ThreadLocal,如果你在startPage和查询之间又执行了其他Mapper方法,那这个分页就会作用到错误的查询上,而且下一行线程池复用时还可能把分页参数带到其他不相关的SQL上,必须保证两者紧挨着。

第二,PageInfo要在执行完查询后立刻包装,不要在中间做流式操作又转成普通List,否则总数会丢失。

第三,复杂SQL的分页count查询是额外执行一条count语句,如果SQL本身关联了多张表,count耗时不可忽略。这时候可以重写countSql,或者提前评估是否真的需要返回总数。

6. 常用增强工具与方案取舍

6.1 多数据源下的Mybatis配置

如果你的项目需要同时访问多个数据库,Mybatis的自动配置就不够用了,需要手动定义多个SqlSessionFactory。基本思路是:为每个数据源定义独立的DataSource、SqlSessionFactory、SqlSessionTemplate,然后用不同的@MapperScan(basePackages = "...", sqlSessionFactoryRef = "...")绑定到不同的Mapper包。

一个常见的坑是:主数据源配置漏掉了@Primary,结果Spring注入DataSource时不知道选哪个,启动直接报NoUniqueBeanDefinitionException。另一个坑是直接在多个类里写@Bean创建SqlSessionFactory,没有给核心组件命名统一规范,最后排查时很难理清哪个Mapper用的是哪套配置。

多数据源场景我觉得更推荐的办法是引入dynamic-datasource-spring-boot-starter这类封装好的框架,它把数据源路由、事务切换都做好了,业务侧只需要用@DS("slave")注解就能切换,比手动维护多个Factory更省心。但只要表数量少、Mapper边界清晰,手写多Factory也能接受。

6.2 原生Mybatis与Mybatis-Plus怎么选

这是团队选型里最常被问到的问题。原生Mybatis的优势在于SQL完全可控,XML写复杂查询、动态SQL都很直观,团队规范可以用XML diff来把控。不足是单表CRUD也要手写XML或注解,开发效率偏低。

Mybatis-Plus的优势是单表CRUD直接继承BaseMapper,配合LambdaQueryWrapper可以在Java里做条件构造,不需要写XML,开发效率高很多。代价是Wrapper拼SQL的隐性逻辑多,复杂SQL不好调,而且一些深度定制需要理解Mybatis-Plus对Mybatis底层做了什么改动。

我的个人倾向是:如果项目以简单增删改查为主,直接上Mybatis-Plus;如果项目里有大量复杂报表查询、多表关联、SQL性能敏感,原生Mybatis更稳妥。两者在SpringBoot3下的依赖坐标也不同,Mybatis-Plus要用mybatis-plus-spring-boot3-starter,注意别和原生Mybatis的starter混在一起引入,否则会出现SqlSessionFactory配置冲突。

7. 一些个人体会与小技巧

整合次数多了,我现在的一套固定做法是这样的:SpringBoot用3.2.x,Mybatis用官方的mybatis-spring-boot-starter3.0.3,配置里把map-underscore-to-camel-case打开,日志用Slf4jImpl配合logback的debug级别,XML统一放在resources/mapper下,Mapper接口包用@MapperScan统一定位。这套组合我实测在多个项目里都跑得很稳。

再分享两个小技巧。

一个是启动时加上mybatis.configuration.safe-rollback-on-connection这种边缘配置不要乱开,默认值经过大量项目验证,改动前一定要想明白业务影响。

另一个是遇到SQL映射问题时,优先看打印出来的Preparing日志和Parameters日志,不要盯着报错信息猜。Mybatis的报错链路都比较长,但核心信息通常集中在最前面的那句BindingException或者TypeException上,顺着日志从Mapper接口找到XML,再从XML找到SQL,问题基本都能定位。

最后补充一点:Mybatis整合这一套东西,版本号是最不能抄错的部分。SpringBoot3生态升级很快,网上不少资料还停留在Boot2时代,照着抄之前一定要确认依赖坐标和版本分支。把版本问题解决了,配置问题就好说了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 7:44:49

SpringBoot+Vue+MySQL源码项目拆解:游戏管理平台部署与避坑实战

一套标榜“可直接运行”的SpringBootVueMySQL源码项目&#xff0c;听起来似乎很简单&#xff0c;但真正动手跑起来&#xff0c;你就会发现里面藏着不少只有踩过坑才知道的门道。这阵子我仔细拆解了一个Web及游戏管理平台信息管理系统的完整源码&#xff0c;后端是SpringBoot&am…

作者头像 李华
网站建设 2026/9/30 7:44:43

Redis密码设置全攻略:从requirepass到ACL的安全加固实践

1. 为什么Redis默认不带密码&#xff0c;却总有人被扫 先说个真实经历。前几年我搭过一套内部用的Redis&#xff0c;没设密码。当时想得很简单&#xff1a;只在内网&#xff0c;访问的人就那几个&#xff0c;没必要折腾认证。结果第二天收到告警&#xff0c;CPU占用飙到100%&am…

作者头像 李华
网站建设 2026/9/30 7:44:28

Spring Boot 3 + Flowable 7 工作流引擎实战:BPMN部署与任务流转

1. Spring Boot 3.x 与 Flowable 7.x 的版本配对&#xff1a;先跨过 javax 到 jakarta 这道坎如果你最近正想把老项目从 Spring Boot 2.x 升到 3.x&#xff0c;同时又把工作流组件换成 Flowable 7.x&#xff0c;那你多半会撞上第一个拦路虎&#xff1a;包名迁移。Spring Boot 3…

作者头像 李华
网站建设 2026/9/30 7:44:02

Redis Cluster数据分片机制详解:从哈希槽到扩缩容避坑指南

在实际业务里&#xff0c;Redis 单机撑不住的时候&#xff0c;绝大多数团队的第一反应就是上 Redis Cluster。但很多人对 Cluster 的使用只停留在“redis-cli --cluster create 一把梭”的层面&#xff0c;一旦遇到槽位迁移、CROSSSLOT 报错、CLUSTERDOWN&#xff0c;就完全不知…

作者头像 李华
网站建设 2026/9/30 7:42:59

手机中框在线三维检测如何落地?从上料定位到结果输出

手机中框为屏幕、主板、电池和按键等部件提供安装基础。随着结构趋于轻薄&#xff0c;表面同时存在平面、台阶、孔槽和胶路&#xff0c;检测任务也从少量点位抽检&#xff0c;逐步转向对多区域高度信息的在线获取。激光三维轮廓测量仪可以连续获得可见表面的高度轮廓&#xff0…

作者头像 李华