spring boot原理分析(五):依赖包中bean自动配置之Mybatis和自定义包配置

前言

    原理分析(三)介绍了spring boot项目中自带的外部依赖的自动化配置,比如jdbc、redis、MongoDB、Kafka、amqp等比较常见的外部依赖项目。除此之外,还有一些不太常见的项目,或者仅仅是因为没有被纳入到spring boot优待范畴中的项目,它们的bean自动化配置也是需要考虑的方面。再进一步,有些项目模块甚至是我们自己内部编写并使用的,也同样需要在引入jar包后注入项目中的bean。针对这样的情况,spring boot提供了怎样的解决方案呢?本篇文章就针对这一问题,以mybatis为例,讲解spring boot项目真·外部依赖包的bean注入。最后会依据原理,给一个内部项目注入bean的简单方案,也算理论应用到实践。
    其实整个流程还是以原理分析(三)为基础,然后进行了简单的拓展而已。所以,我会对原理分析(三)的基本流程进行一个总结,如下图。
spring boot原理分析(五):依赖包中bean自动配置之Mybatis和自定义包配置-process.png
入口类(包含有main函数的类)上@EnableAutoConfiguration,实际上是一个复合注解,内部包含了@Import(AutoConfigurationImportSelector.class)注解。@Import注解会注入AutoConfigurationImportSelector类中的selectImports方法返回的类名的bean。selectImports方法为了返回满足条件的自动配置类的类名,会搜索当前项目和当前项目依赖的jar包中META-INF/目录的spring.factories文件、spring-autoconfigure-metadata.properties文件。spring.factories存储的是自动配置类和其他相关配置类,spring-autoconfigure-metadata.properties存储的是这些配置类的依赖配置。selectImports方法根据配置类和配置类的依赖配置,就能够筛选的不满足依赖配置的配置类,获取最终能够被注入的类的类名。

Mybatis自动配置

    mybatis是一种使用广泛的ORM框架,底层是基于java的jdbc。在spring boot项目中,mysql数据库作为数据源,引入mybatis比较简单,只需要在pom中添加依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.41</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

依赖引入后,在依赖包中可以找到一个命名为mybatis-spring-boot-autoconfigure-xxx.jar的jar包。解包后,可以发现目录中包含META-INF/spring.factories文件。
spring boot原理分析(五):依赖包中bean自动配置之Mybatis和自定义包配置-mybatis-autoconfigure.png
其实这个文件中只有一句配置语句,

 # Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

,用来注入Mybatis自动配置类MybatisAutoConfiguration。为了证明这个文件确确实实在项目运行时被加载过,MybatisAutoConfiguration的bean会被注册到上下文环境,调试监控AutoConfigurationImportSelector类的autoConfigurationEntries对象,得到如下结果:
spring boot原理分析(五):依赖包中bean自动配置之Mybatis和自定义包配置-autoconfigurationEntry.png
。同时也能看到,原理分析(四)中提到的spring boot项目内的spring.factories中满足注入条件的自动配置类也被正常注册。
    既然已经提到了MybatisAutoConfiguration,为了能够深入理解Mybatis的自动配置是如何完成的,还是要了解一下MybatisAutoConfiguration中做了哪些工作。
    MybatisAutoConfiguration的自动配置类(源代码如下)和原理分析(四)中的ServletWebServerFactoryAutoConfiguration比较类似。Mybatis使用SqlSessionFactory构造SqlSession实例,SqlSession是与数据库交互的单线程对象,包括和所有执行SQL操作的方法,实质上是使用了JDBC的连接。SqlSession是线程不安全的,spring下使用SqlSessionTemplate,内部使用ThreadLocal线程变量弥补了SqlSession的线程不安全问题。MybatisAutoConfiguration自动配置类首先就要解决SqlSessionFactory和SqlSessionTemplate的bean的注入问题。
使用@ConditionalOnClass注解可以判断bean是否存在,类中的
sqlSessionFactory(DataSource dataSource)方法会在SqlSessionFactory的bean不存在的条件下注入一个。对于sqlSessionTemplate(SqlSessionFactory sqlSessionFactory)也是同理。至于@ConditionalOnBean(DataSource.class),是因为底层依赖jdbc,jdbc需要配置DataSource,SqlSessionFactory的构造方法需要传入DataSource。AutoConfiguredMapperScannerRegistrar是用来配置Mapper扫描的,前面的文章多次提到过ImportBeanDefinitionRegistrar,

