Spring boot 启动流程及外部化配置

news2024/12/29 11:50:03

平时我们开发Spring boot 项目的时候,一个SpringBootApplication注解加一个main方法就可以启动服务器运行起来,那它到底是怎么运行起来的呢?

Main 入口

我们首先从main方法来看源码,逐步深入:

@SpringBootApplication
public class AutoDemoApplication {

    public static void main(String[] args) {
        // run方法为入口
        SpringApplication.run(AutoDemoApplication.class, args);
    }

}
// 继续调用 run 方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class[]{primarySource}, args);
    }
// 这个run 方法也很简单 做了两件事
// 1. 实例化 SpringApplication 对象
// 2. 执行对象的 run 方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return (new SpringApplication(primarySources)).run(args);
}

SpringApplication

那我们来看下SpringApplication这个对象里面做了什么:

public SpringApplication(Class<?>... primarySources) {
    this((ResourceLoader)null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.addConversionService = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = Collections.emptySet();
    this.isCustomEnvironment = false;
    this.lazyInitialization = false;
    this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
    this.applicationStartup = ApplicationStartup.DEFAULT;
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 先把主类保存起来
    this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
    // 判断运行项目的类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    
 // 初始化器:扫描当前路径下 META-INF/spring.factories 文件,加载ApplicationContextInitializer接口实例
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 设置监听器: 加载 ApplicationListener 接口实例
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

判断运行环境

构造方法内部会调用WebApplicationType.deduceFromClasspath()方法获得应用类型并设置当前应用是普通WEB应用、响应式web应用(REACTIVE)还是非web应用。

this.webApplicationType = WebApplicationType.deduceFromClasspath();

deduceFromClasspath方法由枚举类WebApplicationType提供,具体实现如下:

static WebApplicationType deduceFromClasspath() {
    // 当classpath 下只存在 org.springframework.web.reactive.DispatcherHandler
    // 不存在 org.springframework.web.servlet.DispatcherServlet 和 org.glassfish.jersey.servlet.ServletContainer
    // 则运行模式为reactive 非阻塞模式
    if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
        return REACTIVE;
    } else {
        String[] var0 = SERVLET_INDICATOR_CLASSES;
        int var1 = var0.length;

        for(int var2 = 0; var2 < var1; ++var2) {
            String className = var0[var2];
            if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
                // 普通应用程序
                return NONE;
            }
        }
        // servlet 程序,需要加载并启动内嵌的web服务
        return SERVLET;
    }
}

推断的过程主要使用了ClassUtils.isPresent方法,用来判断指定类名是否存在,是否可以进行加载。源代码如下:

public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
    try {
        forName(className, classLoader);
        return true;
    } catch (IllegalAccessError var3) {
        throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + className + "]: " + var3.getMessage(), var3);
    } catch (Throwable var4) {
        return false;
    }
}

isPresent方法调用了forName方法,如果在调用过程中发生错误则返回false,代表目标类不存在。否则返回true。

我们看下 forName的部分代码:

public static Class<?> forName(String name, @Nullable ClassLoader classLoader) throws ClassNotFoundException, LinkageError {
    // 省略部分代码
    
    		ClassLoader clToUse = classLoader;
            if (classLoader == null) {
                // 如果为空则使用默认类加载器
                clToUse = getDefaultClassLoader();
            }

            try {
                // 返回加载的 class 
                return Class.forName(name, false, clToUse);
            } catch (ClassNotFoundException var9) {
                // 如果加载异常 则尝试加载内部类
                int lastDotIndex = name.lastIndexOf(46);
                if (lastDotIndex != -1) {
                    // 拼接内部类
                    String nestedClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1);

                    try {
                        return Class.forName(nestedClassName, false, clToUse);
                    } catch (ClassNotFoundException var8) {
                    }
                }

                throw var9;
            }
}

通过以上核心代码,可得知 forName方法主要做的事情就是获得类加载器,尝试直接加载类,如果失败则尝试加载该类的内部类,如果依旧失败则抛出异常。

尝试加载的类其实就是去扫描项目中引入的依赖,看看是否能加载到对应的类。

