SpringBoot整合MyBatis怎么配置

SpringBoot 整合 MyBatis 的代码并不多,但 Mapper 扫描、XML 路径、数据库驱动、驼峰映射和事务边界任何一处配错,启动或查询时就会报错。这篇从依赖和目录结构讲起,按当前项目实践梳理配置、Mapper、XML、事务和常见排查方法,不照抄旧版本配置。

SpringBoot 和 MyBatis 的整合,表面上就是加几个依赖、写一个 Mapper、配一段 YAML。

但真正开始写业务后,最常见的报错往往都集中在这些地方:

  • Mapper 没有被 Spring 扫描到
  • XML 文件路径不对
  • namespace 和方法名对不上
  • 数据库下划线字段没有映射到 Java 驼峰属性
  • 参数名在 XML 里取不到
  • 事务注解写了,但实际没有生效
  • 为了打印 SQL,生产环境开了过于详细的日志

这篇从一条最小可用链路开始,讲清 SpringBoot、MyBatis 和 MySQL 是怎么接起来的。

版本方面不再照抄旧项目里的 SpringBoot 2.3.3 和 MyBatis Starter 2.1.3。

新项目应该使用项目当前的 SpringBoot BOM 和兼容的 MyBatis Spring Boot Starter,具体版本以项目依赖管理和官方兼容矩阵为准。

整合链路是什么

一次 Controller 查询,大致会经过下面这条链路:

HTTP 请求
   │
   ▼
Controller
   │ 调用
   ▼
Service
   │ 事务边界、业务编排
   ▼
Mapper 接口代理
   │ MyBatis 生成代理实现
   ▼
Mapper XML / 注解 SQL
   │
   ▼
DataSource 连接池
   │
   ▼
MySQL
​

SpringBoot 主要负责自动配置和 Bean 管理。

MyBatis 负责把 Mapper 方法和 SQL 绑定起来。

数据库驱动和 DataSource 负责真正建立 MySQL 连接。

三者任何一层没有接好,问题都会在启动或第一次调用时暴露出来。

推荐目录结构

一个简单项目可以这样组织:

src/main/java/com/mebugs/demo
├── DemoApplication.java
├── controller
│   └── UserController.java
├── service
│   └── UserService.java
├── mapper
│   └── UserMapper.java
└── model
    └── User.java

src/main/resources
├── application.yml
└── mapper
    └── UserMapper.xml
​

目录不是唯一答案,但 Mapper 接口、XML 和实体职责最好保持清晰。

后面排查扫描和 XML 路径时,目录结构越规整,越不容易绕晕。

依赖怎么配

依赖版本不要从几年前的文章直接复制。

SpringBoot 项目通常由 parent 或 dependency management 统一管理 Spring 依赖版本,MyBatis Starter 选择与当前 Boot 大版本兼容的版本。

核心依赖的职责大致如下:

<dependencies>
    <!-- Web 接口 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MyBatis 与 SpringBoot 的整合 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>

    <!-- MySQL 驱动,现代项目通常使用这个坐标 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
​

老项目里仍可能看到 mysql:mysql-connector-java,是否替换要看项目使用的 SpringBoot 和驱动版本,不要只改坐标而不做完整回归。

如果项目使用其他数据库,只需要替换对应 JDBC 驱动和连接地址,MyBatis 的 Mapper 机制基本不变。

数据源和 MyBatis 配置

一个基础 application.yml 可以这样写:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: demo_user
    password: change-me

mybatis:
  mapper-locations: classpath:/mapper/**/*.xml
  type-aliases-package: com.mebugs.demo.model
  configuration:
    map-underscore-to-camel-case: true
​

这几项分别解决:

  • spring.datasource.url:数据库连接地址和连接参数
  • username/password:数据库账号信息,生产环境不建议硬编码
  • mapper-locations:告诉 MyBatis 去哪里找 XML Mapper
  • type-aliases-package:给实体类型配置别名,减少 XML 中的完整类名
  • map-underscore-to-camel-case:把 user_name 映射到 userName

生产环境建议使用环境变量、Secret 或配置中心注入数据库密码。

不要把真实密码提交到 Git。

配置文件中的日志

开发环境可以临时打开 MyBatis SQL 日志:

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
​

这个配置适合本地快速看 SQL,不建议直接用于生产。

生产环境应该使用项目统一日志体系,并注意参数、Token、密码等敏感信息脱敏。

驼峰映射不是万能的

开启 map-underscore-to-camel-case 后,常见字段可以自动转换:

数据库字段:user_id
Java 属性:userId

数据库字段:created_at
Java 属性:createdAt
​

但复杂查询、别名、嵌套对象和特殊字段仍然建议显式写 resultMap。

如果查询结果全部为 null,排查时不要只盯着 Java 实体类,也要检查 SQL 列名、别名、XML 的 resultType 和实际返回字段。

Mapper 怎么扫描

Mapper 接口需要被 Spring 注册成 Bean。

有两种常见方式。

每个接口加 @Mapper

@Mapper
public interface UserMapper {

    User findById(Long id);
}
​

这种方式直观,但每个 Mapper 都要加注解。

启动类统一扫描

项目 Mapper 较多时,通常在启动类或配置类上统一扫描:

@SpringBootApplication
@MapperScan(basePackageClasses = UserMapper.class)
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
​

示例使用 basePackageClasses 表达扫描范围,实际项目也可以使用明确的包名配置。

不建议写过宽的扫描范围,更不要扫描整个根包后让多个模块的 Mapper 互相混在一起。

两种方式不要混乱

项目可以选择 @Mapper,也可以选择 @MapperScan。

