SpringBoot自动配置原理

1.spring boot启动的时候加载主配置类,开启了自动配置功能@EnableAutoConfiguration,先看一下启动类的main方法

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }

  其中在这个  prepareContext(context, environment, listeners, applicationArguments,printedBanner)  方法中完成了SpringBoot 的main方法中的启动类在Spring容器中的注册,(相当于以前的xml文件),refreshContext(context)这个方法就是初始化spring容器,如果i要深入的可以去看一下Spring容器初始化, 同时去解析主类,由此去解析主类上的注解

  SpringBootApplication 本质上是由 3 个注解组成,分别是

  1. @Configuration
  2. @EnableAutoConfiguration
  3. @ComponentScan
@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 {
    
}

  我们可以直接用这三个注解也可以启动 springboot 应用,只是每次配置三个注解比较繁琐,所以直接用一个复合注解更方便些。然后仔细观察者三个注解,除了 EnableAutoConfiguration可能稍微陌生一点,其他两个注解使用得都很多.

@Configuration:

  Configuration 这个注解大家应该有用过,它是 JavaConfig形式的基于 Spring IOC 容器的配置类使用的一种注解。因为 SpringBoot 本质上就是一个 spring 应用,所以通过这个注解来加载 IOC 容器的配置是很正常的。所以在启动类里面标注了@Configuration,意味着它其实也是一个 IoC容器的配置类。

  传统意义上的 spring 应用都是基于 xml 形式来配置 bean的依赖关系。然后通过 spring 容器在启动的时候,把 bean进行初始化并且,如果 bean 之间存在依赖关系,则分析这些已经在 IoC 容器中的 bean 根据依赖关系进行组装。直到 Java5 中,引入了 Annotations 这个特性,Spring 框架也紧随大流并且推出了基于 Java 代码和 Annotation 元信息的依赖关系绑定描述的方式。也就是 JavaConfig。从 spring3 开始,spring 就支持了两种 bean 的配置方式,一种是基于 xml 文件方式、另一种就是 JavaConfig任何一个标注了@Configuration 的 Java 类定义都是一个JavaConfig 配置类。而在这个配置类中,任何标注了@Bean 的方法,它的返回值都会作为 Bean 定义注册到Spring 的 IOC 容器,方法名默认成为这个 bean 的 id。可以看一下下面这个例子:

@Configuration
public class ConfigurationDemo {
    @Bean
    public DemoClass demoClass(){
        return new DemoClass();
    }
}
public class DemoClass {
    public void say(){
        System.out.println("Say: Hello Wuzz");
    }
}

@ComponentScan(basePackages = "com.wuzz.demo.auto.configuration.demo.configuration")
public class ConfigurationMain {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext=new AnnotationConfigApplicationContext(ConfigurationMain.class);

        String[] defNames=applicationContext.getBeanDefinitionNames();
        for(int i=0;i<defNames.length;i++){
            System.out.println(defNames[i]);
        }
    }
}

  运行main方法可以看到:

@ComponentScan:

  ComponentScan 这个注解是大家接触得最多的了,相当于 xml 配置文件中的<context:component-scan>。 它的主要作用就是扫描指定路径下的标识了需要装配的类,自动装配到 spring 的 Ioc 容器中。标识需要装配的类的形式主要是:@Component、@Repository、@Service、@Controller 这类的注解标识的类。ComponentScan 默认会扫描当前 package 下的的所有加了相关注解标识的类到 IoC 容器中;

@EnableAutoConfiguration

  我们把 EnableAutoConfiguration 放在最后讲的目的并不是说它是一个新的东西,只是他对于 springboot 来说意义重大。仍然是在 spring3.1 版本中,提供了一系列的@Enable 开头的注解,Enable 注解应该是在 JavaConfig 框架上更进一步的完善,是的用户在使用 spring 相关的框架是,避免配置大量的代码从而降低使用的难度。比如说@EnableScheduling,开启计划任务的支持;找到 EnableAutoConfiguration,我们可以看到每一个涉及到 Enable 开头的注解,都会带有一个@Import 的注解。

  Import 注解:import 注解是什么意思呢? 联想到 xml 形式下有一个<import resource/> 形式的注解,就明白它的作用了。import 就是把多个分来的容器配置合并在一个配置中。在JavaConfig 中所表达的意义是一样的。

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

  EnableAutoConfiguration 的 主 要 作 用 其 实 就 是 帮 助springboot 应用把所有符合条件的@Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器中。再回到 EnableAutoConfiguration 这个注解中,我们发现它的 import 是这样:

@Import({AutoConfigurationImportSelector.class})

AutoConfigurationImportSelector 是什么?

  Enable注解不仅仅可以像前面演示的案例一样很简单的实现多个 Configuration 的整合,还可以实现一些复杂的场景,比如可以根据上下文来激活不同类型的 bean,@Import 注解可以配置三种不同的 class

  1. 第一种就是前面演示过的,基于普通 bean 或者带有@Configuration 的 bean 进行诸如
  2. 实现 ImportSelector 接口进行动态注入
  3. 实现 ImportBeanDefinitionRegistrar 接口进行动态注入

  第一种上面已经贴出来Demo程序了 ,现在先来看一下@Import 的基本使用:

//包:com.wuzz.demo.auto.configuration.demo.importdemo.currentpackage
//类:
public class DefaultBean {
}
@Import(OtherConfig.class)
@Configuration
public class SpringConfig {
    @Bean
    public DefaultBean defaultBean(){
        return new DefaultBean();
    }
}
public class SecondMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext=new
                AnnotationConfigApplicationContext(SpringConfig.class);
        String[] defNames=applicationContext.getBeanDefinitionNames();
        for(int i=0;i<defNames.length;i++){
            System.out.println(defNames[i]);
        }
    }
}
//包:com.wuzz.demo.auto.configuration.demo.importdemo.otherpackage
//类:
public class OtherBean {
}
@Configuration
public class OtherConfig {
    @Bean
    public OtherBean otherBean(){
        return new OtherBean();
    }
}

  以上就是@Import 的简单应用 ,但是我们如果希望实现动态的进行注入,那么需要我们去实现 ImportSelector 接口,我们可以这么做:

//包:com.wuzz.demo.auto.configuration.demo.importselector
//类:
public class CacheService {
}
public class CacheImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { //可以获取到注解都元数据,即我们定义的 exclude Map<String,Object> attributes= annotationMetadata.getAnnotationAttributes(EnableDefineService.class.getName()); //动态注入bean :自己去实现判断逻辑实现动态配置 return new String[]{CacheService.class.getName()}; //我这返回的是一个固定的CacheService } }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({CacheImportSelector.
class}) // public @interface EnableDefineService { //配置一些方法 Class<?>[] exclude() default {}; } @SpringBootApplication @EnableDefineService(exclude = CacheService.class) public class EnableDemoMain { public static void main(String[] args) { ConfigurableApplicationContext ca= SpringApplication.run(EnableDemoMain.class,args); System.out.println(ca.getBean(CacheService.class)); } }

  这样子就基本上完成了用 ImportSelector 接口实现Bean的动态注入。

  继续看 ImportBeanDefinitionRegistrar :

//包:com.wuzz.demo.auto.configuration.demo.importselector
//类:
public class LoggerService {
}
public class LoggerDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) { //增加相关动态校验注入,我这里写死 Class beanClass=LoggerService.class; RootBeanDefinition beanDefinition=new RootBeanDefinition(beanClass); String beanName= StringUtils.uncapitalize(beanClass.getSimpleName()); beanDefinitionRegistry.registerBeanDefinition(beanName,beanDefinition); } } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({LoggerDefinitionRegistrar.class}) // public @interface EnableDefineService { //配置一些方法 Class<?>[] exclude() default {}; }
@SpringBootApplication @EnableDefineService(exclude
= CacheService.class) public class EnableDemoMain { public static void main(String[] args) { ConfigurableApplicationContext ca= SpringApplication.run(EnableDemoMain.class,args); System.out.println(ca.getBean(LoggerService.class)); } }

  了解了 ImportSelector 和 ImportBeanDefinitionRegistrar后,对于 EnableAutoConfiguration 的理解就容易一些了它会通过 import 导入第三方提供的 bean 的配置类:

@Import({AutoConfigurationImportSelector.class})

  从名字来看,可以猜到它是基于 ImportSelector 来实现基于动态 bean 的加载功能。@Enable*注解的工作原理 ImportSelector 接口selectImports 返回的数组(类的全类名)都会被纳入到spring 容器中。那么可以猜想到这里的实现原理也一定是一样的,定位到AutoConfigurationImportSelector 这个类中的 selectImports 方法:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        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 StringUtils.toStringArray(configurations);
  }

  本质上来说,其实 EnableAutoConfiguration 会帮助springboot 应用把所有符合@Configuration 配置都加载到当前 SpringBoot 创建的 IoC 容器,而这里面借助了Spring 框架提供的一个工具类 SpringFactoriesLoader 的支持。以及用到了 Spring 提供的条件注解@Conditional,选择性的针对需要加载的 bean 进行条件过滤。