因此,整个应用类型的判断分为以下几个步骤:

  • spring boot 调用 SpringApplication构造方法
  • SpringApplication构造方法调用枚举类的deduceFromClasspath方法
  • deduceFromClasspath方法通过ClassUtils.isPresent来判断是否能成功加载指定的类
  • ClassUtils.isPresent方法通过调用forName方法来判断是否能成功加载

初始化器和监听器

在构造器中我们可以看到尝试去加载两个接口实例,都是利用SPI机制扫描META-INF/spring.factories文件实现的。

  • ApplicationContextInitializer

    这个类当spring boot 上下文Context初始化完成后会调用

  • ApplicationListener

    当 spring boot 启动时 事件 change 都会触发;

    该类是重中之重,比如 dubbo 、nacos集成 spring boot ,都是通过它进行拓展的。

我们可以通过打断点的方式来查看加载流程:

根据 接口名 作为 key 值去加载实现类,加载的时候可能分布在多个 jar 包里面,如果我们自己自定义的话,也可以被加载进去。

在这里插入图片描述

这两个类都是在spring boot 启动前执行的,能帮我们进行一些拓展操作。我们可以自定义初始化器,按照SPI机制即可:实现相关接口,同时在指定路径下创建文件。

  • 我们首先创建一个初始化器以及监听器实现类:
public class StarterApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println("applicationContext 初始化完成 ... ");
    }
}
public class StarterApplicationListener implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println(event.toString());
        System.out.println("ApplicationListener .... " + System.currentTimeMillis());
    }
}
  • 创建指定文件 META-INF/spring.factories

    key 值为接口全限定名 :value 值为实现类全限定名

org.springframework.context.ApplicationContextInitializer=\
 com.example.autodemo.listener.StarterApplicationContextInitializer

org.springframework.context.ApplicationListener=\
  com.example.autodemo.listener.StarterApplicationListener

接口名实在找不到,点进类里面自己拼装一下即可。

在这里插入图片描述

  • 重新运行项目进行测试

    可以看到我们自定义的初始化器和监听器加载成功
    在这里插入图片描述

总结

如上就是 SpringApplication 初始化的代码,基本上没做啥事,主要就是判断了一下运行环境以及利用SPI机制加载META-INF/spring.factories下定义的事件监听器接口实现类。

执行 run 方法

我们直接看 run 方法的源码,逐步分析:

public ConfigurableApplicationContext run(String... args) {
    long startTime = System.nanoTime();
    DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
    ConfigurableApplicationContext context = null;
    // 不是重点,设置环境变量
    this.configureHeadlessProperty();
    // 获取事件监听器 SpringApplicationRunListeners 类型
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    // 执行 start 方法
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    try {
        // 封装参数
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        
        // 重点来了嗷~~~
        // 准备容器环境,会加载配置文件
        // 在这个方法里面会调用所有监听器Listeners的 onApplicationEvent(event),其中就有一个与配置文件相关的监听器被加载:ConfigFileApplicationListener
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        
        // 判断环境的值,并设置一些环境的值
        this.configureIgnoreBeanInfo(environment);
        
        // 打印 banner 可以自定义
        Banner printedBanner = this.printBanner(environment);
        
        // 根据项目类型创建上下文 
        context = this.createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        
        // 准备上下文 
        this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        
        // 重点流程!!!
        // spring 的启动代码 这里去扫描并且初始化单实例 bean ,同时启动了内置web容器
        this.refreshContext(context);
        
        // 空的
        this.afterRefresh(context, applicationArguments);
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
        }

        // 执行 AppilcationRunListeners 中的started方法
        listeners.started(context, timeTakenToStartup);
        // 执行 runner
        this.callRunners(context, applicationArguments);
    } catch (Throwable var12) {
        this.handleRunFailure(context, var12, listeners);
        throw new IllegalStateException(var12);
    }

    try {
        Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
        listeners.ready(context, timeTakenToReady);
        return context;
    } catch (Throwable var11) {
        this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
        throw new IllegalStateException(var11);
    }
}

环境变量及配置

我们直接来看环境变量相关的实现流程:

