springBoot配置文件加載原理探究

儘管用springBoot做開發已經有很長一段時間了,在開發時一般都是直接將application.properties或application.yml,放在開發環境的resources下的,運行起來感覺也沒什麼問題。

但是由於項目最終都是要通過打包,最終打包爲一個jar包運行的。但如果一個項目由於環境不同需要對配置文件修改時,直接將在IDE中修改配置文件再重新打成一個JAR包很耗費時間。

最終通過搜索,得到一個理想的配置文件設置方式。可以在打好的將要運行的springBoot的jar包同級目錄下放置上配置文件,或在其同級目錄下新建一個config目錄,將配置文件放在config目錄中就可以了。試了一下,確實可以,感覺挺高級的,非常棒!

對於一向充滿好奇心的我來說,對於springBoot它能這樣做的原理充滿了興趣,決定通過ide的debug跟蹤下源碼。

軟件環境:
springBoot版本:1.5.4.RELEASE

springBoot初始化listenner

從SpringApplication.run()方法開始打debug開始跟蹤
run方法會調用到這裏:

    /**
     * Static helper that can be used to run a {@link SpringApplication} from the
     * specified sources using default settings and user supplied arguments.
     * @param sources the sources to load
     * @param args the application arguments (usually passed from a Java main method)
     * @return the running {@link ApplicationContext}
     */
    public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return new SpringApplication(sources).run(args);
    }

new SpringApplication(sources)方法中有調用initialize(sources)這個初始化方法
其initialize方法代碼爲:

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void initialize(Object[] sources) {
        if (sources != null && sources.length > 0) {
            this.sources.addAll(Arrays.asList(sources));
        }
        this.webEnvironment = deduceWebEnvironment();
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }

其中重點關注此方法:setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))
在這個方法中,會初始化一些監聽器,主要看下這個方法
它會調用此方法:

    private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
            Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<String>(
                SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
                classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

在此方法中會通過SpringFactoriesLoader.loadFactoryNames方法獲得出一串names的集合,然後再通過createSpringFactoriesInstances方法將names實例化出來

其SpringFactoriesLoader.loadFactoryNames方法爲:

    /**
     * Load the fully qualified class names of factory implementations of the
     * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
     * class loader.
     * @param factoryClass the interface or abstract class representing the factory
     * @param classLoader the ClassLoader to use for loading resources; can be
     * {@code null} to use the default
     * @see #loadFactories
     * @throws IllegalArgumentException if an error occurs while loading factory names
     */
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        try {
            Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            List<String> result = new ArrayList<String>();
            while (urls.hasMoreElements()) {
                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 ex) {
            throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                    "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
    }

其中此條語句:

Enumeration<URL> urls=(classLoader!=null?classLoader.getResources(FACTORIES_RESOURCE_LOCATION):ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));

會通過classLoader在jar內獲取出FACTORIES_RESOURCE_LOCATION的資源。
其FACTORIES_RESOURCE_LOCATION的值可以通過源碼找到這句

    /**
     * The location to look for factories.
     * <p>Can be present in multiple JAR files.
     */
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

那麼也就是springBoot在初始化的時候會加載所有依賴包的META-INF/spring.factories文件
爲此,爲了驗證都能獲取出哪些具體的spring.factories配置文件,我在這個springBoot項目的測試類中寫了方法

    public static void main(String[] args) {
        IndexServiceApplicationTests indexServiceApplicationTests = new IndexServiceApplicationTests();
        try {
            Enumeration<URL> urls = indexServiceApplicationTests.getClass().getClassLoader().getResources("META-INF/spring.factories");
            System.out.println("urls:" + urls);
            while(urls.hasMoreElements()){
                URL url = urls.nextElement();
                System.out.println("urlItem:"+url);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

其輸出結果爲:

urls:sun.misc.CompoundEnumeration@f2a0b8e
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/cloud/spring-cloud-context/1.2.2.RELEASE/spring-cloud-context-1.2.2.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/cloud/spring-cloud-commons/1.2.2.RELEASE/spring-cloud-commons-1.2.2.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/cloud/spring-cloud-netflix-eureka-server/1.3.1.RELEASE/spring-cloud-netflix-eureka-server-1.3.1.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/cloud/spring-cloud-netflix-core/1.3.1.RELEASE/spring-cloud-netflix-core-1.3.1.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/cloud/spring-cloud-netflix-eureka-client/1.3.1.RELEASE/spring-cloud-netflix-eureka-client-1.3.1.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/boot/spring-boot-test/1.5.4.RELEASE/spring-boot-test-1.5.4.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/boot/spring-boot/1.5.4.RELEASE/spring-boot-1.5.4.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/boot/spring-boot-test-autoconfigure/1.5.4.RELEASE/spring-boot-test-autoconfigure-1.5.4.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/boot/spring-boot-autoconfigure/1.5.4.RELEASE/spring-boot-autoconfigure-1.5.4.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/spring-test/4.3.9.RELEASE/spring-test-4.3.9.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/spring-beans/4.3.9.RELEASE/spring-beans-4.3.9.RELEASE.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.jar!/META-INF/spring.factories
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/boot/spring-boot-actuator/1.5.4.RELEASE/spring-boot-actuator-1.5.4.RELEASE.jar!/META-INF/spring.factories

其中前面的/D:/program%20files/maven_repo字段爲我電腦本地maven倉庫的路徑
關注下這條輸出記錄:
urlItem:jar:file:/D:/program%20files/maven_repo/org/springframework/boot/spring-boot/1.5.4.RELEASE/spring-boot-1.5.4.RELEASE.jar!/META-INF/spring.factories
這個路徑爲springboot這個jar包下的spring.facotires文件
隨後,代碼又執行了如下語句:

Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));

先把此配置文件加載爲properties,再獲取出變量factoryClassName屬性的值。通過debug或通過入參可以獲取到factoryClassName爲ApplicationListener.class這個類的名稱,即org.springframework.context.ApplicationListener
獲得了以上信息後,我們便可以打開對應的JAR包,找到對應的配置文件下的org.springframework.context.ApplicationListener鍵,查看都有哪些值(spring-boot-1.5.4.RELEASE.jar!/META-INF/spring.factories中的org.springframework.context.ApplicationListener的值)
通過打開JAR包,在此配置文件中發現瞭如下內容

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
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

上面的代碼result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)))就將這些listener添加進了result集合中,最終返回給了它上一級的調用方法names

    private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
            Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<String>(
                SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
                classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

    @SuppressWarnings("unchecked")
    private <T> List<T> createSpringFactoriesInstances(Class<T> type,
            Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
            Set<String> names) {
        List<T> instances = new ArrayList<T>(names.size());
        for (String name : names) {
            try {
                Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                Assert.isAssignable(type, instanceClass);
                Constructor<?> constructor = instanceClass
                        .getDeclaredConstructor(parameterTypes);
                T instance = (T) BeanUtils.instantiateClass(constructor, args);
                instances.add(instance);
            }
            catch (Throwable ex) {
                throw new IllegalArgumentException(
                        "Cannot instantiate " + type + " : " + name, ex);
            }
        }
        return instances;
    }

從代碼可以看出,上面的代碼將獲取出來的listener全通過反射進行了實例化,最終回到了初始化方法,通過setListeners設置到了SpringApplication的類中

    /**
     * Sets the {@link ApplicationListener}s that will be applied to the SpringApplication
     * and registered with the {@link ApplicationContext}.
     * @param listeners the listeners to set
     */
    public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
        this.listeners = new ArrayList<ApplicationListener<?>>();
        this.listeners.addAll(listeners);
    }

