前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Spring源码】- 08 扩展点之mybatis集成

【Spring源码】- 08 扩展点之mybatis集成

作者头像
Reactor2020
发布2023-03-22 18:58:03
5770
发布2023-03-22 18:58:03
举报
文章被收录于专栏:【云原生 • Prometheus】

概述

mybatis将与spring集成的代码拆分到了mybatis-spring模块,避免mybatisspring之间的耦合,如果你只需要纯粹的使用mybatis api,就避免了必须将spring依赖也耦合进来的问题。mybatis使用中一般是将Sql语句写在xml文件中,为方便操作,我们会创建一个Mapper接口文件进行映射,mybatis提供了采用动态代理方式对Mapper接口类进行包装,这样我们就可以像使用普通对象一样执行各种方法调用。

mybatisspring集成的一个核心任务就是将这些动态代理包装的Mapper对象注入到IoC容器中,这样其它Bean就可以方便的使用如@Autowired等方式进行依赖注入。

MapperScannerConfigurer

需要将mybatis生成的动态代理对象注入到IoC容器中,自然我们想到之前的BeanFactoryPostProcessor的子类BeanDefinitionRegistryPostProcessor这个扩展类。MapperScannerConfigurer就是实现了BeanDefinitionRegistryPostProcessor接口,然后在该接口中通过类扫描器scanner进行扫描注册。

代码语言:javascript
复制
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);//指定引用的SqlSessionFactory
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.registerFilters();
    //basePackage指定扫描Mapper接口包路径
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

ClassPathMapperScanner这个就是继承之前介绍过的SpringClassPathBeanDefinitionScanner类扫描器进行了扩展,它可以实现将包路径下至少含有一个方法的接口类注册到IoC中。

这里有个问题:注册进入的BeanDefinitionbeanClass指向的都是接口,到后续创建对象时会存在问题,接口是没法创建实例的。所以,ClassPathMapperScanner扫描器在注册完成后,又会对BeanDefinition进行处理。处理逻辑位于ClassPathMapperScanner#processBeanDefinitions()方法中,其核心逻辑见下:

代码语言:javascript
复制
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      String beanClassName = definition.getBeanClassName();

      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
      definition.setBeanClass(this.mapperFactoryBeanClass);
   ...
    }
}

其中最重要的一条语句:definition.setBeanClass(this.mapperFactoryBeanClass),偷偷的将BeanDefinitionbeanClass替换成了MapperFactoryBean,而不再指向Mapper接口类。同时将Mapper接口类作为参数传入到了MapperFactoryBean中,即调用下面构造方法:

代码语言:javascript
复制
public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
}

MapperFactoryBean实现了FactoryBean接口,这样实际上它是通过getObject()方法获取到对象然后注入到IoC容器中。而在getObject()方法中,我们就可以使用mybatis api获取到Mapper接口类的动态代理对象:SqlSession#getMapper()

代码语言:javascript
复制
public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
}

上面我们分析了如何将Mapper接口类注入到IoC容器中的实现思路,现在总结下主要有:

  1. 基于BeanDefinitionRegistryPostProcessor接口实现扩展,然后动态向IoC容器中注入Bean
  2. 在注入时,会使用到ClassPathMapperScanner类扫描器将所有的Mapper接口类解析成BeanDefinition集合注入;
  3. 为了解决接口不能创建对象问题,再注入后又将BeanDefinitionbeanClass替换成FactoryBean的实现类:MapperFactoryBean,在该实现类中通过mybatis apiSqlSession#getMapper()获取到Mapper接口的动态代理类

扩展点引入

通过MapperScannerConfigurer,解决了如何将Mapper接口类注入到IoC容器的问题,现在还有另外一个问题,这个扩展点只有注册到Spring中才会起作用,那又如何将其注册到Spring中呢?

方式一:最直接方式就是直接创建MapperScannerConfigurer类型的Bean实例,比如:

代码语言:javascript
复制
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="org.simon.demo01.mapper" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

这种方式是最简单直接的,但是使用角度来说不方便,所以,mybatis-spring-1.2新增了两种方式:<mybatis-scan>标签方式和@MapperScan注解方式。

首先来看下<mybatis:scan>标签方式,添加mybatisschema,然后就可以使用<mybatis:scan base-package="org.simon.demo01.mapper"/>:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
        xmlns:p="http://www.springframework.org/schema/p"
       xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://mybatis.org/schema/mybatis-spring
        http://mybatis.org/schema/mybatis-spring.xsd">

    <!-- 自动扫描 -->
    <context:component-scan base-package="org.simon.demo01" />

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=Asia/Shanghai" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
    </bean>

    <mybatis:scan base-package="org.simon.demo01.mapper"/>