prepareEnvironment

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    // 创建和配置环境变量
    ConfigurableEnvironment environment = this.getOrCreateEnvironment();
    this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach((Environment)environment);
    listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);
    DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment);
    Assert.state(!((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");
    this.bindToSpringApplication((ConfigurableEnvironment)environment);
    if (!this.isCustomEnvironment) {
        environment = this.convertEnvironment((ConfigurableEnvironment)environment);
    }

    ConfigurationPropertySources.attach((Environment)environment);
    return (ConfigurableEnvironment)environment;
}

刚进来我们就可以看到一个核心方法,看名字我们就可以知道这里是去获取或者创建环境,我们看看其源码。

getOrCreateEnvironment

该方法根据 webApplicationType判断当前项目是什么类型,这部分我们在SpringAlication中看到过:WebApplicationType.deduceFromClasspath()

总共定义了三个类型:

  • None :应用程序不作为WEB程序启动,不启动内嵌的服务。
  • SERVLET:应用程序基于servlet的web应用启动,需启动内嵌servlet服务。
  • REAVTIVE:应用程序以响应式web应用启动,需启动内嵌响应式web服务。
private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    } else {
        switch(this.webApplicationType) {
        case SERVLET:
            return new ApplicationServletEnvironment();
        case REACTIVE:
            return new ApplicationReactiveWebEnvironment();
        default:
            return new ApplicationEnvironment();
        }
    }
}

由于我们是 servlet 项目,所以应该调用 ApplicationServletEnvironment,如果版本不一致,名字应该是:StandardServletEnvironment()方法。

ApplicationServletEnvironment继承了 StandardServletEnvironment类,StandardServletEnvironment又继承了StandardEnvironment类,StandardEnvironment又继承了AbstractEnvironment类…(确实很多,但是到这里真没了…)

最终在构造方法中调用了customizePropertySources 方法:

public AbstractEnvironment() {
    this(new MutablePropertySources());
}

protected AbstractEnvironment(MutablePropertySources propertySources) {
    this.logger = LogFactory.getLog(this.getClass());
    this.activeProfiles = new LinkedHashSet();
    this.defaultProfiles = new LinkedHashSet(this.getReservedDefaultProfiles());
    this.propertySources = propertySources;
    this.propertyResolver = this.createPropertyResolver(propertySources);
    this.customizePropertySources(propertySources);
}

这里我们进去看,会发现是个空方法:

protected void customizePropertySources(MutablePropertySources propertySources) {
}

既然这里是空方法,那肯定是调用了StandardServletEnvironment中的方法:

protected void customizePropertySources(MutablePropertySources propertySources) {
    propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
    propertySources.addLast(new StubPropertySource("servletContextInitParams"));
    if (jndiPresent && JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
        propertySources.addLast(new JndiPropertySource("jndiProperties"));
    }
    // 调用父类
    super.customizePropertySources(propertySources);
}

到这里位置,我们的propertySources中就加载了servletConfigInitParams,servletContextInitParams,systemProperties,systemEnvironment,其中后两个是从父类中添加的。

看到这里加载环境全部结束,有没有发现一个问题?application配置文件好像没有加进来?不要急,其他相关的配置文件都是通过监听器拓展加入进来的。

