从零搭SpringBoot+MyBatis,我差点被这些坑劝退
版本冲突是最常见的坑,而且它总是悄无声息地潜伏在pom.xml里。如果你直接复制网上三年前的教程,大概率会遇到DataSource配置失败、Mapper扫描不到、分页插件报错等问题。我经历过一次项目重构,花了整整一个下午排查,结果只是spring-boot-starter-parent和mybatis-spring-boot-starter的版本不匹配。现在官方推荐的做法是:SpringBoot 2.x对应mybatis-spring-boot-starter 2.x,SpringBoot 3.x对应3.x,千万别混用。另外,数据库驱动也得跟上——mysql-connector-java在8.0以后改名为mysql-connector-j,com.mysql.cj.jdbc.Driver取代了旧驱动,很多老文章还在用com.mysql.jdbc.Driver,驱动类名写错直接导致启动失败,而且报错信息极其隐晦,只提示“cannot load driver class”。
依赖引入的陷阱:多一个少一个都是灾难
最基本的依赖清单:spring-boot-starter-web、mybatis-spring-boot-starter、数据库驱动(比如mysql-connector-j)、连接池(HikariCP默认自带),外加一个spring-boot-starter-test。很多人会忘记加spring-boot-starter-jdbc,其实mybatis-spring-boot-starter已经传递依赖了DataSource相关类,但如果你要手动配置事务管理器或JdbcTemplate,那个依赖可能缺了。另一个高频失误是忘记排除旧版驱动中的Logging冲突,比如Slf4J和Logback的实现冲突,启动时控制台疯狂输出“Failed to load class org.slf4j.impl.StaticLoggerBinder”。解决办法是在依赖里排除冲突项,或者统一使用Spring Boot指定的日志版本。如果你用Lombok,记得加上annotationProcessorPaths配置,否则编译时无法生成getter/setter,SQL查询结果映射全是null。
application.yml配置:看似简单,实则暗藏玄机
数据源配置是第一步,也是最容易出错的地方。新版本Spring Boot要求spring.datasource.url而不是jdbc-url,很多新手从别的框架转过来会写成jdbc-url,导致HikariCP找不到连接。标准写法:
spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver
MyBatis的专属配置:
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.stdout.StdOutImpl
这里有个经典踩坑:map-underscore-to-camel-case明明设为true,但查询结果中user_name字段还是映射不到userName属性。原因在于MyBatis的configuration块需要放在mybatis.configuration下,而有些教程写成mybatis.configuration.map-underscore-to-camel-case,这没问题;但如果你不小心写成了mybatis.configuration-properties(注意是复数),配置就完全无效。而且,当你使用PageHelper或MyBatis-Plus时,它们的配置项与原生配置可能互相覆盖,建议分开定义。
Mapper扫描:@MapperScan到底放哪?包路径怎么写?
有两种方式注册Mapper:一是每个Mapper接口上加@Mapper注解;二是在任意@Configuration类上加@MapperScan("com.example.demo.mapper")。第一种方式繁琐且容易遗漏,第二种更推荐。但坑在于扫描路径必须精确到Mapper接口所在的包,如果你扫描了父包但子包有内部类或无用接口,可能会注入非预期的Bean。更隐蔽的坑是:如果你同时使用了@Mapper和@MapperScan,且扫描范围不包含某个Mapper,那个Mapper依然会生效(因为@Mapper单独注册了),但如果你扫描范围包含了多个包,且Mapper接口跨越多个模块,可能会引发Bean重复定义的错误。另外,@MapperScan必须放在Spring Boot主类或配置类上,而且确保该类能被@ComponentScan扫描到,否则扫描不生效。有个同事把@MapperScan写在一个普通的Service类上,结果项目启动后报“Invalid bound statement (not found)”,排查了半天。
XML映射文件:namespace写错等于白写
Mapper XML文件放在classpath:mapper/下,命名规范:UserMapper.xml对应UserMapper接口。XML的namespace必须与Mapper接口的全限定名完全一致,否则MyBatis在运行时找不到SQL语句。很多人复制粘贴时忘记改包名,结果不断报“Invalid bound statement (not found)”。此外,resultMap中的column和property要注意大小写匹配,开启驼峰映射后,column的user_name会自动转成userName,但如果你手动定义的resultMap覆盖了默认映射,驼峰转换就不再生效,必须手动指定property。另一个常见问题:使用#{}和${}的场景混淆。#{}会预编译并防止SQL注入,适合传递参数;${}直接拼接,适合动态表名或列名。但如果你用${}传递用户输入的参数,就是给自己挖坑,比如order by ${sortColumn},攻击者可以通过sortColumn传入恶意SQL片段。我在生产环境遇到过有人传1;DROP TABLE users --,还好用了#{}仅做参数,不直接拼接。
事务管理:@Transactional不生效的几种情况
Spring声明式事务基于AOP,只有被Spring管理的Bean上的@Transactional才有效。如果你在Controller的方法上直接加@Transactional,但Controller本身没有被代理(比如不是Spring Bean,或者方法被final修饰),事务就不会开启。另一个常见情况:在同一个类中,方法A调用方法B,B上有@Transactional,此时事务不会生效,因为这是内部调用,没有经过代理对象。解决方案是:要么把B方法放到另一个Service中,要么通过AopContext.currentProxy()获取代理对象。还有,事务的管理默认只对RuntimeException回滚,如果抛的是Checked Exception(比如IOException),事务不会回滚。需要指定@Transactional(rollbackFor = Exception.class)。多数据源场景下,@Transactional默认使用主数据源的事务管理器,如果你操作了从库,事务会跨数据源失效。记得为每个数据源配置独立的PlatformTransactionManager,并在@Transactional中指定transactionManager名称。
分页插件PageHelper:版本必须和MyBatis对齐
PageHelper是常用的分页插件,但它的版本要求非常严格。mybatis-spring-boot-starter 2.x必须配合PageHelper 5.x,且需要对page-helper-spring-boot-starter的版本号格外留意。如果你引入的是PageHelper 4.x,启动时可能出现“java.lang.AbstractMethodError”或者分页参数无法传递。更隐蔽的是:在整合PageHelper后,如果你在Mapper方法里手动写了select count()的SQL,并且使用了PageHelper.startPage(),那么PageHelper会尝试重写你的SQL,但如果你SQL中有复杂子查询或UNION,重写可能失败,导致count查询异常。解决办法:使用PageHelper.startPage(pageNum, pageSize)后,立即跟着一个MyBatis查询,中间不能有其他数据库操作。另外,分页结果一定要用PageInfo包装,否则你可能拿到的是原始List,丢失了总记录数等分页信息。
多数据源:动态切换的坑与解决方案
当项目需要连接多个数据库(比如主库写、从库读),动态数据源是常见方案。但实现动态数据源本质是继承AbstractRoutingDataSource,并重写determineCurrentLookupKey方法。这个过程中有几点容易踩坑:第一,必须确保在数据库操作之前通过ThreadLocal设置好当前要用的数据源key,而且事务开启后就不能再切换了(因为事务绑定了一个Connection)。第二,@Transactional注解下的方法,如果需要在方法内部切换数据源,必须将事务传播行为设为NOT_SUPPORTED或REQUIRES_NEW,否则Connection会被复用,导致用的仍是旧数据源。第三,连接池配置要分别给每个数据源设置不同的HikariCP配置,比如连接数、超时时间,不能共用。我见过一个生产事故:主从库用了同一个连接池配置,结果从库最大连接数被主库流量占满,导致从库查询超时。
生产环境调优:日志、连接池、缓存与批量操作
上线后,第一件事:关闭MyBatis的SQL日志输出。log-impl设为STDOUT_LOGGING会让所有SQL打印到控制台,在高并发下直接拖垮性能。应该改用logback或log4j2按级别过滤,或者只在debug模式下开启。连接池HikariCP配置:maximum-pool-size建议根据服务器CPU核心数和业务类型调节,一般10~20足够;minimum-idle设置5左右,避免频繁创建连接。开启cache-enabled=true可以让MyBatis使用二级缓存,但注意脏读问题,比如一个查询把结果缓存了,另一个更新操作却没有清缓存,下次读取的还是旧数据。建议在Mapper XML里用<cache>标签配置缓存策略,或者干脆在业务层用Redis做缓存。还有批量插入:如果你的业务需要一次插入几千条数据,别用for循环单条插,要使用SqlSession的批量模式,或者用MyBatis-Plus的saveBatch方法。批量模式性能提升数十倍,因为减少了与数据库的网络往返次数。
总结:没有亲手踩过的坑,都不算真正掌握
回顾整个整合过程,最让人崩溃的往往不是复杂的逻辑,而是那些看似不起眼的配置细节。比如驱动类名的大小写、XML namespace的拼写、@Transactional的内部调用失效、PageHelper的版本冲突、驼峰映射的配置位置……它们每一个都可能让你浪费半天甚至一天时间。我建议你在新项目中,先把一个最简单的单表CRUD跑通,再逐步增加分页、多数据源、缓存等功能。每加一个功能,就进行一次完整的启动测试,不要等所有代码写完再debug。另外,保持依赖版本的一致性非常关键,推荐使用Spring Initializr生成项目骨架,或者参考官方文档的版本对应表。记住一点:任何看似“诡异”的错误,都可以在控制台堆栈信息的第三行找到答案——学会阅读异常堆栈,比盲目百度更有效。如果你现在正准备把一个旧项目从SSM迁移到SpringBoot+MyBatis,那就从我最开始提到的版本冲突开始排查吧,最好把pom.xml里所有第三方依赖的版本都检查一遍,确保没有遗留的旧版jar。这些坑,我替你先踩过了,希望能帮你节省宝贵的开发时间。