SpringBoot手动整合MyBatis:SqlSessionFactory与MapperScannerConfigurer配置详解
2026/9/23 5:03:36 网站建设 项目流程

简介:这份资源面向正在学习或使用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、看事务为什么没回滚的时候,底层的SqlSessionFactoryBeanMapperScannerConfigurerDataSourceTransactionManager这三件套没搞明白,就只能靠猜。这份实例用的是 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,而是自己声明mybatismybatis-springspring-jdbc三个依赖,再用@Configuration类把SqlSessionFactoryBeanMapperScannerConfigurer手动注册成 Bean。这么做的价值在于:你能清楚看到 starter 到底帮你自动配置了什么。starter 内部无非就是MybatisAutoConfiguration里干了同样的事,一旦自动配置失效(比如多数据源场景),你还是得回到手动装配。

依赖清单里几个坐标值得单独说:

依赖版本作用注意点
spring-boot-starter-parent1.3.0.RELEASE统一版本管理老版本,新项目建议 2.7.x 或 3.x
mybatis3.2.7MyBatis 核心与 mybatis-spring 版本要匹配
mybatis-spring1.2.2桥接 Spring1.2.x 对应 MyBatis 3.2.x
mysql-connector-java6.0.5MySQL 驱动6.x 驱动类名带cj
c3p00.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.versionmybatis-spring.version,两者对应关系错了会在启动时报NoSuchMethodError,这是整合阶段最高频的报错之一。

3. 数据源、SqlSessionFactory 与 Mapper 扫描的装配

3.1 DBConfig:用 Environment 读外部配置

数据源不写死在代码里,而是通过Environmentapplication.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 位置、类型别名、插件等组装成一个SqlSessionFactoryclasspath*:前缀里的星号很关键:单个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.daocom.example.user.dao这类结构。setSqlSessionFactoryBeanName用字符串而不是直接注入对象,是为了避免和SqlSessionFactoryBean产生循环依赖——这是官方推荐写法,也是很多人整合时启动报循环依赖的根因。

注意:实例特别强调MyBatisScannerConfigMyBatisConfig不要写在一个类里。原因是MapperScannerConfigurerBeanDefinitionRegistryPostProcessor,执行时机极早,如果和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")保证DBConfigMyBatisConfigMyBatisScannerConfigTransactionConfig都被扫描到。继承SpringBootServletInitializer是为了支持打成 war 部署到外部容器。main方法里遍历打印 Bean 名是个实用调试手段:启动日志里搜sqlSessionFactorydataSourcetransactionManager,能立刻确认装配是否成功,比等到请求报错再回头查快得多。

5. 整合后的排错清单与进阶调优

5.1 四类高频启动/运行报错对照

报错信息根因处理方式
NoSuchMethodError: org.mybatis.spring.SqlSessionFactoryBeanmybatis 与 mybatis-spring 版本不匹配按官方对应表锁版本,3.2.x 配 1.2.x
Invalid bound statement (not found)mapper.xml 没被扫描到或 id 不匹配检查classpath*:mapper/*.xml路径与 id 名
@Transactional不回滚TransactionManagementConfigurer或异常类型不符补事务配置类,写操作加rollbackFor
启动报循环依赖MapperScannerConfigurerSqlSessionFactoryBean同类拆成两个@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 配置」的关键分界线。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询