看了上面的代码,那我们应该如何将application配置文件添加到propertySources呢?是不是应该有如下几步:

  • 找文件:去磁盘中查找对应的配置文件(这几个目录下:classpath、classpath/config、/、/config/、/*/config)
  • 读文件内容:IO流
  • 解析并封装成对象
  • propertySources.addLast添加到最后

需要注意的是springboot 只会加载application命名的文件,大家以为的bootstrap在这里也不会被加载,不要问为什么,那是 cloud 中的文件,也是通过自定义监听器加进去的,梳理完流程后我们可以自己手写一个试试。

我们重新回到prepareEnvironment中,看看监听器相关,最后会走到ConfigFileApplicationListener配置文件的监听器。入口在listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);

environmentPrepared

environmentPrepared

void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
    this.doWithListeners("spring.boot.application.environment-prepared", (listener) -> {
        listener.environmentPrepared(bootstrapContext, environment);
    });
}

我们进入监听器的environmentPrepared方法,最终会进入到 SpringApplicationRunListener接口,这个接口在run方法中的getRunListeners里面获取,最终在spring.factories里面加载实现类:EventPublishingRunListener,执行它里面的environmentPrepared方法:

public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
    this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
}

multicastEvent

public void multicastEvent(ApplicationEvent event) {
    this.multicastEvent(event, this.resolveDefaultEventType(event));
}

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
    Executor executor = this.getTaskExecutor();
    Iterator var5 = this.getApplicationListeners(event, type).iterator();

    while(var5.hasNext()) {
        ApplicationListener<?> listener = (ApplicationListener)var5.next();
        if (executor != null) {
            executor.execute(() -> {
                this.invokeListener(listener, event);
            });
        } else {
            this.invokeListener(listener, event);
        }
    }

}

invokeListener

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    ErrorHandler errorHandler = this.getErrorHandler();
    if (errorHandler != null) {
        try {
            this.doInvokeListener(listener, event);
        } catch (Throwable var5) {
            errorHandler.handleError(var5);
        }
    } else {
        this.doInvokeListener(listener, event);
    }

}

doInvokeListener

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
    try {
        listener.onApplicationEvent(event);
    } catch (ClassCastException var6) {
        String msg = var6.getMessage();
        if (msg != null && !this.matchesClassCastMessage(msg, event.getClass()) && (!(event instanceof PayloadApplicationEvent) || !this.matchesClassCastMessage(msg, ((PayloadApplicationEvent)event).getPayload().getClass()))) {
            throw var6;
        }

        Log loggerToUse = this.lazyLogger;
        if (loggerToUse == null) {
            loggerToUse = LogFactory.getLog(this.getClass());
            this.lazyLogger = loggerToUse;
        }

        if (loggerToUse.isTraceEnabled()) {
            loggerToUse.trace("Non-matching event type for listener: " + listener, var6);
        }
    }

}

这个时候就可以进入到我们的ConfigFileApplicationListener中了,看看是如何加载配置文件的。

public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
    }

    if (event instanceof ApplicationPreparedEvent) {
        this.onApplicationPreparedEvent(event);
    }

}

onApplicationEnvironmentPreparedEvent方法里面读取配置文件:

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    List<EnvironmentPostProcessor> postProcessors = this.loadPostProcessors();
    postProcessors.add(this);
    AnnotationAwareOrderComparator.sort(postProcessors);
    Iterator var3 = postProcessors.iterator();

    while(var3.hasNext()) {
        EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var3.next();
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }

}

进入postProcessEnvironment方法:

public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    this.addPropertySources(environment, application.getResourceLoader());
}

看到addPropertySources方法是不是就感到很熟悉了,这不就是我们的最终目的吗,将文件添加到PropertySources中。

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    RandomValuePropertySource.addToEnvironment(environment);
    (new ConfigFileApplicationListener.Loader(environment, resourceLoader)).load();
}

load方法是读取配置文件的核心方法,需要走两层,最后从loadWithFilteredProperties进入,有的版本代码是没有分开的,这里按照自己的版本来。

void load() {
    FilteredPropertySource.apply(this.environment, "defaultProperties", ConfigFileApplicationListener.LOAD_FILTERED_PROPERTY, this::loadWithFilteredProperties);
}

private void loadWithFilteredProperties(PropertySource<?> defaultProperties) {
    this.profiles = new LinkedList();
    this.processedProfiles = new LinkedList();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap();
    this.initializeProfiles();

    while(!this.profiles.isEmpty()) {
        ConfigFileApplicationListener.Profile profile = (ConfigFileApplicationListener.Profile)this.profiles.poll();
        if (this.isDefaultProfile(profile)) {
            this.addProfileToEnvironment(profile.getName());
        }

        this.load(profile, this::getPositiveProfileFilter, this.addToLoaded(MutablePropertySources::addLast, false));
        this.processedProfiles.add(profile);
    }

    this.load((ConfigFileApplicationListener.Profile)null, this::getNegativeProfileFilter, this.addToLoaded(MutablePropertySources::addFirst, true));
    this.addLoadedPropertySources();
    this.applyActiveProfiles(defaultProperties);
}

文件监听代码分析

代码看的很乱,我们打断点来逐步分析,从this.load(profile, this::getPositiveProfileFilter, this.addToLoaded(MutablePropertySources::addLast, false));处开始。

确定搜索路径

点进去可以发现这里再搜索路径,到底搜索了那些路径呢?

private void load(ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) {
    this.getSearchLocations().forEach((location) -> {
        String nonOptionalLocation = ConfigDataLocation.of(location).getValue();
        boolean isDirectory = location.endsWith("/");
        Set<String> names = isDirectory ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMES;
        names.forEach((name) -> {
            this.load(nonOptionalLocation, name, profile, filterFactory, consumer);
        });
    });
}

private Set<String> getSearchLocations() {
    Set<String> locations = this.getSearchLocations("spring.config.additional-location");
    if (this.environment.containsProperty("spring.config.location")) {
        locations.addAll(this.getSearchLocations("spring.config.location"));
    } else {
        locations.addAll(this.asResolvedSet(ConfigFileApplicationListener.this.searchLocations, "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/"));
    }

    return locations;
}

是我们上面说的五个路径吧?以后别人问你从那几个路径可以读取springboot 的配置文件,大家应该都知道了吧,还能告诉他是怎么知道这五个路径的,因为内部已经将这五个路径写死了。
在这里插入图片描述

路径搜索

既然已经确定了从那几个路径搜索文件,那么接下来就是怎么搜索了。

private void load(ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) {
    this.getSearchLocations().forEach((location) -> {
        boolean isDirectory = location.endsWith("/");
        Set<String> names = isDirectory ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMES;
        names.forEach((name) -> {
            this.load(location, name, profile, filterFactory, consumer);
        });
    });
}

首先是判断是否为目录,然后去搜索文件,上面我们说过了,springboot 只加载名字为 application 的文件,是因为在代码里面也写死了。

private Set<String> getSearchNames() {
    if (this.environment.containsProperty("spring.config.name")) {
        String property = this.environment.getProperty("spring.config.name");
        Set<String> names = this.asResolvedSet(property, (String)null);
        names.forEach(this::assertValidConfigName);
        return names;
    } else {
        return this.asResolvedSet(ConfigFileApplicationListener.this.names, "application");
    }
}

如果是目录,也知道我们要查询什么文件了,但是还要判断它的后缀名,我们只接受 ymlpropertise后缀的文件。

private void load(String location, String name, ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) {
    if (!StringUtils.hasText(name)) {
        Iterator var13 = this.propertySourceLoaders.iterator();

        PropertySourceLoader loader;
        do {
            if (!var13.hasNext()) {
                throw new IllegalStateException("File extension of config file location '" + location + "' is not known to any PropertySourceLoader. If the location is meant to reference a directory, it must end in '/'");
            }

            loader = (PropertySourceLoader)var13.next();
        } while(!this.canLoadFileExtension(loader, location));

        this.load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
    } else {
        Set<String> processed = new HashSet();
        Iterator var7 = this.propertySourceLoaders.iterator();

        while(var7.hasNext()) {
            PropertySourceLoader loaderx = (PropertySourceLoader)var7.next();
            String[] var9 = loaderx.getFileExtensions();
            int var10 = var9.length;

            for(int var11 = 0; var11 < var10; ++var11) {
                String fileExtension = var9[var11];
                if (processed.add(fileExtension)) {
                    this.loadForFileExtension(loaderx, location + name, "." + fileExtension, profile, filterFactory, consumer);
                }
            }
        }

    }
}

这里我们关注getFileExtensions即可,因为只有这两个实例,所以我们只处理这两种后缀的。

在这里插入图片描述

需要注意的是:当某个配置既有ymlpropertise,那我们以propertise为准。

添加进PropertySources

上面文件搜索路径找到文件后我们就是要将文件添加到PropertySources中了,这个时候回到核心方法处:

查看addLoadedPropertySources方法。

这代码是不是也很简单,获取到环境变量后,然后将我们刚刚加载的文件先封装,然后在添加到最后。

这便是配置文件加载的全部操作。

private void addLoadedPropertySources() {
    MutablePropertySources destination = this.environment.getPropertySources();
    List<MutablePropertySources> loaded = new ArrayList(this.loaded.values());
    Collections.reverse(loaded);
    String lastAdded = null;
    Set<String> added = new HashSet();
    Iterator var5 = loaded.iterator();

    while(var5.hasNext()) {
        MutablePropertySources sources = (MutablePropertySources)var5.next();
        Iterator var7 = sources.iterator();

        while(var7.hasNext()) {
            PropertySource<?> source = (PropertySource)var7.next();
            if (added.add(source.getName())) {
                this.addLoadedPropertySource(destination, lastAdded, source);
                lastAdded = source.getName();
            }
        }
    }

}
private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded, PropertySource<?> source) {
    if (lastAdded == null) {
        if (destination.contains("defaultProperties")) {
            destination.addBefore("defaultProperties", source);
        } else {
            destination.addLast(source);
        }
    } else {
        destination.addAfter(lastAdded, source);
    }

}

自定义监听器

上面我们说了 bootstrap 文件就是通过监听器的形式加载进来的,那我们也手写一个监听器来记载文件。

我们还按照如下步骤来做不就可以了嘛。

  • 找文件:去磁盘中查找对应的配置文件(这几个目录下:classpath、classpath/config、/、/config/、/*/config)
  • 读文件内容:IO流
  • 解析并封装成对象
  • propertySources.addLast添加到最后

