为什么 MyBatis Mapper 接口能像普通 Bean 一样被 @Autowired?

案例

案例一: MyBatis单独使用

在 resources 目录下新建 mybatis-config.xml 配置文件,文件内容如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--配置日志-->
    <settings>
        <setting name="logImpl" value="LOG4J2"/>
    </settings>
    
    <!--配置数据源和事务管理器-->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/study"/>
                <property name="username" value="root"/>
                <property name="password" value="xxxxxx"/>
            </dataSource>
        </environment>
    </environments>

	<!--配置 mapper 文件的位置-->
    <mappers>
        <mapper resource="com/study/mybatis/UserMapper.xml"/>
    </mappers>
</configuration>

resources 下面的 com/study/mybatis 文件夹中新建 UserMapper.xml 文件,文件内容如下:

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.study.mybatis.mapper.UserMapper">  
    <select id="getUserById" resultType="com.study.mybatis.entity.User">  
        select * from User where id = #{id}  
    </select>  
</mapper>

同时在对应层级的 package 下新建 UserMapper 接口,该接口中包含一个 getUserById() 方法和 UserMapper.xml 文件中配置的相对应。代码如下:

java 复制代码
@Mapper  
public interface UserMapper {  
    User getUserById(@Param("id") Long id);  
}

然后在 Java 代码中,可以通过现构造 SqlSessionFactory 对象,从这个对象中获取一个 SqlSession 对象,然后再通过它获取到 UserMapper 接口的动态代理对象,然后通过这个动态代理对象来调用对应的方法。代码如下:

java 复制代码
public static void main(String[] args) throws IOException {
    // 读取配置文件生成 SqlSessionFactory 对象
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    // 获取 SqlSession 对象,然后通过该对象获取 Mapper 接口的动态代理对象
    // 然后根据操作该动态代理对象
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper userMapper = session.getMapper(UserMapper.class);
        User user = userMapper.getUserById(1L);
        System.out.println(JSON.toJSONString(user));
    }
}

案例二: MyBatis和Spring一起使用

java 复制代码
@Configuration
@MapperScan("com.study.mapper") 
public class MyBatisConfig {
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study");
        dataSource.setUsername("root");
        dataSource.setPassword("xxxxxx");
        return dataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
        );
        return sessionFactoryBean.getObject();
    }
}

@RestController  
@RequestMapping("/users")  
public class UserController {  
    @Autowired  
    private UserMapper userMapper;  
  
    @GetMapping("/{id}")  
    @ResponseBody  
    public User getUserById(@PathVariable Long id) {  
        return userMapper.getUserById(id);  
    }  
}

从上面的两个案例可以看到,当 MyBatis 单独使用的时候,需要手动通过 SqlSession 对象来获取 Mapper 接口的动态代理对象;当 MyBatis 和 Spring 单独使用的时候,可以把 Mapper 对象当作一个普通的 Bean 对象,通过 @Autowired 注解注入到需要使用的类里面就可以了。

那当 MyBatis 和 Spring 一起使用的时候,是如何做到把 Mapper 对象作为一个 Bean 注入到 Spring 中的呢?本文将带你从源码的角度进行分析。

先说结论,Spring 通过把 @MapperScan 注解指定的路径下的 Mappper 接口扫描成 Bean 定义,并通过 MapperFactoryBean 作为 Bean 定义的 BeanClass 属性,在 MapperFactoryBeangetObject() 方法中还是调用了 SqlSessiongetMapper() 方法获取到动态代理对象,并把这个动态代理对象放到 Spring 容器中,供其它需要注入它的地方使用。

源码

首先看下 @MapperScan 注解,它通过 @Import 注解引入了实现了 ImportBeanDefinitionRegistrar 接口的 MapperScannerRegistrar 类。代码如下:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
@Documented  
@Import(MapperScannerRegistrar.class)  
@Repeatable(MapperScans.class)  
public @interface MapperScan {
}