如果两种方式同时使用,通常不会因此直接出错,但团队规范会变得不清楚。

建议在项目里统一一种方式,新增 Mapper 时不需要猜。

Mapper 和 XML 怎么对应

Java Mapper 接口:

public interface UserMapper {

    User findById(Long id);

    int updateName(Long id, String name);
}
​

对应 XML:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.mebugs.demo.mapper.UserMapper">

    <select id="findById" resultType="User">
        SELECT id, user_name, created_at
        FROM sys_user
        WHERE id = #{id}
    </select>

    <update id="updateName">
        UPDATE sys_user
        SET user_name = #{name}
        WHERE id = #{id}
    </update>

</mapper>
​

这里必须对上四个地方:

  • XML 的 namespace 对应 Mapper 接口全限定名
  • XML 的 id 对应接口方法名
  • 方法参数名和 XML 参数表达式能对应
  • XML 文件位于 mapper-locations 能扫描到的路径

任何一个不匹配,都可能出现 Invalid bound statement 或参数找不到。

参数命名要明确

单个简单参数有时可以直接使用参数名,但多参数方法建议显式加 @Param:

int updateName(@Param("id") Long id,
               @Param("name") String name);
​

XML 中对应:

<update id="updateName">
    UPDATE sys_user
    SET user_name = #{name}
    WHERE id = #{id}
</update>
​

如果不加 @Param,编译参数名、MyBatis 参数解析和实际 XML 写法可能对不上。

这个问题常见报错就是找不到某个参数的 getter。

#{} 和 ${} 不要混用

#{} 会使用预编译参数,适合值条件:

WHERE id = #{id}
​

${} 是字符串直接替换,适合少数必须动态替换 SQL 结构的场景,例如经过白名单校验的排序字段。

ORDER BY ${safeOrderColumn}
​

如果把用户输入直接放到 ${},就可能造成 SQL 注入。

排序字段、表名和列名不能直接相信前端传值,应该先映射到后端定义的白名单。

Service 和事务边界

Mapper 只负责数据访问,事务边界通常放在 Service 层。

@Service
public class UserService {

    private final UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Transactional
    public void rename(Long id, String name) {
        userMapper.updateName(id, name);
        // 这里还可以继续写其他数据库操作
    }
}
​

SpringBoot 检测到 DataSource 后,通常会自动配置事务管理器。

@Transactional 能否生效,还取决于调用是否经过 Spring 代理。

如果在同一个类内部通过 this.rename(...) 调用,可能绕过代理,和其他 Spring AOP 注解一样需要留意自调用问题。

事务里只放数据库操作和必要的业务判断,不要把长时间远程调用、文件操作和用户等待塞进事务。

常见启动和运行报错

找不到 Mapper Bean

常见现象:

No qualifying bean of type UserMapper
​

优先检查:

  1. Mapper 是否加了 @Mapper
  2. @MapperScan 的包路径是否正确
  3. 启动类是否位于合理的根包
  4. 是否把接口写成了普通类或放错模块

Invalid bound statement

常见现象:

Invalid bound statement (not found)
​

优先检查:

  • XML 是否被打进最终 classpath
  • mapper-locations 是否写对
  • namespace 是否是接口全限定名
  • id 是否和方法名一致
  • XML 是否有拼写或 XML 语法错误

可以检查构建产物中是否真的存在:

target/classes/mapper/UserMapper.xml
​

不要只在 IDE 里确认文件存在,最终运行的是构建后的 classpath。

字段映射为 null

排查顺序可以是:

  1. 直接执行 SQL,确认数据库确实有值
  2. 查看返回列名和 Java 属性名
  3. 确认驼峰映射配置是否被当前环境加载
  4. 检查 XML 的 resultType 或 resultMap
  5. 对复杂查询使用显式 resultMap

P27 原文配套的驼峰映射问题适合作为本篇排查章节,不单独重复发布。

配置改了但不生效

先确认实际生效的配置文件和环境:

  • 当前激活的 profile
  • 配置文件是否位于 classpath
  • YAML 缩进是否正确
  • 属性前缀是否属于当前 starter 版本
  • 是否有环境变量或配置中心覆盖

配置项名称不能只凭旧文章记忆。

升级 SpringBoot 或 MyBatis Starter 后,先看对应版本文档和自动配置报告。

整合时的检查清单

项目第一次接入时,可以按这张清单走:

  1. SpringBoot、MyBatis Starter 和数据库驱动版本兼容
  2. DataSource 能正常建立连接
  3. Mapper 通过 @Mapper 或 @MapperScan 注册
  4. XML 位于 mapper-locations 扫描路径
  5. namespace、id、参数名一一对应
  6. 读写字段确认驼峰映射或显式 resultMap
  7. Service 层定义事务边界
  8. #{} 用于值参数,${} 只接受白名单结构参数
  9. SQL 日志只在开发或受控排查环境开启
  10. 数据库密码不提交到代码仓库

小结

SpringBoot 整合 MyBatis,核心就是把四个连接点接好:

  • 依赖和版本
  • DataSource 和数据库
  • Mapper 接口与 XML
  • Service 事务边界

最容易出错的地方不是 SQL 本身,而是扫描路径、XML 路径、namespace、方法名、参数名和字段映射。

一句话总结:SpringBoot 负责把组件装起来,MyBatis 负责把方法和 SQL 绑起来,真正稳定的整合还需要清楚的目录、明确的映射和合适的事务边界。

不要把几年前的版本号和配置原样复制到新项目里。先确认项目当前的 SpringBoot、MyBatis Starter 和数据库驱动版本,再按兼容关系落配置,少踩很多旧坑。

更多推荐

章节目录