首先创建一个监听器的实现类:

需要注意的是泛型是ApplicationEnvironmentPreparedEvent类,不要问什么,你忘了我们看代码的时候,在doInvokeListener中,文件监听器的触发事件类型是ApplicationEnvironmentPreparedEvent吗?所以想要查询到我们的自定义文件,类型要设置正确。

代码完全是按照我们上面的步骤来的,所以没什么好说的。

public class MyPropertySourceListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {

        MutablePropertySources destination = event.getEnvironment().getPropertySources();
        Properties properties = new Properties();
        try {
            InputStream in = MyPropertySourceListener.class.getClassLoader().getResourceAsStream("test.properties");
            properties.load(in);
            ConcurrentHashMap<Object,Object> ch = new ConcurrentHashMap<>();
            for (Map.Entry entry : properties.entrySet()) {
                ch.put(entry.getKey(),entry.getValue());
            }
            OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource("test.properties",ch);
            destination.addLast(source);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

监听器写完之后,我们需要将实现类配置到SPI里面:(我这里面有两个自定义的初始化器和监听器,所以总共是三个了。)

org.springframework.context.ApplicationContextInitializer=\
 com.example.autodemo.listener.StarterApplicationContextInitializer

org.springframework.context.ApplicationListener=\
  com.example.autodemo.listener.StarterApplicationListener,\
  com.example.autodemo.listener.MyPropertySourceListener

这个时候我们重新运行项目,就可以看到我们的自定义监听器生效了,将文件加载进来了。

通过这种方式加载进来的配置信息我们就可以直接通过@Value注解读取了,不需要在使用@ConfigurationProperties等注解了。

在这里插入图片描述

refreshContext

该类启动spring的代码加载了bean,同时启动了内置的web容器,我们简单看下源码流程:

一直往下看 refresh 即可,最后跳入AbstractApplicationContext类的refresh方法,可以发现这里就是spring 容器的启动代码,还是那熟悉的12个流程:

该方法加载或者刷新一个持久化的配置,,可能是XML文件、属性文件或关系数据库模式。但是由于这是一种启动方式,如果失败,那么应该销毁所有以及创建好的实例。换句话说:在调用该方法之后,要么全部实例化,要么完全不实例化。

public void refresh() throws BeansException, IllegalStateException {
    // 加载或刷新配置前的同步处理
    synchronized(this.startupShutdownMonitor) {
        StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
        // 为刷新而准备此上下文
        this.prepareRefresh();
        
        // 告诉子类去刷新内部 bean 工厂
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        // 准备好bean 工厂,以便再此上下文中使用
        this.prepareBeanFactory(beanFactory);

        try {
            // 允许在上下文子类中对bean工厂进行后置处理
            this.postProcessBeanFactory(beanFactory);
            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
            // 调用在上下文中注册为bean的工厂处理器
            this.invokeBeanFactoryPostProcessors(beanFactory);
            // 注册拦截bean创建的bean处理器
            this.registerBeanPostProcessors(beanFactory);
            beanPostProcess.end();
            // 初始化消息资源
            this.initMessageSource();
            // 初始化事件多路广播器
            this.initApplicationEventMulticaster();
            // 初始话上下文子类中的其他特殊bean
            this.onRefresh();
            // 注册监听器(检查监听器的bean 并注册他们)
            this.registerListeners();
            // 实例化所有剩余的单例 不包括懒加载的
            this.finishBeanFactoryInitialization(beanFactory);
            // 发布相应的事件
            this.finishRefresh();
        } catch (BeansException var10) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
            }
            // 发生异常了销毁所有已经创建好的bean
            this.destroyBeans();
            // 重置 active 标识
            this.cancelRefresh(var10);
            throw var10;
        } finally {
            // 重置内核中的公用缓存
            this.resetCommonCaches();
            contextRefresh.end();
        }

    }
}