在它的 registerBeanDefinitions() 方法中,会从 @MapperScan 注解中获取配置的值,比如像 sqlSessionFactoryRef, basePackages 等一些配置,然后构造了一个MapperScannerConfigurer 的 Bean 定义然后注册。代码如下:

java 复制代码
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
          generateBaseBeanName(importingClassMetadata, 0));
    }
  }

  void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);

	// 省略代码

    String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
    if (StringUtils.hasText(sqlSessionTemplateRef)) {
      builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
    }

    String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
    if (StringUtils.hasText(sqlSessionFactoryRef)) {
      builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
    }

    // 省略代码
    
    List<String> basePackages = new ArrayList<>();
basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
        .collect(Collectors.toList()));

    basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
        .collect(Collectors.toList()));

    if (basePackages.isEmpty()) {
      basePackages.add(getDefaultBasePackage(annoMeta));
    }

    builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));

    // for spring-native
    builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

	// 这里注册了一个MapperScannerConfigurer类型的Bean定义
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }
}

MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,这个接口的作用在前面的文章 3 个案例看透 Spring @Component 扫描:从普通应用到 Spring Boot介绍过。 在它的 postProcessBeanDefinitionRegistry() 方法中会委托 ClassPathMapperScanner 去扫描路径下的 Mapper 接口为 Bean 定义。代码如下:

java 复制代码
public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }

	// 这里会为scanner注册过滤器
    scanner.registerFilters();
    // 调用scan方法扫描Mapper
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }
}

ClassPathMapperScanner 继承了 ClassPathBeanDefinitionScanner,在它的 doScan() 方法中,首先会调用父类的 doScan() 方法扫描 Bean 定义,然后在 processBeanDefinitions() 方法中将 Bean 的 BeanClass 设置为 MapperFactoryBean,从名字可以看出它是一个实现了 FactoryBean 接口的类。代码如下:

java 复制代码
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        
        if (beanDefinitions.isEmpty()) {
          // 省略代码
        } else {
            processBeanDefinitions(beanDefinitions);
        }
        return beanDefinitions;
    }
    
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        AbstractBeanDefinition definition;
        BeanDefinitionRegistry registry = getRegistry();
        for (BeanDefinitionHolder holder : beanDefinitions) {
            definition = (AbstractBeanDefinition) holder.getBeanDefinition();
        
            // 设置BeanClass为MapperFactoryBean
            definition.setBeanClass(this.mapperFactoryBeanClass);
        
            // 省略代码
        }
    }
}

在 Spring 中当一个 Bean 定义的 BeanClass 被设置为 FactoryBean 的时候,最终注册到容器中的实际上是它的 getObject() 方法返回的对象。而 MapperFactoryBeangetObject() 方法实际上还是通过 SqlSession 对象来获取 Mapper 的动态代理对象。从而实现了把原来 手动通过调用 SqlSessiongetMapper() 方法得到动态代理对象放到 Spring 容器中了,其它地方就可以通过 @Autowired 注解进行注入。代码如下:

java 复制代码
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    @Override
    public T getObject() throws Exception {
      return getSqlSession().getMapper(this.mapperInterface);
    }
}
相关推荐
考虑考虑4 小时前
Redis8中的布谷鸟过滤器
redis·后端·程序员
Sammyyyyy4 小时前
Node.js 做 Web 后端优势为什么这么大?
开发语言·前端·javascript·后端·node.js·servbay
重庆穿山甲4 小时前
Cola架构深度解析:企业级应用架构设计指南
后端
IT_陈寒4 小时前
🔥5个必学的JavaScript性能黑科技:让你的网页速度提升300%!
前端·人工智能·后端
莫克5 小时前
java文件上传
后端
LeonMinkus5 小时前
dubbo3使用grpc开发分布式服务
后端
一只韩非子6 小时前
Spring AI Alibaba 快速上手教程:10 分钟接入大模型
java·后端·ai编程
起风了___6 小时前
20 分钟搞定:Jenkins + Docker 一键流水线,自动构建镜像并部署到远程服务器
后端