MyBatis-Spring 优化 Mapper 接口使用的实践与原理

MyBatis-Spring 优化 Mapper 接口使用的实践与原理

  • 一、纯 MyBatis 项目 Mapper 接口使用的核心痛点
    • 1.1 配置与调用流程繁琐
    • 1.2 代码规范难以统一
    • 1.3 依赖管理不清晰
  • ​二、MyBatis-Spring 实现 Mapper 接口自动化注册的原理与优势
    • 2.1 MapperScannerConfigurer
    • 2.2 ClassPathMapperScanner
    • 2.3 MapperFactoryBean

Java 开发领域, MyBatis 作为一款优秀的持久层框架,凭借其灵活的 SQL 映射功能,受到众多开发者的青睐。然而,在纯 MyBatis 项目中, Mapper 接口的使用却存在诸多痛点。随着 MyBatisSpring 框架的整合,这些问题得到了有效解决,尤其是 Mapper 接口的自动化注册机制,极大提升了开发效率与代码规范性。接下来,我们深入探讨纯 MyBatis 项目 Mapper 接口使用的困境,以及 MyBatis-Spring 如何实现接口的自动化注册与便捷调用

一、纯 MyBatis 项目 Mapper 接口使用的核心痛点

1.1 配置与调用流程繁琐

在纯 MyBatis 项目中,使用 Mapper 接口时,开发者需要手动通过SqlSession获取对应的 Mapper 实例,代码示例如下:

try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.selectById(1);
}

上述代码中,每次使用 Mapper 接口都需要重复获取SqlSessionMapper 实例的过程,不仅增加了代码量,还容易导致资源管理混乱。特别是在大型项目中,大量的 Mapper 接口调用会使代码变得冗长且难以维护。

1.2 代码规范难以统一

由于获取 Mapper 实例的操作分散在各个业务逻辑中,不同开发者的编码习惯和风格差异,会导致 Mapper 接口的使用方式不统一。有些开发者可能会在多个地方重复编写获取SqlSessionMapper 的代码,有些则可能在资源关闭时出现异常处理不规范的情况。这种不规范的代码编写方式,增加了团队协作的难度,也给后期的代码维护和调试带来了困扰。

1.3 依赖管理不清晰

MyBatis 项目中,Mapper 接口与SqlSession紧密耦合,使得代码的依赖关系变得复杂。在进行单元测试或模块重构时,由于 Mapper 接口依赖于SqlSession的创建和管理,难以实现独立的测试和模块拆分,降低了代码的可测试性和可维护性。

​二、MyBatis-Spring 实现 Mapper 接口自动化注册的原理与优势

在传统的 MyBatis 项目中,获取 Mapper 接口实例需要通过SqlSession.getMapper()方法,这一过程繁琐且不够优雅。而 MyBatis-Spring 整合后,开发者可以直接将 Mapper 接口作为 Bean 注入到 Spring 容器中使用

通过MapperScannerConfigurerClassPathMapperScannerMapperFactoryBean等关键类的协同工作,MyBatis-Spring 实现了 Mapper 接口的自动化注册,让开发者摆脱了繁琐的SqlSession.getMapper()调用,极大提升了开发效率和代码的简洁性。