总结

以上便是spring boot 启动流程以及外部配置的所有内容。

从启动流程来说:

  • 首先判断项目类型
  • 初始化监听器和初始化器(可以理解为外部拓展)
  • 创建环境变量和上下文,还会创建一个配置文件的统一容器
  • 打印 banner
  • 执行 spring 核心流程
  • 启动 tomcat

从配置文件来说:

随着springboot 启动,会创建一个环境变量 environment ,在这个环境变量里面创建一个配置文件统一管理的容器,叫做PropertySources,创建好容器后,springboot 会基于监听器的形式从5个固定路径下查找以application命名且拓展名为properties和yml的文件,并通过io流读入内存,最后通过不同的解析器进行解析,解析完成后添加到最后。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/61091.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python实现Stacking回归模型(随机森林回归、极端随机树回归、AdaBoost回归、GBDT回归、决策树回归)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 Stacking通常考虑的是异质弱学习器&#xff08;不同的学习算法被组合在一起&#xff09;&#xff0c;st…

(附源码)SSM宠物领养系统 毕业设计 031654

SSM宠物领养系统 摘 要 如今&#xff0c;随着人们生活水平不断提高&#xff0c;人们的生活在物质满足的基础上&#xff0c;更多的人将生活的重点放在追求精神享受的过程中。于此同时&#xff0c;Internet铺天盖地的普及&#xff0c;使得这样的人纷纷通过Internet的方式去寻找精…

