Spring Boot 系列(一)依赖管理以及自动装配

依赖管理

  1. 为什么导入dependency时不需要指定版本?
    在Spring Boot 入门程序中,项目pom.xml文件有两个核心依赖,分别是spring-boot-strarter-parent和spring-boot-starter-web,关于这两个依赖的相关介绍如下:
    spring-boot-starter-parent依赖
    示例代码如下
<!-- Spring Boot父项目依赖管理 -->
 <parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent<11./artifactId>
 <version>1.5.14.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
 </parent>

上述代码,将spring-boot-start-parent依赖作为SpringBoot项目的统一父项目依赖管理,并将项目版本号统一改为2.2.2.RELEASE,该版本可以根据实际开发需求进行修改
使用“Ctr+鼠标左键”进入并查看spring-boot-starter-parent底层源文件,发现spring-boot-starter-parent的底层有一个父依赖spring-boot-dependencies,核心代码如下:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>1.5.14.RELEASE</version>
        <relativePath>../../spring-boot-dependencies</relativePath>
    </parent>

进行查看spring-boot-dencendencies底层源文件,核心代码具体如下:

    <properties>
        <!-- Dependency versions -->
        <activemq.version>5.14.5</activemq.version>
        <antlr2.version>2.7.7</antlr2.version>
        <appengine-sdk.version>1.9.64</appengine-sdk.version>
        <artemis.version>1.5.6</artemis.version>
        <aspectj.version>1.8.13</aspectj.version>
        <assertj.version>2.6.0</assertj.version>
        <atomikos.version>3.9.3</atomikos.version>
        <bitronix.version>2.1.4</bitronix.version>
...

从spring-boot-dependencies底层源文件可以看出,该文件通过标签对一些常用技术框架的依赖文件进行了统一版本号管理,例如activemq、spring、tomcat等,都有与Spring Boot 1.5.14版本相匹配的版本,这也是pom.xml引入依赖文件不需要标注依赖文件版本号的原因。
需要说明的是,如果pom.xml引入的依赖文件不是spring-boot-starter-parent管理的,那么在pom.xml引入依赖文件时,需要使用标签指定依赖文件的版本号,

  1. spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,那么项目运行的JAR包是从何而来的?
    spring-boot-starter-web依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
    </dependencies>

由上述代码可以发现,spring-boot-starter-web依赖启动器的主要作用是提供Web开发场景所需的底层所有依赖。
正是如此,在pom.xml中映入spring-boot-starter-web依赖启动器时,就可以实现Web场景的开发,而不需要额外导入Tomcat服务器以及其他的Web依赖文件等。当然,这些引入的依赖文件的版本号还是由spring-boot-starter-parent父依赖进行统一管理。
Spring Boot 除了提供以上介绍的Web依赖启动器之后,还提供了其他很多开发场景的相关依赖,可以打开Spring Boot官方文档,搜索“Starters”关键子查询场景依赖启动器。



列出了Spring Boot官方提供的部分场景依赖启动器,这些依赖启动器适用于不同场景的开发,使用时只需要在pom.xml文件中导入对应的以来启动器即可。
需要说明,Spring Boot 官方并不是针对所有场景开发的技术都提供有场景启动器,例如Mybatis,Druid等等,Spring Boot 官方就没有提供有对应的依赖启动器,为了充分利用Spring Boot框架优势,在SpingBoot官方没有整合这些技术框架的情况下,Mybatis,Druid等技术框架所在的技术团队主动和Spring Boot 框架进行了整合,实现了各自的依赖启动器,例如mabatis-spring-boot-starter、druid-spring-boot-starter等。我们在pom.xml中引入这些第三方的依赖启动器的时候,切记要配置对应的版本号。

自动装配(启动流程)

概念:能够在我们添加jar包依赖的时候,自动为我们配置一些组件的相关配置,我们无需配置或者只需要少量配置就可以能够运行编写的项目。
问题:Spring Boot到底如何进行自动装配的,都把哪些组件进行了自动配置?
Spring Boot 应用的启动入口是@SpringBootApplication注解标注类中的main方法,@SpringBootApplication能够扫描Spring组件并自动配置Sppring Boot

