Springboot启动原理之@SpringBootApplication

4,681 阅读5分钟

大家好我是初晨,之前写了很多关于SpringBoot的文章,相信大家已经感受到了SpringBoot相对于传统Spring带来的便捷,那么本篇文章我们就来分析一下SpringBoot带来的便捷到底便捷在哪

不知道大家有没有注意到,当我们创建一个springboot项目时,都会用到如下的启动类

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

从代码上来看,显然注解@SpringBootApplication和SpringApplication类和他的run方法为核心。

那么本篇文章我们先来分析一下@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 {
    ...
}

其中重要的三个注解,分别为@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan。下面我们来分别看一下。

一:@SpringBootConfiguration

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

我们发现他是应用了@Configuration。

@Configuration这个注解用于以JavaConfig的方式来定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。也是SpringBoot社区推荐使用的配置形式。

@Configuration与传统xml配置文件的区别:
  1. 文件结构
    传统xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
        xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
            http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
            http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd" default-lazy-init="false">
    
    
    </beans>

    采用@Configuration:

    @Configuration
    public class TestConfig {
    }

  2. bean的定义
    传统xml:

    <bean id="testService" class="TestServiceImpl">
    </bean>

    采用@Configuration:

    @Configuration
    public class TestConfig {
        @Bean
        public TestService testService(){
             return new TestServiceImpl();
        }  
    }

所以任何一个java类上使用了@Configuration,都代表他是一个配置类。
任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。

二:@ComponentScan

这个注解在spring中很重要,如果你理解了@ComponentScan,你就理解了spring。
大家都知道spring是一个依赖注入的框架,所有的内容都是围绕bean定义及其依赖关系。
但是spring并不知道你定义了哪些个bean,除非你告诉spring可以从哪里找到你定义的那些bean。
而@ComponentScan的作用就是告诉Spring可以从哪里找到定义的bean

通过basePackages属性可以控制@ComponentScan自动扫描的范围,如果没有指定@ComponentScan的扫描范围,那么默认的扫描范围是从声明@ComponentScan所在类的package进行扫描。

所以我们可以发现,创建的springboot项目目录结构一般是这样的


启动类一直在项目的根目录下,这样才能在不配置扫描范围的情况下扫描到所有定义的bean

三:@EnableAutoConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class[] exclude() default {};

    String[] excludeName() default {};
}

这里重要的注解有两个,分别是@AutoConfigurationPackage和@Import

@AutoConfigurationPackage

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}

这里发现他使用@Import注解引入了一个类Registrar

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    Registrar() {
    }

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

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

那么这个类是用来干嘛的呢?我们断点跟一下,看一下registerBeanDefinitions方法是做什么的。


这里发现new PackageImport(metadata).getPackageName()返回的返回了当前主程序类的同级以及子级的包组件。

这也证明了@ComponentScan默认扫描其所在类的package。

@Import({AutoConfigurationImportSelector.class})

接下来我们具体看一看AutoConfigurationImportSelector这个类

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    } else {
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
        AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

这个方法调用了getAutoConfigurationEntry方法

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        configurations = this.removeDuplicates(configurations);
        Set exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = this.filter(configurations, autoConfigurationMetadata);
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
}

这里我们重点看一下getCandidateConfigurations方法,先断点看一下方法的返回值


该方法返回的是需要实例化的类信息列表,有了他spring就可以通过类加载器将需要实例化的类加载到jvm中。
现在我们看一下该方法的代码,发现他是借用了SpringFactoriesLoader类的方法

protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List 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;
}

再来看一下loadFactoryNames方法

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

private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap result = (MultiValueMap)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        try {
            Enumeration urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            //....省略

            cache.put(classLoader, result);
            return result;
        } catch (IOException var13) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
        }
    }
}

发现他读取了一个名为spring.factories的文件


比如我们找一下redis的配置


点击去看一下

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    ......
}

@ConditionalOnClass({RedisOperations.class})    表示必须存在RedisOperations这个类,否则不解析该注解修饰的配置类。

再看看RedisProperties这个类

public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
    ......省略
}

是不是眼熟了!这个就是我们在使用redis时写在application.properties里面,需要配置redis的信息
对应的配置应为:

spring.redis.host=127.0.0.1
spring.redis.port=6379   
spring.redis.timeout=0
spring.redis.password=

同时也可以看到redis默认为我们配置了host和port属性。所以在配置redis时,如果端口号围为默认的6379,我们也可以不写的原因。


所以@EnableAutoConfiguration的大致原理就是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。

四:结尾

@SpringBootApplication我们分析完了,下篇文章我们分析SpringApplication这个类


对于springBoot还不了解的朋友可以看我的SpringBoot系列教程