这个接口的实现类可以由@Import注解引入,会自动调用它的registerBeanDefinitions方法。registerBeanDefinitions方法传入了用来获取注解信息的AnnotationMetadata和用来注册bean的BeanDefinitionRegistry。ImportBeanDefinitionRegistrar这个接口可以用来注入bean或者实现自己定义的类@Component注解。

这里的类@Component注解就是@Mapper。这个自动配置类更细节部分就不再深入探索了,这部分知识和jdbc、ORM框架、Mybatis实现强相关,感兴趣可以去研究一下。

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {

  private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);

  private final MybatisProperties properties;

  private final Interceptor[] interceptors;

  private final ResourceLoader resourceLoader;

  private final DatabaseIdProvider databaseIdProvider;

  private final List<ConfigurationCustomizer> configurationCustomizers;

  public MybatisAutoConfiguration(MybatisProperties properties,
                                  ObjectProvider<Interceptor[]> interceptorsProvider,
                                  ResourceLoader resourceLoader,
                                  ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                  ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
  }

  @PostConstruct
  public void checkConfigFileExists() {
    if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
      Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
      Assert.state(resource.exists(), "Cannot find config location: " + resource
          + " (please add config file or check your Mybatis configuration)");
    }
  }

  @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()));
    }
    Configuration configuration = this.properties.getConfiguration();
    if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
      configuration = new Configuration();
    }
    if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
      for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
        customizer.customize(configuration);
      }
    }
    factory.setConfiguration(configuration);
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }

    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, ResourceLoaderAware {

    private BeanFactory beanFactory;

    private ResourceLoader resourceLoader;

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

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

      ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

      try {
        if (this.resourceLoader != null) {
          scanner.setResourceLoader(this.resourceLoader);
        }

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

        scanner.setAnnotationClass(Mapper.class);
        scanner.registerFilters();
        scanner.doScan(StringUtils.toStringArray(packages));
      } catch (IllegalStateException ex) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
      }
    }

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

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
      this.resourceLoader = resourceLoader;
    }
  }

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

    @PostConstruct
    public void afterPropertiesSet() {
      logger.debug("No {} found.", MapperFactoryBean.class.getName());
    }
  }

}

本地项目的自动化配置示例

    根据上面原理分析(四)中例子的铺垫和Mybatis自动化配置的示例,想必已经可以领会自定义项目包中该预先注入自己需要的bean。有个问题是,之前的例子都是源码中标准的规范性代码,自动化配置类会使用很多工具,比如@ConditionalOnBean、@ConditionalOnClass、@ConditionalOnMissingBean、@EnableConfigurationProperties、ImportBeanDefinitionRegistrar、BeanFactoryAware等等。对于自定义的项目来说,可能并不需要运用这么多的工具,只要能把项目目录下的bean注入即可。下面要说的一种方式,非常简单,也是运用到了上面的spring boot自动化配置的原理。也许已经有人在用这种方式了,只是不太清楚为什么这么做,现在恰好可以将实践和理论结合起来。
    如何只是简单的想要其他项目在引用本地自定义包时自动注入项目目录下的bean,可以直接使用@ComponentScan注解,将配置文件放在项目上层包中,@ComponentScan会迭代地扫描所有子目录下的bean并注入。或者甚至不用放在项目的上层包中,只需要配置@ComponentScan的basePackages参数为需要扫描的目录即可。

@Configuration
@ComponentScan()
public class AutoConfig {

}

    现在的问题就就转换为在其他包引入这个依赖包时,如何时AutoConfig注入生效。方法也有很多,使用上面普遍采用的方式就是新建resources/META-INF目录,在META-INF中新增spring.factories文件,在文件中写入:

 # Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.demo.AutoConfig

org.demo.AutoConfig代表就是上面扫描配置类的完整引用。当然在此基础上,你还可以添加更高级的特征,之前的源码例子已经给了很好的示范,这里不再赘述。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值