@SpringBootApplication
public class SpringbootDemoApplication {
 public static void main(String[] args) {
 SpringApplication.run(SpringbootDemoApplication.class, args);
 }
}
//注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Target({ElementType.TYPE})
//表示注解的声明周期,runtime 运行时
@Retention(RetentionPolicy.RUNTIME)
//表示注解可以记录在javadoc中
@Documented
//表示可以被子类继承该注解
@Inherited
//标注该类为配置类
@SpringBootConfiguration
//启动自动配置功能
@EnableAutoConfiguration
//包扫描器
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}

从上述源码可以看出,@SpringBootApplication注解是一个组合注解,其中@Target,@Retention,@Documented,@Inherited是注解的元数据信息,主要看后面三个注解:@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan,关于这三个核心注解的相关说明具体如下:

1. @SpringBootConfiguration
@SpringBootConfiguration注解表示Spring Boot配置类,查看@SrpingBootConfiguration注解源码,如下

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}

上述源码可以看到,@SrpingBootConfiguration注解内部有一个核心注解@Configuration,该注解是Spring框架提供的,表示当前类是一个配置类(xml配置文件的注解表现形式),并可以被组件扫描器扫描的配置类,只不过@SrpingBootConfiguration是被SpringBoot重新封装命名。

2. @EnableAutoConfiguration
@EnableAutoConfiguratio注解表示开启自动配置功能,该注解是SpringBoot框架最重要的注解,也是实现自动化配置的注解。同样的,查看该注解的源码信息,如下

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//自动配置包
@AutoConfigurationPackage
//自动配置类扫描导入
@Import({EnableAutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

可以看到他是一个组合注解,Spring中很多Enable开头的注解,其作用就是借助@Import来收集并注册特定场景相关的bean,并加载到IOC容器中。@EnableAutoConfiguration就是借助@Import来收集所有符合自动配置的bean定义,并加载到IIOC容器。
下面,对这俩核心注解分别说明:
(1)@AutoConfigurationPackage
查看其源代码,如下

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//导入Registrar中注册的组件
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}

从上述源码可以看出,@AutoConfigurationPackage 注解的功能是由@Import注解实现,它是Spring框架的底层注解,它的作用是给容器导入某个组件类,例如@Import(AutoConfigurationPackages.Registrar.class),他就是将Register这个组件类导入到容器中,可以查看Registrar类中registerBeanDefintions方法,这个方法就是导入组件类的具体实现:

    @Order(-2147483648)
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//(new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()
            AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
        }
    }
}

上述源码可以看到,在RegisterBeanDefinitions()方法,使用Debug模式启动项目,可以看到选中的部分((new AutoConfigurationPackages.PackageImport(metadata)).getPackageName())就是com.lagou。也就是说,@AutoConfigurationPackage主要作用就是将主程序类所在包以及子包的组件扫描到spring 容器中。
因此在定义项目包结构时,要求定义的包结构非常规范,项目主程序启动类必须要定义在最外层的根目录位置,然后再根目录位置内部建立子包和类进行业务开发没这样才能够保证定义的类可以被组件扫描器扫描到。

(2)@Import({EnableAutoConfigurationImportSelector.class})
@Import({EnableAutoConfigurationImportSelector.class})
将EnableAutoConfigurationImportSelector这个类导入到Spring容器中,EnableAutoConfigurationImportSelector可以帮助springboot应用将所有的符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IOC容器(ApplicationContext)中。

  1. 继续观察EnableAutoConfigurationImportSelector这个类
public class EnableAutoConfigurationImportSelector extends AutoConfigurationImportSelector {
    public EnableAutoConfigurationImportSelector() {
    }

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass().equals(EnableAutoConfigurationImportSelector.class) ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }
}
  1. 再看下 AutoConfigurationImportSelector 类,通过源码分析这个类是通过selectImports这个方法告诉springboot需要导入哪些组件
public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            try {
//获取自动配置元信息,需要传入beanClassLoader这个类加载器
                AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
                AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
                List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
                configurations = this.removeDuplicates(configurations);
                configurations = this.sort(configurations, autoConfigurationMetadata);
                Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
                this.checkExcludedClasses(configurations, exclusions);
                configurations.removeAll(exclusions);
//过滤
                configurations = this.filter(configurations, autoConfigurationMetadata);
                this.fireAutoConfigurationImportEvents(configurations, exclusions);
                return (String[])configurations.toArray(new String[configurations.size()]);
            } catch (IOException var6) {
                throw new IllegalStateException(var6);
            }
        }
    }
  1. 深入研究loadMetadate方法
    public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