ConfigFileApplicationListener執行過程

通過上面,可以得知在springApplication初始化的時候會加載ConfigFileApplicationListener這個類,那麼它是在什麼時候調用了這個類呢?仍然是通過源碼,探究Application的run方法

    /**
     * Run the Spring application, creating and refreshing a new
     * {@link ApplicationContext}.
     * @param args the application arguments (usually passed from a Java main method)
     * @return a running {@link ApplicationContext}
     */
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            analyzers = new FailureAnalyzers(context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            listeners.finished(context, null);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, analyzers, ex);
            throw new IllegalStateException(ex);
        }
    }

其中有如下兩行代碼:

        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();

starting方法的代碼爲:

    public void starting() {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.starting();
        }
    }

通過代碼可知stating方法就是通過遍歷listeners,來依次觸發listener的starting方法。最終會執行到這裏:

    public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
        ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
        Iterator var4 = this.getApplicationListeners(event, type).iterator();

        while(var4.hasNext()) {
            final ApplicationListener<?> listener = (ApplicationListener)var4.next();
            Executor executor = this.getTaskExecutor();
            if (executor != null) {
                executor.execute(new Runnable() {
                    public void run() {
                        SimpleApplicationEventMulticaster.this.invokeListener(listener, event);
                    }
                });
            } else {
                this.invokeListener(listener, event);
            }
        }

    }

上面的this.getApplicationListeners(event, type)會獲得由上文初始化的那10多個集合,然後再通過迭代器進行遍歷listener。每遍歷到一個listener,就從線程器中開啓一個線程,去執行這個listener

    protected void invokeListener(ApplicationListener listener, ApplicationEvent event) {
        ErrorHandler errorHandler = this.getErrorHandler();
        if (errorHandler != null) {
            try {
                listener.onApplicationEvent(event);
            } catch (Throwable var7) {
                errorHandler.handleError(var7);
            }
        } else {
            try {
                listener.onApplicationEvent(event);
            } catch (ClassCastException var8) {
                String msg = var8.getMessage();
                if (msg != null && !msg.startsWith(event.getClass().getName())) {
                    throw var8;
                }

                Log logger = LogFactory.getLog(this.getClass());
                if (logger.isDebugEnabled()) {
                    logger.debug("Non-matching event type for listener: " + listener, var8);
                }
            }
        }

    }
}