2.通过@EnableAutoConfiguration进行Bean的自动注入,其中 AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader) 通过类加载器获取默认配置数据,跟进去代码我们可以发现:

protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) { return loadMetadata(classLoader, PATH); }

  通过类加载器去扫描对应路径下的配置文件,进行默认配置的载入,这里的载入只是初步的,在 selectImports 方法里有个  filter 方法会对配置文件里的条件进行深度的过滤。会结合前面的元数据进行过滤,为什么要过滤呢? 原因是很多的@Configuration 其实是依托于其他的框架来加载的,如果当前的 classpath 环境下没有相关联的依赖,则意味着这些类没必要进行加载,所以,通过这种条件过滤可以有效的减少@configuration 类的数量从而降低SpringBoot 的启动时间。这里我们可以写一个例子来说明这个文件的妙用,先创建一个新的项目demo-core,然后工程下文件如下:

//包:com.wuzz.demo
//类:
@Configuration
public class DemoConfig {

    @Bean
    public DemoCoredemoCore(){
        return new DemoCore();
    }
}
public class DemoCore{
    public String study(){
        System.out.println("good good study, day day up");
        return "wuzz.demo.com";
    }
}
// META-INF.spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wuzz.demo.DemoConfig
// META-INF.spring-autoconfigure-metadata.properties
com.wuzz.demo.DemoConfig.ConditionalOnClass=com.wuzz.demo.auto.configuration.demo.importselector.TestClass

  然后将其打成jar包,然后在ImportSelector工程中引用该jar

<dependency>
    <groupId>com.wuzz.demo</groupId>
    <artifactId>demo-core</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

  最后在Main程序中添加:

System.out.println(ca.getBean(DemoCore.class).study());

  然后启动程序会报错,即容器中无法找到该Bean,但是在我们 com.wuzz.demo.auto.configuration.demo.importselector.TestClass 这个类存在的时候,便会打印出  good good study, day day up,或许你会联想到 starter的导入是否与之有关,答案是肯定的。

  接下去AutoConfigurationImportSelector给容器中导入了一系列的组件通过List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);获取候选的配置,通过SpringFactoriesLoader.loadFactoryNames() 扫描所有 jar 包类路径下的META-INF/spring.factories,将扫描到的文件内容包装成properties对象,从properties中获取到EnableAutoConfiguration.class类名对应的值,然后把他们添加到容器中。SpringFactoriesLoader 这个工具类的使用。它其实和java 中的 SPI 机制的原理是一样的,不过它比 SPI 更好的点在于不会一次性加载所有的类,而是根据 key 进行加载。SpringFactoriesLoader 的 作 用 是 从classpath/META-INF/spring.factories 文件中,根据 key 来加载对应的类到 spring IoC 容器中。将类路径下  META-INF/spring.factories 里面配置的 所有EnableAutoConfiguration的值添加到容器中。以下就是该文件中的一部分代码:

# Auto Configure
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,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
等等一些列的类

  每一个这样的 XXXXAutoConfiguration类都是容器中的一个组件,都加入到容器中,用他们来自动配置

3.每个自动配置类@EnableAutoConfiguration进行自动配置功能

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

    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    /**
     * Exclude specific auto-configuration classes such that they will never be applied.
     * @return the classes to exclude
     */
    Class<?>[] exclude() default {};

    /**
     * Exclude specific auto-configuration class names such that they will never be
     * applied.
     * @return the class names to exclude
     * @since 1.3.0
     */
    String[] excludeName() default {};

}

@AutoConfigurationPackage:自动配置包

  @Import(AutoConfigurationPackages.Registrar.class)

  spring的底层注解@Import,给容器导入一个组件()

  AutoConfigurationPackages.Registrar.class:是将@SpringBootConfiguration 标注的类所在的包下的子包里面所有的组建扫描到spring容器中。

  @Import(AutoConfigurationImportSelector.class):导入组件;

 将所有需要导入的组件以全类名的方式返回:这些组件就会被添加到容器中

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        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 StringUtils.toStringArray(configurations);
}

configurations:里面存储的数组信息就是需要spring自动配置的全类名。(XXX.AutoConfiguration)。 

4.以HttpEncodingAutoConfiguration为例解释自动配置原理