//文件为需要加载的配置类的路径
        return loadMetadata(classLoader, "META-INF/spring-autoconfigure-metadata.properties");
    }

    static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
        try {
//读取spring-boot-autoconfigure-1.5.14.RELEASE.jar包中的spring-autoconfigure-metadata.properties的信息生成url
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
            Properties properties = new Properties();
//解析urls枚举对象中的信息封装成properties对象并加载
            while(urls.hasMoreElements()) {
                properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource((URL)urls.nextElement())));
            }
//根据封装好的properties对象生成AutoConfigurationMetadata对象返回
            return loadMetadata(properties);
        } catch (IOException var4) {
            throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", var4);
        }
    }
  1. selectImports方法中还有一个方法getCandidateConfigurations方法贴一下源码:
   protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//这个方法需要传入两个参数getSpringFactoriesLoaderFactoryClass()和getBeanClassLoader()
//getSpringFactoriesLoaderFactoryClass()这个方法返回的是EnableAutoConfiguration.class
//getBeanClassLoader()返回的是beanClassLoader(类加载器)
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
    }
  1. 继续点开loadFactory方法
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();

        try {
//如果类加载器不为null,则加载器路径下spring.fatories文件,将其中设置的配置类的全路径信息封装 为Enumeration对象
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            ArrayList result = new ArrayList();

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
                String factoryClassNames = properties.getProperty(factoryClassName);
                result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
            }

            return result;
        } catch (IOException var8) {
            throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]", var8);
        }
    }

会去读取一个spring.factories的文件,读不到会报错,可以看下这个文件


# Auto Configure
//这边的EnableAutoConfiguration 就是上面的loadFactoryNames方法的getSpringFactoriesLoaderFactoryClass()
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
...

@EnableAutoConfiguration就是从classpasth中查找META-INF/spring.factories配置文件,并将其中的org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的配置项通过反射实例化为对应的标注了@Configuration的javaConfig形式的配置类,并加载到IOC容器中。
spring-autoconfigure-metadata.properties存储的是待自动装配候选类“的过滤条件,这个信息很重要,框架会根据里面的规则逐一对候选类进行计算看是否需要被自动装配进容器,并不是全部加载。
spring.factories这个文件存储了spring-boot所有默认支持的待自动装配候选类。
总结
springboot底层实现自动配置的步骤

  1. springboot应用启动
  2. @SpringBootApplication
  3. @EnableAutoConfiguratiom
  4. @AutoConfigurationPackage:这个组合注解主要是@Import(AutoConfigurationPackage.Registar.class),它通过Registrar类导入到容器中,而Registrar类作用是扫描著配置类同级目录以及子包,并将相应的组件导入到springboot创建管理的容器中。
  5. @Import(AutoConfigurationImportSelector.class):它通过将AutoConfigurationImportSelector类导入到容器中,AutoConfigurationImportSelector类作用是通过selectImports方法执行的过程中,会使用内部的工具类SpringFactoriesLoader,查找classpath上所有jar包中META-INF/spring.factories进行加载,实现将配置类信息交给SpringFactory加载器进行一系列的容器创建过程。

3. @ComponentScan注解
@ComponentScan注解具体扫描的包的根路径由Spring Boot主程序启动类的所在的包位置决定,在扫描过程中由前面介绍的@AutoConfigurationPackage注解进行解析,从而得到主程序启动类所在的包的具体位置

总结:

@SpringBootApplication
|-- @SpringBootConfiguration
//通过javaConfig的方式来添加组件到IOC容器中
|---- @Configuration
|-- @EnableAutoConfiguration
//自动配置包,和@ComponentScan扫描到的添加到IOC中
|---- @AutoConfigurationPackage
//Spring.factories所定义的bean添加到IOC容器中
|---- @Import(AutoConfigurationImportSelector
//包扫描
|-- @ComponentScan

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

推荐阅读更多精彩内容