最終都會觸發listener的onApplicationEvent方法。
這裏只跟蹤下ConfigFileApplicationListener這個監聽器。
當觸發到ConfigFileApplicationListener的onApplicationEvent時,會執行如下的代碼

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

由debu跟蹤,會發現初始化時運行的是onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event)這個方法
隨後進入:

    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }

postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication()方法爲:

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
        configureIgnoreBeanInfo(environment);
        bindToSpringApplication(environment, application);
    }

在addPropertySources方法中,發現瞭如下關鍵代碼:

    /**
     * Add config file property sources to the specified environment.
     * @param environment the environment to add source to
     * @param resourceLoader the resource loader
     * @see #addPostProcessors(ConfigurableApplicationContext)
     */
    protected void addPropertySources(ConfigurableEnvironment environment,
            ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        new Loader(environment, resourceLoader).load();
    }

後面的new Loader(environment,resourceLoader).load()方法代碼爲:

        public void load() {
            this.propertiesLoader = new PropertySourcesLoader();
            this.activatedProfiles = false;
            this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
            this.processedProfiles = new LinkedList<Profile>();

            // Pre-existing active profiles set via Environment.setActiveProfiles()
            // are additional profiles and config files are allowed to add more if
            // they want to, so don't call addActiveProfiles() here.
            Set<Profile> initialActiveProfiles = initializeActiveProfiles();
            this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
            if (this.profiles.isEmpty()) {
                for (String defaultProfileName : this.environment.getDefaultProfiles()) {
                    Profile defaultProfile = new Profile(defaultProfileName, true);
                    if (!this.profiles.contains(defaultProfile)) {
                        this.profiles.add(defaultProfile);
                    }
                }
            }

            // The default profile for these purposes is represented as null. We add it
            // last so that it is first out of the queue (active profiles will then
            // override any settings in the defaults when the list is reversed later).
            this.profiles.add(null);

            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                for (String location : getSearchLocations()) {
                    if (!location.endsWith("/")) {
                        // location is a filename already, so don't search for more
                        // filenames
                        load(location, null, profile);
                    }
                    else {
                        for (String name : getSearchNames()) {
                            load(location, name, profile);
                        }
                    }
                }
                this.processedProfiles.add(profile);
            }

            addConfigurationProperties(this.propertiesLoader.getPropertySources());
        }

上面首先初始化一個profiles隊列,其隊列爲一個lifo隊列(lastInFirstOut後進先出),代碼爲:

this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());

隨後判斷下當前環境中是否有設profile,沒有的話,就使用默認的profile,在profiles中加入一個名爲default的profile.隨後又在profiles中加入了一個null,對於爲什麼要加入一個null,代碼裏也有相應的註釋說明。

// The default profile for these purposes is represented as null. We add it
// last so that it is first out of the queue (active profiles will then
// override any settings in the defaults when the list is reversed later).
this.profiles.add(null);

大致意思是說放一個null值在profiles隊列的末尾,由於隊列是lifo類型的,所以null值就會最先出隊,先將默認配置給初始化。當其他激活的profile出隊的時候,就會重載默認的配置。
而後關注這個方法中的這段代碼:

            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                for (String location : getSearchLocations()) {
                    if (!location.endsWith("/")) {
                        // location is a filename already, so don't search for more
                        // filenames
                        load(location, null, profile);
                    }
                    else {
                        for (String name : getSearchNames()) {
                            load(location, name, profile);
                        }
                    }
                }
                this.processedProfiles.add(profile);
            }

在這裏就是配置文件體現加載順序的主要代碼
String location : getSearchLocations()
在getSearchLocations代碼中,在沒有設置其他配置文件的情況下,就會在配置文件的路徑中加入如下地址

            locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
                            DEFAULT_SEARCH_LOCATIONS));

及DEFAULT_SEARCH_LOCATIONS的值,而其值在此類的頭部也可以找到它的定義:

    // Note the order is from least to most specific (last one wins)
    private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

這裏寫的先後順序是classpath:/,classpath:/config/,file:./,file:./config/,但上面有註釋說明,這個順序是通過由後到前的順序來進行選擇的。
通過asResolvedSet這個方法,也可以得證:

        private Set<String> asResolvedSet(String value, String fallback) {
            List<String> list = Arrays.asList(StringUtils.trimArrayElements(
                    StringUtils.commaDelimitedListToStringArray(value != null
                            ? this.environment.resolvePlaceholders(value) : fallback)));
            Collections.reverse(list);
            return new LinkedHashSet<String>(list);
        }

Collections.reverse(list);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章