(十一) 共享模型之无锁【CAS 与 volatile】

一、问题引出&#xff08;P158&#xff09; 1. 取款案例 interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程&#xff0c;每个线程做 -10 元 的操作* 如果初始余额为 10000 那么正确的结果应当是 0*/st…

Seata概述基础

分布式事务原因&#xff1a; 单体架构的spring事务不能跨机器&#xff0c;不能跨数据源 分布式事务的概念&#xff1a; 一个业务流程&#xff0c;在分布式系统&#xff08;微服务&#xff09;中&#xff0c;每个业务模块都是一个分支&#xff0c;保证每个业务分支一起成功&am…

ROS1 LTS版本安装教程

Abstract: "ROS is primarily targeted at the Ubuntu , though other Linux systems as well as Mac OS X, Android, and Windows are supported to varying degrees. " 一、系统要求 ROS版本系统版本长期支持ROS Kinetic KameWily: amd64、i386 Xenial: amd64、i…

JSP住宅小区物业管理系统(源代码+开题报告+论文+答辩PPT)科大云炬

小区物业管理毕业设计 &#xff08;论文&#xff09; 目 录 摘要--------------------------------------------------------------------------------------------1 ABSTRACT-----------------------------------------------------------------------------------2 第1章…

制作composer包提供sdk扩展

目录 1、初始化包 2、将代码推送到github远程仓库 3、为写好扩展包打上tag标签标记当前代码版本 4、将包发布到包管理平台 初始化包&#xff0c;生成 Creates a basic composer.json file in current directory composer init composer init 按照引导就可以生成了 , 详细的…

Python学习笔记 - 异常处理

前言 为了增强程序的健壮性&#xff0c;计算机程序的编写也需要考虑如何处理一些异常的情况&#xff0c;Python 语言提供了异常处理功能&#xff0c;本博文主要介绍 Python 异常处理机制。 一 异常处理举例 为了学习 Python 异常处理机制&#xff0c;首先看下面进行除法运算的…

MySQL的主从复制与读写分离详解

MySQL的主从复制与读写分离详解读写分离概述什么是读写分离为什么要读写分离什么时候要读写分离MySQL主从复制与读写分离主从复制的概念MySQL支持的复制类型主从复制的工作过程主从复制示例保证MySQL主从服务器时间同步主节点服务器配置从节点服务器设置验证MySQL读写分离MySQL…

Vue3 学习笔记 —— Hooks、全局函数和变量、Vue3 插件

