简介:这份资源面向正在学习或使用Spring Boot进行后端开发的Java开发者,尤其是需要将MyBatis持久层框架接入Spring Boot项目的初中级工程师。内容围绕整合的核心流程展开,涵盖依赖导入、数据源与MyBatis配置、Mapper接口与XML映射文件编写,以及在Service层调用Mapper完成数据库查询的完整实例,帮助读者理解Spring Boot与MyBatis协作的配置要点与常见写法。资源包共1个PDF文件,大小约57KB,以图文与代码片段结合的方式呈现,便于按步骤对照实践。目前已有452人学习下载,适合作为整合MyBatis时的速查参考,也可用于梳理配置项与映射文件的对应关系,快速搭建可运行的持久层结构。
1. 为什么现在还要手写一遍 SpringBoot 整合 MyBatis
很多人第一次接触 SpringBoot 持久层,直接上 MyBatis-Plus 或者 JPA,BaseMapper一继承,CRUD 全有了,确实省事。但真到了排查慢 SQL、调分页插件、写动态 SQL、看事务为什么没回滚的时候,底层的SqlSessionFactoryBean、MapperScannerConfigurer、DataSourceTransactionManager这三件套没搞明白,就只能靠猜。这份实例用的是 SpringBoot 1.3.0.RELEASE + MyBatis 3.2.7 + mybatis-spring 1.2.2 的组合,版本偏老,但配置骨架和今天 SpringBoot 2.x/3.x 整合 MyBatis 的思路完全一致,只是注解和 starter 换了个壳。适合两类人:一是刚学完 SSM 想过渡到 SpringBoot 的,二是用了几年 MyBatis-Plus 却说不清@MapperScan背后干了什么的。把这份配置从头搭一遍,比看十篇面试题管用。
2. pom.xml 依赖坐标与数据源选型
2.1 为什么不用官方 mybatis-spring-boot-starter
这份实例走的是「手动装配」路线,没有引入mybatis-spring-boot-starter,而是自己声明mybatis、mybatis-spring、spring-jdbc三个依赖,再用@Configuration类把SqlSessionFactoryBean和MapperScannerConfigurer手动注册成 Bean。这么做的价值在于:你能清楚看到 starter 到底帮你自动配置了什么。starter 内部无非就是MybatisAutoConfiguration里干了同样的事,一旦自动配置失效(比如多数据源场景),你还是得回到手动装配。
依赖清单里几个坐标值得单独说:
| 依赖 | 版本 | 作用 | 注意点 |
|---|---|---|---|
| spring-boot-starter-parent | 1.3.0.RELEASE | 统一版本管理 | 老版本,新项目建议 2.7.x 或 3.x |
| mybatis | 3.2.7 | MyBatis 核心 | 与 mybatis-spring 版本要匹配 |
| mybatis-spring | 1.2.2 | 桥接 Spring | 1.2.x 对应 MyBatis 3.2.x |
| mysql-connector-java | 6.0.5 | MySQL 驱动 | 6.x 驱动类名带cj |
| c3p0 | 0.9.5.2 | 连接池 | 实例里声明了但实际用 Druid |
| spring-jdbc | 由 parent 管理 | 事务与 JdbcTemplate | 事务管理器依赖它 |
这里有个容易踩的坑:实例的 pom 里同时声明了 c3p0 和后面代码里用的 Druid,但 pom 片段里并没有列出 Druid 依赖。实际跑起来要么补上com.alibaba:druid,要么把DBConfig里的DruidDataSource换成ComboPooledDataSource。我一般会统一成 Druid,因为它的监控和maxActive参数更好调。
2.2 依赖引入的完整写法
<properties> <start-class>com.us.Application</start-class> <mybatis.version>3.2.7</mybatis.version> <mybatis-spring.version>1.2.2</mybatis-spring.version> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties> <dependencies> <!-- Web 能力,Controller 层需要 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MySQL 驱动,6.x 版本驱动类为 com.mysql.cj.jdbc.Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.5</version> </dependency> <!-- MyBatis 核心与 Spring 桥接,版本必须成对 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <!-- 事务管理器依赖,由 parent 统一版本 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> </dependencies><start-class>是 SpringBoot 打包可执行 jar 时指定的入口类,用spring-boot-maven-plugin时它会读这个属性。maven.compiler.source/target锁 1.8,是因为 MyBatis 3.2.7 和 SpringBoot 1.3 在更高 JDK 上编译容易出字节码兼容问题。参数上唯一需要你改的是mybatis.version和mybatis-spring.version,两者对应关系错了会在启动时报NoSuchMethodError,这是整合阶段最高频的报错之一。
3. 数据源、SqlSessionFactory 与 Mapper 扫描的装配
3.1 DBConfig:用 Environment 读外部配置
数据源不写死在代码里,而是通过Environment从application.properties读,这样打包后改配置不用重新编译。
@Configuration public class DBConfig { @Autowired private Environment env; @Bean(name = "dataSource") public DruidDataSource dataSource() { // checkNotNull 保证 url/username 缺失时启动即失败,而不是运行到查询才报错 final String url = Preconditions.checkNotNull(env.getProperty("ms.db.url")); final String username = Preconditions.checkNotNull(env.getProperty("ms.db.username")); final String password = env.getProperty("ms.db.password"); // 默认 200,配置里覆盖为 500 final int maxActive = Integer.parseInt(env.getProperty("ms.db.maxActive", "200")); DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setMaxActive(maxActive); return dataSource; } }Preconditions.checkNotNull来自 Guava,作用是「快速失败」:url 或 username 没配,应用启动阶段就抛NullPointerException,而不是等第一个请求进来才报连接错误。maxActive是连接池最大活跃连接数,实例给到 500,这个值要结合数据库max_connections一起看,盲目调大会把数据库连接打满。@Bean(name = "dataSource")显式命名,是因为后面SqlSessionFactoryBean和事务管理器都要按名字注入,多数据源时这个名字就是区分依据。
3.2 application.properties 里的连接串参数
ms.db.url=jdbc:mysql://localhost:3306/dev?prepStmtCacheSize=517&cachePrepStmts=true&autoReconnect=true&characterEncoding=utf-8&allowMultiQueries=true ms.db.username=root ms.db.password=admin ms.db.maxActive=500连接串里几个参数值得逐个拆:cachePrepStmts=true开启预编译语句缓存,配合prepStmtCacheSize=517设定缓存条数,对高频重复 SQL 提升明显;autoReconnect=true让连接断开后自动重连,但要注意它和连接池的testOnBorrow有职责重叠,生产环境更推荐靠连接池的保活检测;characterEncoding=utf-8防止中文乱码;allowMultiQueries=true允许一次执行多条 SQL,批量操作时有用,但也放大了 SQL 注入的风险面,非必要不开。
提示:
ms.db.这个前缀是自定义的,不是 SpringBoot 约定的spring.datasource.。用自定义前缀的好处是不会和自动配置冲突,坏处是自动配置彻底失效,所有 Bean 都得自己声明。
3.3 MyBatisConfig:SqlSessionFactoryBean 扫描 mapper.xml
@Configuration public class MyBatisConfig { @Autowired private DataSource dataSource; @Bean(name = "sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory(ApplicationContext applicationContext) throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); // classpath*: 会扫描所有 jar 和目录下的 mapper/*.xml sessionFactory.setMapperLocations(applicationContext.getResources("classpath*:mapper/*.xml")); return sessionFactory; } }SqlSessionFactoryBean是 MyBatis 和 Spring 之间的核心桥梁,它负责把DataSource、mapper.xml 位置、类型别名、插件等组装成一个SqlSessionFactory。classpath*:前缀里的星号很关键:单个classpath:只会在第一个匹配的 classpath 根下找,classpath*:才会遍历所有 classpath 条目,多模块项目里 mapper.xml 分散在不同 jar 时,必须用后者。setMapperLocations接收的是Resource[],所以用getResources而不是getResource。
3.4 MyBatisScannerConfig:MapperScannerConfigurer 扫描 dao 接口
@Configuration public class MyBatisScannerConfig { @Bean public MapperScannerConfigurer MapperScannerConfigurer() { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); // 扫描 com.example 下任意子包的 dao 包 mapperScannerConfigurer.setBasePackage("com.example.*.dao"); // 指定使用哪个 SqlSessionFactory,多数据源时必须显式指定 mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory"); return mapperScannerConfigurer; } }MapperScannerConfigurer干的事是:扫描指定包下的接口,为每个接口生成动态代理 Bean 注册到容器,这样@Autowired UserDao才能注入成功。basePackage支持*通配,com.example.*.dao能匹配com.example.base.dao、com.example.user.dao这类结构。setSqlSessionFactoryBeanName用字符串而不是直接注入对象,是为了避免和SqlSessionFactoryBean产生循环依赖——这是官方推荐写法,也是很多人整合时启动报循环依赖的根因。
注意:实例特别强调
MyBatisScannerConfig和MyBatisConfig不要写在一个类里。原因是MapperScannerConfigurer是BeanDefinitionRegistryPostProcessor,执行时机极早,如果和SqlSessionFactoryBean同处一个配置类,会导致dataSource还没初始化就被引用。
3.5 TransactionConfig:事务管理器不能省
@Configuration public class TransactionConfig implements TransactionManagementConfigurer { @Autowired private DataSource dataSource; @Bean(name = "transactionManager") @Override public PlatformTransactionManager annotationDrivenTransactionManager() { return new DataSourceTransactionManager(dataSource); } }实现TransactionManagementConfigurer接口并重写annotationDrivenTransactionManager,等于告诉 Spring「这就是默认事务管理器」,之后@Transactional注解才会生效。返回DataSourceTransactionManager是因为我们只有单一数据源;多数据源场景下每个数据源配一个事务管理器,再用@Transactional(transactionManager = "xxx")指定。少了这个类,@Transactional会静默失效——不报错,但回滚不生效,这是最隐蔽的坑。
4. Dao、Service、Controller 三层串起来跑通
4.1 Dao 接口与 mapper.xml 的对应关系
public interface UserDao { // 方法名必须与 mapper.xml 中 <select id="..."> 一致 public List<User> getList(Map<String, Object> map); }Dao 接口本身不需要任何注解,MapperScannerConfigurer会为它生成代理。方法名getList必须和mapper.xml里<select id="getList">的 id 完全一致,命名空间则对应接口全限定名。参数用Map<String,Object>是为了接收 Controller 传来的任意查询条件,动态 SQL 里用#{key}取值。如果参数是单个对象,MyBatis 会按属性名匹配;多个参数则要用@Param注解,否则只能按arg0/param1这种默认名取,可读性差。
4.2 Service 层注入 Dao
@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; public Object getList(Map<String, Object> map) { return userDao.getList(map); } }@Service把实现类注册为 Bean,@Autowired按类型注入UserDao代理。这里没有加@Transactional,因为纯查询不需要事务;如果是写操作,方法上要加@Transactional,并且注意默认只对RuntimeException回滚,受检异常要显式写rollbackFor = Exception.class。
4.3 Controller 暴露 HTTP 接口
@Controller @RequestMapping(value = "/users") public class UserController { @Autowired private UserService userService; @RequestMapping(method = RequestMethod.GET, produces = "application/json;charset=UTF-8") @ResponseBody public ResponseEntity<?> list(HttpServletRequest request) { // 把 request 参数统一转成 Map 传给 Service Map<String, Object> map = CommonUtil.getParameterMap(request); return new ResponseEntity<Object>(userService.getList(map), HttpStatus.OK); } }@Controller+@ResponseBody等价于@RestController,老版本里常这么写。produces = "application/json;charset=UTF-8"解决返回中文乱码。CommonUtil.getParameterMap是自定义工具,把request.getParameterMap()转成普通Map,因为原生Map<String,String[]>在 MyBatis 里取值不方便。访问路径就是localhost:8099/users?id=99,端口由application.properties里的server.port决定。
4.4 启动类扫描配置包
@ComponentScan(basePackages = "com.example") @SpringBootApplication public class Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); } public static void main(String[] args) throws Exception { ApplicationContext ctx = SpringApplication.run(Application.class, args); // 启动后打印所有 Bean 名,方便确认配置类是否被扫描到 String[] beanNames = ctx.getBeanDefinitionNames(); Arrays.sort(beanNames); for (String beanName : beanNames) { System.out.println(beanName); } } }@ComponentScan(basePackages = "com.example")保证DBConfig、MyBatisConfig、MyBatisScannerConfig、TransactionConfig都被扫描到。继承SpringBootServletInitializer是为了支持打成 war 部署到外部容器。main方法里遍历打印 Bean 名是个实用调试手段:启动日志里搜sqlSessionFactory、dataSource、transactionManager,能立刻确认装配是否成功,比等到请求报错再回头查快得多。
5. 整合后的排错清单与进阶调优
5.1 四类高频启动/运行报错对照
| 报错信息 | 根因 | 处理方式 |
|---|---|---|
NoSuchMethodError: org.mybatis.spring.SqlSessionFactoryBean | mybatis 与 mybatis-spring 版本不匹配 | 按官方对应表锁版本,3.2.x 配 1.2.x |
Invalid bound statement (not found) | mapper.xml 没被扫描到或 id 不匹配 | 检查classpath*:mapper/*.xml路径与 id 名 |
@Transactional不回滚 | 缺TransactionManagementConfigurer或异常类型不符 | 补事务配置类,写操作加rollbackFor |
| 启动报循环依赖 | MapperScannerConfigurer与SqlSessionFactoryBean同类 | 拆成两个@Configuration类 |
Invalid bound statement是整合后最常遇到的运行期错误,八成是 mapper.xml 的namespace写错,或者 xml 文件放在了resources下但目录名不是mapper。用classpath*:mapper/*.xml时,xml 必须位于某个 classpath 根下的mapper目录,src/main/resources/mapper/UserMapper.xml是标准位置。
5.2 打开 MyBatis 的 SQL 日志
排查慢 SQL 和参数绑定问题,第一步是把执行的 SQL 打出来。在application.properties里加:
# 指定 dao 包下的日志级别为 DEBUG,MyBatis 会打印 SQL、参数、结果行数 logging.level.com.example.base.dao=DEBUG日志级别设到DEBUG后,控制台会输出==> Preparing:、==> Parameters:、<== Total:三段,分别对应预编译 SQL、绑定参数、返回行数。如果只看到 Preparing 没有 Parameters,说明参数没传进去,回头查Map的 key 和#{}里的名字是否一致。生产环境别长期开 DEBUG,日志量很大,建议只在排查时临时打开。
5.3 分页与拦截器的接入位置
实例本身没带分页,但整合完成后接分页插件是顺理成章的一步。MyBatis 的分页靠Interceptor实现,注册位置就在SqlSessionFactoryBean上:
@Bean(name = "sqlSessionFactory") public SqlSessionFactoryBean sqlSessionFactory(ApplicationContext ctx) throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setMapperLocations(ctx.getResources("classpath*:mapper/*.xml")); // 注册分页插件,PageHelper 是常见选择 PageInterceptor pageInterceptor = new PageInterceptor(); Properties props = new Properties(); props.setProperty("helperDialect", "mysql"); props.setProperty("reasonable", "true"); pageInterceptor.setProperties(props); sessionFactory.setPlugins(pageInterceptor); return sessionFactory; }setPlugins接收Interceptor数组,可以同时挂分页、SQL 打印、乐观锁等多个拦截器,执行顺序按数组顺序。helperDialect指定数据库方言,reasonable为 true 时页码越界会自动纠正到合法范围。拦截器本质是 MyBatis 四级插件链上的一环,理解它挂在SqlSessionFactoryBean上而不是 Spring 容器里,是区分「MyBatis 配置」和「Spring 配置」的关键分界线。
本文还有配套的精品资源,点击获取