结合tk.mybatis插件源码,说说spring boot如何实现自动注入

一、前言

自动配置是spring boot 一个重要特性,所谓“自动”就是我们直接引用功能所需jar包,除了极个别的核心配置外,几乎不用额外的配置,从而减少繁琐配置项,也就是常说的约定大于配置思想的体现。下面结合mybatis插件--tk.mybatis说说spring boot如何实现的自动配置的。

tk.mybatis可以看做是mybatis框架的一个插件,项目提供了常规的增删改查操作以及Example 相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,使用它可以很方便的进行开发,可以节省开发人员大量的时间。项目地址

二、实现原理

1、spring boot入口

既然是自动配置,肯定是在spring boot启动中实现的注入,我们来从spring的入口找寻答案:

@SpringBootApplication //spring boot启动的核心注解
public class NewMediaMpApiApplication 

    public static void main(String[] args) {
        SpringApplication.run(NewMediaMpApiApplication.class, args);
    }

}

这是spring boot 最简单的启动类,点开@SpringBootApplication这个注解看下。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration//开启自动配置类
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  //omit code ...
}

2、自动配置入口

这个注解是有多个注解复合而成,其中@EnableAutoConfiguration由名字可以看它应该就是实现自动配置的入口,顺便说一下,@Enable**开头的注解在spring boot中往往都是开启xx功能的入口,我们在看源码的时候可以从这里入手。跟进去看看:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
   //omit code ...
}

这个注解的描述官方给出的描述是:

Enable auto-configuration of the Spring Application Context, attempting to guess and configure beans that you are likely to need

即,为spring容器上下文开启自动配置,尝试猜测你需要加载哪些配置bean。

3、收集需要配的类

顺着@Import可以找到,整个自动配置的实现都是通过AutoConfigurationImportSelector这个选择器实现把jar中需要的组件导入到spring IOC容器中这一功能。(@Import是通过导入的方式将类加载到spring容器中的),AutoConfigurationImportSelector实现了ImportSelector接口,官方文档是这么描述这个接口的:

Interface to be implemented by types that determine which @{@link Configuration}, class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

文档说的很明白,实现这个接口的类按照给定的选择条件(通常是注解中的一个或多个属性),筛选出需要注入的配置类。该接口定义了一个方法:返回满足条件配置类的数组。我们看下AutoConfigurationImportSelector是如何实现这个方法的:

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
    //核心方法,获取满足条件的配置信息
        AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
                autoConfigurationMetadata, annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

核心方法就是获取配置类信息,我们往下继续跟进到底是怎么获取的配置:

    protected AutoConfigurationEntry getAutoConfigurationEntry(
            AutoConfigurationMetadata autoConfigurationMetadata,
            AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        }
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
    //获取所有候选的配置信息
        List<String> configurations = getCandidateConfigurations(annotationMetadata,
                attributes);
    //过滤掉重复的
        configurations = removeDuplicates(configurations);
    //排除掉明确需要排除的配置
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }

这个方法主要的功能是获取了候选的配置类,然后再按规则过滤掉,这里只关注如何获取候选配置类的,往下走:

    /**
     * Return the auto-configuration class names that should be considered
     * 返回认为需要自动配置的类
     */
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
    //主要方法,加载配置
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

4、配置类是从哪里加载的呢?

我们看到加载配置类的方法了,胜利就在前方,进SpringFactoriesLoader.loadFactoryNames()方法:

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    }

//loadFactoryNames主要调用的方法
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String, String> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        try {
      //FACTORIES_RESOURCE_LOCATION的值 "META-INF/spring.factories"
            Enumeration<URL> urls = (classLoader != null ?
                    classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                     
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            result = new LinkedMultiValueMap<>();
      //根据spring.factories配置中的value值获取候选的配置类
            while (urls.hasMoreElements()) {
        //循环遍历配置项的值
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    String factoryClassName = ((String) entry.getKey()).trim();
                    for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
            //将配置类的文件路径加入到结果集中
                        result.add(factoryClassName, factoryName.trim());
                    }
                }
            }
            cache.put(classLoader, result);
            return result;
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" +
                    FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
    }

至此,我们找到自动配置加载的起始位置: META-INF/spring.factories。spring通过SpringFactoriesLoader.loadFactoryNames()扫描Java jar包下META-INF/spring.factories文件,获取所有需要加载的配置类,从而初始化到spring容器中。

下面我们去看下使用自动配置的jar包中看是否是这样实现的,用前文说到我们以tk.mybaits为例:

<img src="https://upload-images.jianshu.io/upload_images/14480907-b912eb91a942dab1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="1.png" style="zoom: 67%;" />

