Spring Boot 多模块项目 Mapper 扫描无效的真实原因与解决方案
各位朋友,大家好!今天我们来深入探讨一个在 Spring Boot 多模块项目中经常遇到的问题:Mapper 扫描无效。这个问题看似简单,但其背后的原因却往往比较复杂,涉及到 Spring 容器、类加载机制、Maven 构建等多个方面。我们将从问题现象出发,逐步分析各种可能的原因,并提供详细的解决方案,帮助大家彻底解决这个问题。
一、问题现象:明明配置了 Mapper 扫描,却无法注入 Bean
在 Spring Boot 多模块项目中,我们通常会将数据访问层(DAO)的接口定义在单独的模块中,然后在业务逻辑层(Service)的模块中引入并使用这些接口。为了让 Spring 容器能够识别并管理这些 Mapper 接口,我们需要配置 Mapper 扫描。
然而,有时候我们会发现,即使按照官方文档配置了 Mapper 扫描,仍然无法将 Mapper 接口注入到 Service 层,导致 NullPointerException 或者其他类似的错误。这通常表现为以下几种情况:
- 启动时没有报错,但是运行时报
NoSuchBeanDefinitionException。 - 启动时报错,提示找不到 Mapper 接口的 Bean 定义。
- Mapper 接口可以被扫描到,但是执行 SQL 时出现问题,例如无法找到对应的 XML 映射文件。
二、常见原因分析与解决方案
导致 Mapper 扫描无效的原因有很多,下面我们将逐一分析,并给出相应的解决方案。
1. Maven 依赖问题:未正确引入 Mapper 接口所在模块
这是最常见的原因之一。如果 Service 模块没有正确引入 Mapper 接口所在的模块,那么 Spring 容器自然无法扫描到这些接口。
- 原因分析: Maven 的依赖管理是基于传递性的。如果 Service 模块没有直接依赖 Mapper 接口所在的模块,即使该模块被其他模块依赖,Spring 容器也无法扫描到其中的 Mapper 接口。
-
解决方案: 确保 Service 模块的
pom.xml文件中包含了 Mapper 接口所在模块的依赖。<dependencies> <!-- 其他依赖 --> <dependency> <groupId>com.example</groupId> <artifactId>mapper-module</artifactId> <version>1.0.0</version> </dependency> </dependencies>
2. Mapper 扫描路径配置错误
Spring Boot 提供了 @MapperScan 注解来指定 Mapper 接口的扫描路径。如果扫描路径配置错误,Spring 容器就无法找到 Mapper 接口。
- 原因分析:
@MapperScan注解的basePackages属性用于指定要扫描的包路径。如果basePackages属性配置的路径与 Mapper 接口实际所在的包路径不匹配,或者没有配置该属性,就会导致扫描失败。 -
解决方案: 检查
@MapperScan注解的配置,确保basePackages属性指向 Mapper 接口所在的正确包路径。@Configuration @MapperScan("com.example.mapper") // 确保此路径指向 Mapper 接口所在的包 public class MyBatisConfig { }或者,如果你的 Mapper 接口都在同一个包下,可以直接使用
*通配符。@Configuration @MapperScan("com.example.*.mapper") // 扫描 com.example 下所有子模块的 mapper 包 public class MyBatisConfig { }注意:
@MapperScan注解应该放在 Spring Boot 应用的启动类或者配置类上。
3. Mapper 接口未被 Spring 容器识别
即使扫描路径配置正确,如果 Mapper 接口本身没有被 Spring 容器识别为 Bean,仍然无法注入。
- 原因分析: MyBatis-Spring 提供了多种方式来将 Mapper 接口注册到 Spring 容器中。如果这些方式没有生效,Mapper 接口就无法被识别。
-
解决方案:
-
确保 Mapper 接口使用
@Mapper注解: MyBatis-Spring 会自动扫描带有@Mapper注解的接口,并将其注册为 Bean。这是最常用的方式,强烈推荐使用。@Mapper public interface UserMapper { User selectById(Long id); } -
使用
MapperFactoryBean手动注册: 如果由于某些原因无法使用@Mapper注解,可以手动使用MapperFactoryBean来注册 Mapper 接口。@Configuration public class MyBatisConfig { @Bean public MapperFactoryBean<UserMapper> userMapper(SqlSessionFactory sqlSessionFactory) { MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class); factoryBean.setSqlSessionFactory(sqlSessionFactory); return factoryBean; } } -
使用
SqlSessionTemplate手动获取: 在某些特殊情况下,可能需要手动使用SqlSessionTemplate来获取 Mapper 接口的实例。@Autowired private SqlSessionTemplate sqlSessionTemplate; public User getUserById(Long id) { UserMapper userMapper = sqlSessionTemplate.getMapper(UserMapper.class); return userMapper.selectById(id); }
-
4. MyBatis 配置问题:XML 映射文件缺失或配置错误
如果 Mapper 接口可以被扫描到,但是执行 SQL 时出现问题,例如无法找到对应的 XML 映射文件,可能是 MyBatis 的配置问题。
- 原因分析: MyBatis 需要 XML 映射文件来定义 SQL 语句和结果映射。如果 XML 映射文件缺失、路径配置错误或者内容有误,就会导致执行 SQL 时出现问题。
-
解决方案:
- 确保 XML 映射文件存在: 检查 XML 映射文件是否真实存在,并且位于正确的路径下。通常情况下,XML 映射文件应该与 Mapper 接口位于相同的包路径下,并且文件名与 Mapper 接口名相同。例如,如果 Mapper 接口名为
UserMapper.java,那么 XML 映射文件应该名为UserMapper.xml。 -
配置
mybatis.mapper-locations属性: 在application.properties或者application.yml文件中,使用mybatis.mapper-locations属性来指定 XML 映射文件的位置。mybatis.mapper-locations=classpath:mapper/*.xml注意:
classpath:前缀表示从类路径下查找文件。可以使用通配符来指定多个文件或者目录。 -
检查 XML 映射文件的内容: 仔细检查 XML 映射文件的内容,确保 SQL 语句和结果映射配置正确。特别是
namespace属性,必须与 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.mapper.UserMapper"> <select id="selectById" resultType="com.example.model.User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>
- 确保 XML 映射文件存在: 检查 XML 映射文件是否真实存在,并且位于正确的路径下。通常情况下,XML 映射文件应该与 Mapper 接口位于相同的包路径下,并且文件名与 Mapper 接口名相同。例如,如果 Mapper 接口名为
5. 类加载器问题:多模块项目中的类加载隔离
在多模块项目中,Maven 会使用不同的类加载器来加载不同模块中的类。如果 Mapper 接口和 Service 模块由不同的类加载器加载,Spring 容器可能无法正确识别 Mapper 接口。
- 原因分析: Java 的类加载器采用双亲委派模型。当一个类加载器收到类加载请求时,它会首先委派给父类加载器去加载。如果父类加载器无法加载,才会尝试自己加载。在多模块项目中,如果 Mapper 接口和 Service 模块由不同的类加载器加载,即使它们位于相同的包路径下,Spring 容器也可能将它们视为不同的类。
-
解决方案: 解决类加载器问题的方法比较复杂,通常需要调整 Maven 的构建配置,确保 Mapper 接口和 Service 模块由同一个类加载器加载。
-
使用
maven-shade-plugin打包:maven-shade-plugin可以将所有依赖的类打包到一个 JAR 文件中,从而避免类加载器隔离的问题。<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> -
调整 Maven 依赖范围: 可以尝试调整 Maven 依赖的范围,例如将
provided范围改为compile范围,或者将runtime范围改为compile范围。注意: 调整 Maven 依赖范围可能会影响项目的构建和部署,需要谨慎操作。
-
6. Spring Boot 版本兼容性问题
在某些情况下,Spring Boot 版本之间的兼容性问题也可能导致 Mapper 扫描无效。
- 原因分析: Spring Boot 和 MyBatis-Spring 的版本之间存在一定的兼容性要求。如果使用的版本不兼容,可能会导致 Mapper 扫描失败。
- 解决方案: 查阅 Spring Boot 和 MyBatis-Spring 的官方文档,确保使用的版本兼容。可以尝试升级或者降级 Spring Boot 或者 MyBatis-Spring 的版本,看看是否能够解决问题。
7. 其他配置问题:例如事务管理、数据源配置等
除了上述常见原因之外,还有一些其他的配置问题也可能导致 Mapper 扫描无效。例如,事务管理配置错误、数据源配置错误等。
- 原因分析: 如果事务管理配置错误,可能会导致 MyBatis 无法正确获取数据库连接。如果数据源配置错误,可能会导致 MyBatis 无法连接到数据库。
- 解决方案: 仔细检查事务管理和数据源的配置,确保配置正确。可以尝试使用 Spring Boot 提供的默认配置,或者手动配置事务管理器和数据源。
三、问题排查步骤
当遇到 Mapper 扫描无效的问题时,可以按照以下步骤进行排查:
| 步骤 | 内容 |
|---|---|
| 1 | 确认 Maven 依赖: 检查 Service 模块的 pom.xml 文件,确保包含了 Mapper 接口所在模块的依赖。 |
| 2 | 检查 Mapper 扫描路径: 检查 @MapperScan 注解的配置,确保 basePackages 属性指向 Mapper 接口所在的正确包路径。 |
| 3 | 确认 Mapper 接口是否被识别: 检查 Mapper 接口是否使用了 @Mapper 注解,或者是否通过 MapperFactoryBean 手动注册。 |
| 4 | 检查 XML 映射文件: 确保 XML 映射文件存在,并且位于正确的路径下。检查 XML 映射文件的内容,确保 SQL 语句和结果映射配置正确。 |
| 5 | 查看日志信息: 查看 Spring Boot 的启动日志,查找是否有相关的错误信息。例如,是否有 NoSuchBeanDefinitionException 异常,或者是否有 MyBatis 相关的错误信息。 |
| 6 | Debug 调试: 使用 Debug 工具,逐步跟踪 Spring 容器的启动过程,查看 Mapper 接口是如何被扫描和注册的。 |
| 7 | 调整 Spring Boot 版本: 尝试升级或者降级 Spring Boot 或者 MyBatis-Spring 的版本,看看是否能够解决问题。 |
| 8 | 检查其他配置: 仔细检查事务管理和数据源的配置,确保配置正确。 |
四、代码示例:一个完整的 Spring Boot 多模块项目示例
为了更好地理解上述内容,我们提供一个完整的 Spring Boot 多模块项目示例,展示如何正确配置 Mapper 扫描。
1. 项目结构:
my-project/
├── pom.xml (父模块)
├── common-module/ (通用模块,包含实体类)
│ └── src/main/java/com/example/common/model/User.java
│ └── pom.xml
├── mapper-module/ (Mapper 接口模块)
│ └── src/main/java/com/example/mapper/UserMapper.java
│ └── src/main/resources/mapper/UserMapper.xml
│ └── pom.xml
├── service-module/ (Service 模块)
│ └── src/main/java/com/example/service/UserService.java
│ └── pom.xml
└── application/ (Spring Boot 应用模块)
└── src/main/java/com/example/Application.java
└── src/main/resources/application.properties
└── pom.xml
2. 父模块 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>common-module</module>
<module>mapper-module</module>
<module>service-module</module>
<module>application</module>
</modules>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.7.18</spring-boot.version>
<mybatis-spring-boot.version>2.2.2</mybatis-spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>common-module</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>mapper-module</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
3. common-module/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>common-module</artifactId>
<packaging>jar</packaging>
</project>
4. common-module/src/main/java/com/example/common/model/User.java:
package com.example.common.model;
public class User {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
5. mapper-module/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>mapper-module</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common-module</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
6. mapper-module/src/main/java/com/example/mapper/UserMapper.java:
package com.example.mapper;
import com.example.common.model.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
User selectById(Long id);
}
7. mapper-module/src/main/resources/mapper/UserMapper.xml:
<?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.mapper.UserMapper">
<select id="selectById" resultType="com.example.common.model.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
8. service-module/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service-module</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>mapper-module</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
9. service-module/src/main/java/com/example/service/UserService.java:
package com.example.service;
import com.example.common.model.User;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
10. application/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>application</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>service-module</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
11. application/src/main/java/com/example/Application.java:
package com.example;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描 Mapper 接口
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
12. application/src/main/resources/application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mapper/*.xml
在这个示例中,我们使用了 @MapperScan 注解来扫描 com.example.mapper 包下的 Mapper 接口。同时,我们确保了 service-module 模块依赖了 mapper-module 模块。这样,Spring 容器就可以正确扫描到 Mapper 接口,并将其注入到 UserService 中。
五、总结:多方面因素共同作用,逐一排查方能解决问题
Mapper 扫描无效是一个常见但又令人头疼的问题,其原因可能涉及到 Maven 依赖、Mapper 扫描路径配置、Mapper 接口识别、MyBatis 配置、类加载器隔离等多个方面。我们需要逐一排查这些可能的原因,才能找到问题的根源并解决问题。希望通过今天的讲解,能够帮助大家更好地理解 Mapper 扫描的原理,并能够有效地解决 Mapper 扫描无效的问题。