@Configuration // 这是一个配置类 也可以给容器中添加组件
// 启用指定类的ConfigurationProperties功能
// 将配置文件中对应的值跟HttpEncodingProperties绑定,并将HttpEncodingProperties加入IOC容器中
@EnableConfigurationProperties(HttpEncodingProperties.class)
// spring底层@Conditional注解,根据不同的条件。如果满足指定条件,整个配置类才生效
// 判断当前应用是否web应用
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// 判断当前项目有没有这个类CharacterEncodingFilter:MVC中乱码解决的过滤器
@ConditionalOnClass(CharacterEncodingFilter.class)
// 判断配置文件中是否存在某个配置spring.http.encoding.enabled,如果不存在也是成立的
// 也会通过 matchIfMissing = true判断默认生效
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

    private final HttpEncodingProperties properties;

    public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
        this.properties = properties;
    }

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
        return filter;
}

根据当前不同的条件判断,决定当前配置类是否生效?一旦配置类生效:这个配置类就会给容器中添加各种组件,组件的属性来源于对应的XXXProperties类中获取的,这些类的每一个属性都来源于配置文件。

5.所有配置文件中能配置的属性在 xxxProperties类中封装着,配置文件中能配什么,这里就有什么。

//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingProperties {}

1.spring boot启动会加载大量的自动配置类

2.我们看我们需要的功能spring boot 默认写好的自动配置类有没有Properties

3.我们看这个自动配置类中配置了那些组件,没有的话自己编写

4.给容器中自动配置类添加组件时候,会从prperties类中获取某些属性,我们就可以在配置文件中指定他的值

  XXXAutoConfiguration自动配置类给容器中添加组件就会有对应的XXXProperties来封装配置文件中相关的属性每个自动配置类几乎都存在@ConditionalXXX的拓展注解,底层是spring的@Conditional的注解,主要满足指定条件的自动配置类才会加载生效。可以通过主配置文件加上配置debug=true;来查看自动配置报告。

SpringApplication的run方法的实现是我们主要探索入口,该方法的主要流程大体可以归纳如下:

1)如果我们使用的是SpringApplication的静态运行方法,那么,这个方法里面首先要创建一个SpringApplication对象实例,然后调用这个创建好的SpringApplication的实例方法。在SpringApplication实例初始化的时候,它会提前做几件事情:

  • 根据类路径里面是否存在某个特征类org.springframework.web.context.ConfigurableWebApplicationContext来决定是否应该创建一个为Web应用程序使用的ApplicationContext类型。

  • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationContextInitializer

  • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationListener

  • 初步并设置main方法的定义类。

2)SpringApplication实例初始化完成并完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行所有通过SpringFactoriesLoader可以查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。

3)创建并配置当前Spring Boot应用将要使用的环境(包括配置要使用的PropertySource以及Profile)。

4)遍历调用所有SpringApplicationRunListenerenvironmentPrepared()方法,告诉他们:“当前SpringBoot应用使用的环境准备好了咯!”。

5)如果SpringApplication的showBanner属性被设置为true,则打印banner。

6)根据用户是否明确设置了applicationContextClass类型以及初始化阶段的结果,决定该为当前SpringBoot应用程序创建的类型的ApplicationContext并创建完成,然后根据条件决定是否添加ShutdownHook,决定是否使用自定义的BeanNameGenerator,决定是否使用自定义的ResourceLoader,当然,最重要的,将之前准备好的环境设置给创建好的ApplicationContext使用。

7)ApplicationContext创建好之后,SpringApplication会再次替换Spring-FactoriesLoader,查找并加载classpath中所有可用的ApplicationContext-Initializer,然后遍历调用这些ApplicationContextInitializerinitialize(applicationContext)方法来对已经创建好的ApplicationContext进行进一步的处理。

8)遍历调用所有SpringApplicationRunListenercontextPrepared()方法。

9)最核心的一步,将先通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext

10)遍历调用所有SpringApplicationRunListenercontextLoaded()方法。

11)调用ApplicationContextrefresh()方法,完成IoC容器可用的最后一道工序。

12)发现当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们。

13)正常情况下,遍历执行SpringApplicationRunListenerfinished()方法,(如果整个过程出现异常,则依然调用所有SpringApplicationRunListenerfinished()方法,只不过这种情况下发生异常信息一并替换处理)

posted @ 2018-06-07 17:05  吴振照  阅读(551)  评论(0编辑  收藏