打开spring.factories文件看看配置文件里的内容:

image.png

这个配置文件告诉spring,MapperAutoConfiguration就是tk.mybaits的配置类,根据此配置类将运行所需的内容注入到spring的IOC容器中。

三、tk加载到spring容器中

1、MapperAutoConfiguration配置文件详解

我们来详细学习下这个配置类,简化细节:

//声明这是一个配置类
@org.springframework.context.annotation.Configuration
//这两个配置类必须存在才能初始换当前配置类
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
//spring容器中必须存在DatcSource才会初始化当前配置类
@ConditionalOnBean(DataSource.class)
//导入MybatisProperties.class文件中的关于mybatis的配置
@EnableConfigurationProperties({MybatisProperties.class})
//当前配置类必须在DataSourceAutoConfiguration.class加载后才能初始化
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
//当前配置类必须在MybatisAutoConfiguration加载之前才能初始化
@AutoConfigureBefore(name = "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration")
public class MapperAutoConfiguration {
  
  //omit code...
  
        //从spring中为MapperAutoConfiguration赋值
      public MapperAutoConfiguration(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();
    }
 
    //如果spring容器中没有sqlSessionFactory,注入这个bean
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

      //omit code ...
    }
  
  
    //如果spring容器中没有sqlSessionTemplate,注入这个bean
    @Bean  
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
      //omit code ...
    }
  
  //这个是整个tk.mybatis的核心配置文件,即扫描
  public static class AutoConfiguredMapperScannerRegistrar
            implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    
    //扫描所有被@Mapper注解修饰的接口,也是在这一个过程中,tk生成了内置的常用方法的sql,用于调用时在mybatis执行
    @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

            logger.debug("Searching for mappers annotated with @Mapper");
          
            //omit code...
        }
    //omit code ...
  }

}

本配置类的注解描述部分,主要声明了所依赖的bean,需要以及初始化所需要的属性,是初始化到spring容器的先决条件。tk作为mybatis的插件,在加载数据源先关组件后,加载mybatis前插入tk加载过程,使用上述注解组合达到这个目的。前置条件满足后,最终目的还是AutoConfiguredMapperScannerRegistrar配置的加载,即tk生成了内置的常用方法的sql,用于调用时在mybatis执行。

2、@Configuration配合使用的常见注解

另外,还有写配合@Configuration使用的常见注解:

  1. 属性类注解

    • EnableConfigurationProperties

    • @ConfigurationProperties(prefix = "xxx")

      在需要注入配置的类上加上这个注解,prefix的意思是,以该前缀打头的配置,@EnableConfigurationProperties({MybatisProperties.class}) 开启tk.mybatis的配置项

  2. 条件系列注解,@Conditon*

    • @ConditionalOnBean 仅仅在当前上下文中存在某个对象时,才会实例化一个Bean

      @ConditionalOnBean(DataSource.class) 表示容器中已经注入了数据源的对象

    • @ConditionalOnClass 某个class位于类路径上,才会实例化一个Bean),该注解的参数对应的类必须存在,否则不解析该注解修饰的配置类

      @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) 表示必须要存在这两个和数据路连接相关的类

    • @ConditionalOnExpression 当表达式为true的时候,才会实例化一个Bean

    • @ConditionalOnMissingBean 仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean

    • @ConditionalOnMissingClass 某个class类路径上不存在的时候,才会实例化一个Bean

    • @ConditionalOnNotWebApplication 不是web应用时,才会执行

  3. 顺序系列注解

    • @AutoConfigureAfter 在加载配置的类之后再加载当前类

      @AutoConfigureAfter(DataSourceAutoConfiguration.class) 表示在加载数据源的配置之后加载tk.mybatis的配置类

    • @AutoConfigureBefore 在加载配置的类之后再加载当前类

      @AutoConfigureBefore(name = "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration") 表示要在mybatis配置类加载前完成tk.mybatis的注入

至此,spring将tk运行所需的全部组件在启动时注入到spring的IOC容器中。

3、spring boot内置的自动配置项

从而实现了spring boot的自动配置,了解整个过程后,我们就可以知道spring boot中那些导入包简单配置下就能用的原理了,比如Spring data jpa 、Spring data redis等配置上连接地址就可以用了,其实秘密就在于此,我们来看下spring boot 中的内置的自动配置:

spring boot 内置.png

是不是看着很眼熟,这就是spring boot常说的“简化配置”的关键所在。

四、自动配置的思维导图

我将自动配置的过程整理成思维导图帮助理解和理解,如下:

spring boot 自动配置.png

欢迎拍砖,欢迎交流~

注:转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容