</beans>

后台处理类NamespaceHandler<scan>标签注册解析器MapperScannerBeanDefinitionParser

代码语言:javascript
复制
public class NamespaceHandler extends NamespaceHandlerSupport {

  @Override
  public void init() {
    registerBeanDefinitionParser("scan", new MapperScannerBeanDefinitionParser());
  }

}

再看下MapperScannerBeanDefinitionParser解析器:

代码语言:javascript
复制
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);

    ClassLoader classLoader = ClassUtils.getDefaultClassLoader();

    builder.addPropertyValue("processPropertyPlaceHolders", true);
    try {
      String annotationClassName = element.getAttribute(ATTRIBUTE_ANNOTATION);
      if (StringUtils.hasText(annotationClassName)) {
        @SuppressWarnings("unchecked")
        Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) classLoader
            .loadClass(annotationClassName);
        builder.addPropertyValue("annotationClass", annotationClass);
      }
      String markerInterfaceClassName = element.getAttribute(ATTRIBUTE_MARKER_INTERFACE);
      if (StringUtils.hasText(markerInterfaceClassName)) {
        Class<?> markerInterface = classLoader.loadClass(markerInterfaceClassName);
        builder.addPropertyValue("markerInterface", markerInterface);
      }
      String nameGeneratorClassName = element.getAttribute(ATTRIBUTE_NAME_GENERATOR);
      if (StringUtils.hasText(nameGeneratorClassName)) {
        Class<?> nameGeneratorClass = classLoader.loadClass(nameGeneratorClassName);
        BeanNameGenerator nameGenerator = BeanUtils.instantiateClass(nameGeneratorClass, BeanNameGenerator.class);
        builder.addPropertyValue("nameGenerator", nameGenerator);
      }
      String mapperFactoryBeanClassName = element.getAttribute(ATTRIBUTE_MAPPER_FACTORY_BEAN_CLASS);
      if (StringUtils.hasText(mapperFactoryBeanClassName)) {
        @SuppressWarnings("unchecked")
        Class<? extends MapperFactoryBean> mapperFactoryBeanClass = (Class<? extends MapperFactoryBean>) classLoader
            .loadClass(mapperFactoryBeanClassName);
        builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
      }
    } catch (Exception ex) {
      XmlReaderContext readerContext = parserContext.getReaderContext();
      readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause());
    }

    builder.addPropertyValue("sqlSessionTemplateBeanName", element.getAttribute(ATTRIBUTE_TEMPLATE_REF));
    builder.addPropertyValue("sqlSessionFactoryBeanName", element.getAttribute(ATTRIBUTE_FACTORY_REF));
    builder.addPropertyValue("lazyInitialization", element.getAttribute(ATTRIBUTE_LAZY_INITIALIZATION));
    builder.addPropertyValue("basePackage", element.getAttribute(ATTRIBUTE_BASE_PACKAGE));

    return builder.getBeanDefinition();
  }

最关键的就是第一句BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);,又是将MapperScannerConfigurer动态注入到Spring中,下面一堆都是解析标签属性进行依赖注入。

再来看下@MapperScan注解方式,如:@MapperScan(basePackages = "org.simon.demo01.mapper")

代码语言:javascript
复制
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
}

@MapperScan注解上面使用了使用了一种非常常见的扩展方式:@Import扩展。通过@Import注解,引入了MapperScannerRegistrar,它是ImportBeanDefinitionRegistrar类型,通常和@Import注解组合使用,实现动态注入功能:

代码语言:javascript
复制
 @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //获取注解上属性
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
    }
  }

  void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {
    //创建一个MapperScannerConfigurer的BeanDefinition
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);

    /**
     * 下面就是解析注解属性值,通过PropertyValue方式进行依赖注入到Bean中
     */
    builder.addPropertyValue("processPropertyPlaceHolders", true);

    Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
    if (!Annotation.class.equals(annotationClass)) {
      builder.addPropertyValue("annotationClass", annotationClass);
    }

    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
      builder.addPropertyValue("markerInterface", markerInterface);
    }

    ...//各种依赖注入


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

    //将生成的BeanDefinition注册到IoC中
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

}

方法中同样有BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class)这句,动态的将MapperScannerConfigurer注入到Spring中,然后是一堆的解析注解属性进行依赖注入,这样通过@Import+ImportBeanDefinitionRegistrar动态注入,就实现了将MapperScannerConfigurer扩展点注册到Spring中。