2.1 MapperScannerConfigurer

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,在 Spring 容器加载 Bean 定义阶段,它会扫描指定包下的 Mapper 接口,并将其注册为 Spring Bean

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
  if (this.processPropertyPlaceHolders) {
    processPropertyPlaceHolders();
  }
  // 创建ClassPathMapperScanner扫描器
  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.registerFilters();
  // 执行扫描,将Mapper接口注册为BeanDefinition
  scanner.scan(
      StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
public int scan(String... basePackages) {
    // 记录扫描开始前的bean定义总数
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

    // 执行实际的扫描逻辑
    doScan(basePackages);

    // 如果启用了注解配置处理,注册必要的注解处理器
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

    // 返回本次扫描新增的bean定义数量
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

上述代码中,postProcessBeanDefinitionRegistry方法是核心。它创建ClassPathMapperScanner实例,配置扫描器的各项属性,然后调用scan方法扫描指定包路径下的 Mapper 接口,并将其注册为 Spring BeanDefinition

2.2 ClassPathMapperScanner

ClassPathMapperScanner继承自ClassPathBeanDefinitionScanner,重写了isCandidateComponentdoScan方法,用于筛选和处理 Mapper 接口。

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    // 省略构造函数等代码

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        // 检查类不是接口且不依赖于封闭类
        return (beanDefinition.getMetadata().isInterface() &&!beanDefinition.getMetadata().isAbstract());
    }


    @Override
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 调用父类方法扫描候选组件
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        
        if (beanDefinitions.isEmpty()) {
            logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            // 处理扫描到的BeanDefinition
            processBeanDefinitions(beanDefinitions);
        }
        
        return beanDefinitions;
    }
    
	private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
	  AbstractBeanDefinition definition;
	  BeanDefinitionRegistry registry = getRegistry();
	  
	  for (BeanDefinitionHolder holder : beanDefinitions) {
	    definition = (AbstractBeanDefinition) holder.getBeanDefinition();
	    
	    // 处理作用域代理情况
	    if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
	      // ... 
	    }
	    
	    String beanClassName = definition.getBeanClassName();
	    LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
	        + "' mapperInterface");
	
	    // 【核心代码】设置Mapper接口作为构造函数参数(将 Mapper 接口的类名作为参数传递给MapperFactoryBean的构造函数)
	    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
	    
	    // 设置mapperInterface属性,用于Spring Native支持
	    try {
	      definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName));
	    } catch (ClassNotFoundException ignore) {
	      // ... 
	    }
	
	    // 【核心代码】设置Bean类为MapperFactoryBean
	    definition.setBeanClass(this.mapperFactoryBeanClass);
	
	    // 设置addToConfig属性
	    definition.getPropertyValues().add("addToConfig", this.addToConfig);
	
	    // 设置MockitoPostProcessor需要的属性
	    definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);
	
	    // 配置SqlSessionFactory或SqlSessionTemplate
	    boolean explicitFactoryUsed = false;
	    // ...
	
	  }
	}
    
    // 省略其他方法
}

doScan方法是扫描过程的核心入口,它首先调用父类的doScan方法获取候选组件,然后通过processBeanDefinitions方法对这些组件进行进一步处理

processBeanDefinitions方法是 Mapper 接口注册的关键环节,它执行以下重要操作:

  1. 将扫描到的接口类的 BeanClass 设置为MapperFactoryBean,这意味着 Spring 在创建 Bean 实例时,会使用MapperFactoryBeangetObject()方法来获取实例
  2. 保存原始接口类名,并将其作为构造函数参数设置到 BeanDefinition 中,用于后续MapperFactoryBean初始化时的mapperInterface属性
  3. 设置 Autowire 模式为 byType,确保 Spring 能够正确注入依赖
  4. 配置SqlSessionFactorySqlSessionTemplate属性,为MapperFactoryBean提供必要的依赖

2.3 MapperFactoryBean

Mapper 接口被注册为 Bean 后,其对应的 Bean 实例是通过MapperFactoryBean创建的。MapperFactoryBean实现了FactoryBean接口,用于生成 Mapper 接口的代理实例。

getObject方法是核心,它通过SqlSessionTemplate获取 Mapper 接口的代理实例,最终实现了将 Mapper 接口作为 Bean 注入到 Spring 容器中使用的功能。

public class MapperFactoryBean<T> implements FactoryBean<T>, InitializingBean, ApplicationContextAware {

    private Class<T> mapperInterface;
     /**
	  * 获取Mapper接口的代理实现实例。
	  * 此方法通过SqlSession获取指定Mapper接口的代理实现,
	  */
	  @Override
	  public T getObject() throws Exception {
	    return getSqlSession().getMapper(this.mapperInterface);
	  }

	  @Override
	  public Class<T> getObjectType() {
	    return this.mapperInterface;
	  }
	
	  @Override
	  public boolean isSingleton() {
	    return true;
	  }
	  
	  protected void checkDaoConfig() {
	    // ...
	  }

}

对于BeanFactoryPostProcessorFactoryBean 不熟悉的同学可以参考一下下面文章:
【Spring BeanFactoryPostProcessor:机制解读与代码实践】
【浅析 Spring 中 FactoryBean 的实现与使用】

你可能感兴趣的:(Mybatis,Spring系列,mybatis,spring,java)