目录 1. Hooks 1.1 Vue2 中的 mixins 1.1.1 mixins 是什么&#xff1f; 1.1.2 mixins 缺点&#xff1f; 1.2 Vue3 中的 Hooks 1.2.1 Vue3 Hooks 是什么&#xff1f; 1.2.2 Vue3 内置 hooks 举例 1.2.3 自定义 Hooks 2. 全局函数和全局变量 2.1 app.config.globalProp…

【深入设计模式】适配器模式—一切皆有可能

文章目录1. 适配器模式1.1 适配器模式简介1.2 适配器模式结构1.3 适配器模式示例2. 适配器模式在源码中的应用3. 总结适配器这个词来源于硬件领域&#xff0c;是一个独立的硬件设备接口&#xff0c;允许硬件或电子接口与其它硬件或电子接口相连&#xff0c;比如常见的电源适配器…

(附源码)SSM芜湖公共书房服务平台 毕业设计 250859

SSM芜湖公共书房服务平台 摘 要 一座有底蕴的城市&#xff0c;应该是一个阅读的城市&#xff1b;一个有魅力的城市&#xff0c;应该是一个散发着书香的城市&#xff0c;而全民阅读量逐年增加&#xff0c;是社会进步、文明程度提高的重要标志。各大城市启动“全民阅读”工作以来…

Redis缓存过期和和内存淘汰策略

目录 1、MaxMemory 2、Expire数据结构 3、删除策略 3.1、惰性删除 3.2、主动删除 3.3、缓存淘汰策略 ​​​​​​​3.4、缓存淘汰策略的选择 1、MaxMemory Redis作为DB使用时&#xff0c;为了保证数据的完整性&#xff0c;不允许淘汰任何键值对。Redis作为缓存使用时&…

汇编语言与微机原理 期末半开卷复习整理(上)

8086CPU寄存器 8086&#xff1a;16位&#xff0c;4.77MHz~10MHz,16根数据线&#xff0c;20根地址线 AX/AL:乘除法指令中用作累加器&#xff0c;IO指令中用作数据寄存器&#xff0c;可显式或隐含调用 AH&#xff1a;在LAHF用作目的寄存器&#xff0c;隐含调用。 AL&#xff1a;…

大数据必学Java基础(一百一十一):过滤器注解应用和开发案例

文章目录 过滤器注解应用和开发案例 一、过滤器注解应用 二、开发案例

Python学习日记-第三十八天-生成器(第二节)

系列文章目录 使用greenlet&#xff0c;gevent完成多任务一、使用greenlet&#xff0c;gevent完成多任务 这里要先在pycharm里面提前安装好greenlet和gevent的包 操作&#xff1a; 代码&#xff1a; from greenlet import greenlet import timedef test1():while True:prin…

Android入门第39天-系统设置Configuration类

简介 本节给大家介绍的Configuration类是用来描述手机设备的配置信息的&#xff0c;比如屏幕方向&#xff0c; 触摸屏的触摸方式等。 Configuration给我们提供的方法列表 densityDpi&#xff1a;屏幕密度fontScale&#xff1a;当前用户设置的字体的缩放因子hardKeyboardHidd…

[深度学习] python基础支持汇总

这个系列放一些看神经网络源码过程中的python语法现象 文章目录前言一、list操作://extends/append的区别1.引入2.细致场景再现前言 例如&#xff1a;这个系列放一些看神经网络源码过程中的python语法现象, 直接解析语法太干瘪无聊.希望用这个方式来巩固所学知识 一、list操作…

电商评论文本情感分类(中文文本分类+中文词云图)(第一部分-textcnn)

电商评论文本情感分类(中文文本分类中文词云图) 第一部分 第二部分Bert部分 本项目包含&#xff1a; 1.中文文本处理 2.中文词云图绘制 3.中文词嵌入 4.基于textcnn的中文文本分类&#xff08;Test_Acc89.2000&#xff09; 5.基于bert的中文文本分类&#xff08;Test_Acc0.…

ServiceComb场景及其原理

文章目录Java-ChassisEnableServiceComb初始化SCBSPIServiceUtils自定义SPI加载器职责链管理器FilterChainsManager/ConsumerHandlerManagerRpcSchema注册服务如何保活&#xff1f;RpcReferencePropertySourcesPlaceholderConfigurerThreadPoolExecutorEx/LinkedBlockingQueueE…