SpringBoot自动装配

不论是通过<mybatis:scan>标签方式,还是@MapperScan注解方式,这些是常规的第三方模块与Spring进行集成方式。这种集成方式比较繁琐的是:你不光要通过<mybatis:scan>@MapperScan注解将第三方集成进来,你还需要初始化一些依赖对象,比如这里的DataSourceSqlSessionFactory等。当一个项目集成了很多第三方模块时,每个模块都这样搞一下,配置的工作量就大了,比如最常使用的ssm集成配,传统Spring集成要搞一大堆配置。

所以,SpringBoot提出了一个比较优秀的思想:自动装配。需要什么模块直接把依赖添加进来,自动完成装配,对于个性化可以在属性文件中进行配置,从使用角度来说,即插即用,不需要有太多的编码。第三方程序和spring就像完全融入一体一样,简化项目构建时集成成本,也降低项目配置的复杂性,所以SpringBoot会被越来越多的项目所采用,进而也推动微服务的兴起。

SpringBoot中使用mybatis,直接依赖mybatis-spring-boot-starter,它会把mybatismybatis-springmybatis-spring-boot-autoconfigure三个依赖包都添加进来。前面两个依赖包好理解,这里关键是第三个依赖包,就是通过它实现了mybatis自动装配功能。下面我们来看下SpringBoot是如何实现mybatis的主动装配。

1、首先,定义一个mybatis主动装配配置类,如下:

代码语言:javascript
复制
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

  public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
      ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
      ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
      ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
 ...
  }

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    applyConfiguration(factory);
    ...//省略一堆配置
    if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
      factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
    }

    return factory.getObject();
  }


  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  
  public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

    private BeanFactory beanFactory;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      if (!AutoConfigurationPackages.has(this.beanFactory)) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
        return;
      }

      logger.debug("Searching for mappers annotated with @Mapper");

      List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
      if (logger.isDebugEnabled()) {
        packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
      }

      BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
      builder.addPropertyValue("processPropertyPlaceHolders", true);
      builder.addPropertyValue("annotationClass", Mapper.class);
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
      BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
      Stream.of(beanWrapper.getPropertyDescriptors())
          .filter(x -> x.getName().equals("lazyInitialization")).findAny()
          .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
      registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
      this.beanFactory = beanFactory;
    }

  }


  @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
  }
}

这里主要利用MapperScannerRegistrarNotFoundConfiguration类上的@Import(AutoConfiguredMapperScannerRegistrar.class)引入,然后在AutoConfiguredMapperScannerRegistrarBeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class)这句又是动态注入MapperScannerConfigurer。不过,主动装配配置类中,还会把相关的依赖也一起创建、初始化,比如:SqlSessionFactorySqlSessionTemplate

@EnableConfigurationProperties(MybatisProperties.class)mybatis相关配置引入进来,这样在创建、初始化过程中的定制需求就可以通过配置修改。

2、有了这个主动装配配置类还不行,下一步就是看如何让主动装配配置类生效。SpringBoot提供了SpringFactoriesLoader工厂加载机制,类似于JDK中的SPI机制,实现将模块META-INF/spring.factories文件中配置注入到Spring容器中。mybatis-spring-boot-autoconfigure模块下META-INF/spring.factories文件中就有MybatisAutoConfiguration

代码语言:javascript
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

3、使用时依赖添加进来,配置下属性,就可以直接使用,基本不再需要编码:

代码语言:javascript
复制
mybatis.mapper-locations: classpath:mapper/*Mapper.xml
mybatis.type-aliases-package: com.example.demo.entity

总结

从上面来看,mybatisspring集成的关键的是将mybatis-spring模块下MapperScannerConfigurer集成进来,因为,它是一个BeanDefinitionRegistryPostProcessor类型的扩展,内部通过自定义scanner扫描Mapper接口自动注册到IoC容器中,这一点在各种集成方式中是统一一样的。不同点在于:MapperScannerConfigurer扩展类是如何被引入的。传统的Spring方式通过@Mapper注解或<mybatis:scan>自定义标签实现,但是对于一些依赖对象还是需要手工创建,比较繁琐;而SpringBoot利用自动装配,让第三方模块集成变成了一个插件,即插即用,无需太多编码。

分析了mybatis集成方式,从中也学习了如何利用Spring的各种扩展点进行定制,更重要的是也为我们开发自己模块和Spring集成提供了思路。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Reactor2020 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • MapperScannerConfigurer
  • 扩展点引入
  • SpringBoot自动装配
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档