Spring Boot 源码分析(一)

Spring Boot 简介

Spring Boot 采用约定大于配置的思想对应用程序进行默认配置,减少了大量的配置时间。

Spring Boot 包含的特性如下:

  • 创建可以独立运行的 Spring 应用。
  • 直接嵌入 Tomcat 或 Jetty 服务器,不需要部署 war 文件。
  • 提供推荐的基础pom文件来简化 Apache Maven配置。
  • 尽可能的根据项目依赖来自动配置 Spring 框架。
  • 提供可以直接在生产环境中使用的功能,如性能指标,应用信息,和应用健康检查。
  • 没有代码生成,也没有 xml 配置文件。

通过 Spring Boot,创建新的 Spring 应用变得非常容易,而且创建出的 Spring 应用符合通用的最佳实践。只需要简单几个步骤就可以创建出一个Web应用。对于 Spring Boot 的创建,请参考 Spring 官方文档

Spring Boot 加载

因为 Spring Boot 内置了 Tomcat 或 Jetty 服务器,不需要直接部署 war 文件,所以 Spring Boot 的程序入口为一个普通的主函数,主函数类如下:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

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

看似和普通的程序没有什么区别,其实它最主要的步骤都是通过注解@SpringBootApplication和方法SpringApplication.run()完成了。

其实所有的依赖在这一步就可以完成注入,主要的步骤是:Spring 读取所有依赖中的META-INF/spring.factories文件,该文件指明了那些依赖可以被自动加载,然后根据ImportSelector类选择加载那些依赖,使用ConditionOn系列注解排除掉不需要的配置文件,最后将剩余的配置文件所代表的 Bean 加载到 Spring 的 IOC 容器中去。

META-INF/spring.factories文件的读取

Spring 所有的配置文件都分布在 jar 包的META-INF/spring.factories中,如下:

image.png

每一个META-INF/spring.factories为一个键值对列表,截取了部分spring-boot jar包下的spring.factories文件,如下:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.web.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener

在程序中,这段列表会被解析为Map<K, List<V>>的格式,即一个MutiValueMap(一个健对应多个值的Map),键和每个值都是一个类的全限定名。
主函数中,只有一个方法,即SpringApplication.run(),如下:

@SpringBootApplication
public class DemoApplication {

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

}

我们来看一个读取META-INF/spring.factories的例子。

在程序初始化时,即运行new SpringApplication()时,就会读取一次配置文件,我们来分析是如何读取配置文件的。

SpringApplication.run(DemoApplication.class, args)方法是SpringApplication类提供的一个静态方法,调用的是run(new Class<?>[] { primarySource }, args)方法。如下:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args);
}

其中,读取配置文件的操作就发生在SpringApplication类的实例化过程中。实例化的代码如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 把初始化的初始化器存到数组中
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 把初始化的监听器加入到数组中
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 获得主类
    this.mainApplicationClass = deduceMainApplicationClass();
}

getSpringFactoriesInstances(ApplicationContextInitializer.class)中 Spring 读取了spring.factories文件。如下:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 直接利用反射创建实例
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

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

loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader)函数:该函数是SpringFactoriesLoader类提供的一个静态方法,主要用于读取factoryClass的类全限定名这个键在spring.factories中的值列表
loadSpringFactories(classLoader)函数读取所有的配置文件中的属性的键值对,并把值缓存到一个cache中,在需要进行读取的时候读取。
执行完loadSpringFactories(classLoader)函数后,spring.factories配置文件中的键值对将变成如下形式缓存到cache中:

 // 以属性文件的 key 作为键,value 作为值,注意这个值用一个链表表示,因为可能一个键对应多个值
cache : 
    key = classloader.toString()
    ->
    value = {
        "org.springframework.boot.env.PropertySourceLoader" 
            -> ["org.springframework.boot.env.PropertiesPropertySourceLoader", "org.springframework.boot.env.YamlPropertySourceLoader"],
        "org.springframework.boot.SpringApplicationRunListener" 
            -> ["org.springframework.boot.context.event.EventPublishingRunListener"],
        "org.springframework.boot.SpringBootExceptionReporter" 
            -> ["org.springframework.boot.diagnostics.FailureAnalyzers"],
        "org.springframework.context.ApplicationContextInitializer" 
            -> ["org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer", "org.springframework.boot.context.ContextIdApplicationContextInitializer", "org.springframework.boot.context.config.DelegatingApplicationContextInitializer", "org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer"],
        ....只列举部分,其他的省略
    }

具体加载和读取配置文件的源码如下所示:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    } try {
        Enumeration<URL> urls = (classLoader != null ?
            // FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"    classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        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);
    }
    
    // output
    // like:
    // org.springframework.boot.autoconfigure.EnableAutoConfiguration -> {LinkedList@1446} size = 118
    //   |
    //   |- org.springframework.boot.autoconfigure.EnableAutoConfiguration
        
}

以指定的factoryClass的类全限定名为键,尝试从缓存中取出值,对于ApplicationContextInitializer.class这个factoryClass来说,会取出所有键为org.springframework.context.ApplicationContextInitializer的值,于是取出了:

"org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer"
"org.springframework.boot.context.ContextIdApplicationContextInitializer"
"org.springframework.boot.context.config.DelegatingApplicationContextInitializer"
"org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer"

总结

  • SpringFactoriesLoader类的静态方法实现了依赖文件的读取,读取所有的配置类的全限定名名称。
  • SpringApplication 在初始化时就完成了最基本配置的读取和实例化。
  • 而这个cache在 Spring 上下文加载当中非常重要。

Spring 上下文加载

前面描述了如何将配置文件所在的类加载到内存中。那么 Spring Boot 的自动化配置时如何实现的呢?
在 Spring Mvc 中,我们需要手工配置试图解析器等基础配置,需要如下配置:

@Bean
public ViewResolver viewResolver() {
    return new ThymeleafViewResolver();
}

既然 Spring Boot 可以实现自动化配置,那么在 Spring Boot 中,则一定也有类似的语句加载默认的试图解析器。

注解简介

先介绍几个 Spring Boot 的配置注解

  • @SpringBootApplication
    @SpringBootApplication是一个复合注解,定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 注解一
@EnableAutoConfiguration // 注解二
// 没有配置 basepackages,默认代表当前包及其子包
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) // 注解三
public @interface SpringBootApplication {
    // ...
}

注解一代表配置注解,定义如下:

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

注解一可以把由@SpringBootApplication注解修饰的类作为配置类来使用,在DemoApplication类中由@Bean注解修饰的代码都会被注入到 Spring 的 Ioc 容器中,由 Spring 来统一管理。
注解三会自动扫描该类所在包,将各种组件注入到 Ioc 容器中,所以我们也可以在DemoApplication类上增加@Controller等注解,将其作为一个 Controller 。

  • @EnableAutoConfiguration
    @EnableAutoConfiguration定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage  // 注解一
@Import(AutoConfigurationImportSelector.class) // 注解二
public @interface EnableAutoConfiguration {
    // ...
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。