Spring Boot多模块项目中Mapper扫描无效的真实原因与解决方案

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>

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 扫描无效的问题。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注