主頁 > 後端開發 > 精盡Spring Boot原始碼分析 - SpringApplication 啟動類的啟動程序

精盡Spring Boot原始碼分析 - SpringApplication 啟動類的啟動程序

2021-07-02 06:15:11 後端開發

該系列文章是筆者在學習 Spring Boot 程序中總結下來的,里面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼注釋 Spring Boot 原始碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 原始碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點擊一下“推薦”,也可以關注博主,感激不盡~

該系列其他文章請查看:《精盡 Spring Boot 原始碼分析 - 文章導讀》

概述

我們 Spring Boot 專案的啟動類通常有下面三種方式

// 方式一
@SpringBootApplication
public class Application {

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

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).run(args);
    }
}
// 方式三
@SpringBootApplication
public class Application extends SpringBootServletInitializer {

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

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }
}

方式一方式二本質上都是通過呼叫 SpringApplication#run(..) 方法來啟動應用,不同的是方式二通過構建器模式,先構建一個 SpringApplication 實體物件,然后呼叫其 run(..) 方法啟動應用,這種方式可以對 SpringApplication 進行配置,更加的靈活,

我們再來看到方式三,它和方式一差不多,不同的是它繼承了 SpringBootServletInitializer 這個類,作用就是當你的 Spring Boot 專案打成 war 包時能夠放入外部的 Tomcat 容器中運行,如果是 war 包,那上面的 main(...) 方法自然是不需要的,當然,configure(..) 方法也可以不要,

在上篇 《精盡 Spring Boot 原始碼分析 - Jar 包的啟動實作》 文章中講到,通過 java -jar 啟動 Spring Boot 應用時,最侄訓呼叫我們啟動類的 main(..) 方法,那么本文主要就是對 SpringApplication 這個類進行分析,至于上面 @SpringBootApplication 注解和方式三的相關內容在后續的文章會講到,

SpringApplicationBuilder

org.springframework.boot.builder.SpringApplicationBuilder,SpringApplication 的構建器,如下:

public class SpringApplicationBuilder {

	private final SpringApplication application;

	private ConfigurableApplicationContext context;

	private final AtomicBoolean running = new AtomicBoolean(false);

	private final Set<Class<?>> sources = new LinkedHashSet<>();

	public SpringApplicationBuilder(Class<?>... sources) {
		this.application = createSpringApplication(sources);
	}
    
    protected SpringApplication createSpringApplication(Class<?>... sources) {
		return new SpringApplication(sources);
	}
    
    public ConfigurableApplicationContext run(String... args) {
		if (this.running.get()) {
			// If already created we just return the existing context
			return this.context;
		}
		configureAsChildIfNecessary(args);
		if (this.running.compareAndSet(false, true)) {
			synchronized (this.running) {
				// If not already running copy the sources over and then run.
				this.context = build().run(args);
			}
		}
		return this.context;
	}
    
    public SpringApplication build() {
		return build(new String[0]);
	}
    
    public SpringApplication build(String... args) {
		configureAsChildIfNecessary(args);
		this.application.addPrimarySources(this.sources);
		return this.application;
	}
}

上面僅列出了 SpringApplicationBuilder 的部分代碼,它支持對 SpringApplication 進行配置,底層還是通過 SpringApplication 這個類來啟動應用的,不過多的講述,感興趣的可以去看看,

SpringApplication

org.springframework.boot.SpringApplication,Spring 應用啟動器,正如其代碼上所添加的注釋,它來提供啟動 Spring 應用的功能,

Class that can be used to bootstrap and launch a Spring application from a Java main method.

相關屬性

public class SpringApplication {

	/**
	 * Spring 應用背景關系(非 Web 場景)
	 * The class name of application context that will be used by default for non-web environments.
	 */
	public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
			+ "annotation.AnnotationConfigApplicationContext";

	/**
	 * Spring 應用背景關系(Web 場景)
	 * The class name of application context that will be used by default for web environments.
	 */
	public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
			+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

	/**
	 * Spring 應用背景關系(Reactive 場景)
	 * The class name of application context that will be used by default for reactive web environments.
	 */
	public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
			+ "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

	/**
	 * 主 Bean(通常為我們的啟動類,優先注冊)
	 */
	private Set<Class<?>> primarySources;
	/**
	 * 來源 Bean(優先注冊)
	 */
	private Set<String> sources = new LinkedHashSet<>();
	/**
	 * 啟動類
	 */
	private Class<?> mainApplicationClass;
	/**
	 * Banner 列印模式
	 */
	private Banner.Mode bannerMode = Banner.Mode.CONSOLE;
	/**
	 * 是否列印應用啟動耗時日志
	 */
	private boolean logStartupInfo = true;
	/**
	 * 是否接收命令列中的引數
	 */
	private boolean addCommandLineProperties = true;
	/**
	 * 是否設定 ConversionService 型別轉換器
	 */
	private boolean addConversionService = true;
	/**
	 * Banner 物件(用于輸出橫幅)
	 */
	private Banner banner;
	/**
	 * 資源加載物件
	 */
	private ResourceLoader resourceLoader;
	/**
	 * Bean 名稱生成器
	 */
	private BeanNameGenerator beanNameGenerator;
	/**
	 * Spring 應用的環境物件
	 */
	private ConfigurableEnvironment environment;
	/**
	 * Spring 應用背景關系的 Class 物件
	 */
	private Class<? extends ConfigurableApplicationContext> applicationContextClass;
	/**
	 * Web 應用的型別(Servlet、Reactive)
	 */
	private WebApplicationType webApplicationType;
	/**
	 * 是否注冊鉤子函式,用于 JVM 關閉時關閉 Spring 應用背景關系
	 */
	private boolean registerShutdownHook = true;
	/**
	 * 保存 ApplicationContextInitializer 物件(主要是對 Spring 應用背景關系做一些初始化作業)
	 */
	private List<ApplicationContextInitializer<?>> initializers;
	/**
	 * 保存 ApplicationListener 監聽器(支持在整個 Spring Boot 的多個時間點進行擴展)
	 */
	private List<ApplicationListener<?>> listeners;
	/**
	 * 默認的配置項
	 */
	private Map<String, Object> defaultProperties;
	/**
	 * 額外的 profile
	 */
	private Set<String> additionalProfiles = new HashSet<>();
	/**
	 * 是否允許覆寫 BeanDefinition
	 */
	private boolean allowBeanDefinitionOverriding;
	/**
	 * 是否為自定義的 Environment 物件
	 */
	private boolean isCustomEnvironment = false;
	/**
	 * 是否支持延遲初始化(需要通過 {@link LazyInitializationExcludeFilter} 過濾)
	 */
	private boolean lazyInitialization = false;
}

上面基本上列出了 SpringApplication 的所有屬性,每個屬性都比較關鍵,大家先有一個印象,后面也可以回過頭來看

構造器

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // <1> 設定資源加載器
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // <2> 設定主要的 Class 類物件,通常是我們的啟動類
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // <3> 通過 `classpath` 判斷是否存在相應的 Class 類物件,來決定當前 Web 應用的型別(REACTIVE、SERVLET、NONE),默認為 **SERVLET**
    // 不同的型別后續創建的 Environment 型別不同
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    /**
     * <4> 初始化所有 `ApplicationContextInitializer` 型別的物件,并保存至 `initializers` 集合中
     * 通過類加載器從 `META-INF/spring.factories` 檔案中獲取 ApplicationContextInitializer 型別的類名稱,并進行實體化
     */
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    /**
     * <5> 初始化所有 `ApplicationListener` 型別的物件,并保存至 `listeners` 集合中
     * 通過類加載器從 `META-INF/spring.factories` 檔案中獲取 ApplicationListener 型別的類名稱,并進行實體化
     * 例如有一個 {@link ConfigFileApplicationListener}
     */
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // <6> 獲取當前被呼叫的 `main` 方法所屬的 Class 類物件,并設定(主要用于列印日志)
    this.mainApplicationClass = deduceMainApplicationClass();
}

在我們自己的啟動類中不管是通過哪種方式都是會先創建一個 SpringApplication 實體物件的,可以先看下它的 run(Class<?>, String...) 方法:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    // 同樣是先創建一個 SpringApplication 物件
    return new SpringApplication(primarySources).run(args);
}

實體化的程序中做了不少事情,如下:

  1. 設定資源加載器,默認為 null,可以通過 SpringApplicationBuilder 設定

  2. 設定 primarySources 為主要的 Class 類物件,通常是我們的啟動類

  3. 通過 classpath 判斷是否存在相應的 Class 類物件,來決定當前 Web 應用的型別(REACTIVE、SERVLET、NONE),默認為 SERVLET,不同的型別后續創建的 Environment 型別不同

    public enum WebApplicationType {
    
    	NONE, SERVLET, REACTIVE;
    
    	private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
    			"org.springframework.web.context.ConfigurableWebApplicationContext" };
    
    	private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
    
    	private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
    
    	private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
    
    	private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
    
    	private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
    
    	static WebApplicationType deduceFromClasspath() {
    		if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
    				&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
    			return WebApplicationType.REACTIVE;
    		}
    		for (String className : SERVLET_INDICATOR_CLASSES) {
    			if (!ClassUtils.isPresent(className, null)) {
    				return WebApplicationType.NONE;
    			}
    		}
    		return WebApplicationType.SERVLET;
    	}
    }
    

    很簡單,就是依次判斷當前 JVM 中是否存在相關的 Class 類物件,來決定使用哪種 Web 型別,默認是 SERVLET 型別

  4. 初始化所有 ApplicationContextInitializer 型別的物件,并保存至 initializers 集合中

  5. 初始化所有 ApplicationListener 型別的物件,并保存至 listeners 集合中,例如 ConfigFileApplicationListenerLoggingApplicationListener

  6. 獲取當前被呼叫的 main 方法所屬的 Class 類物件,并設定(主要用于列印日志)

    private Class<?> deduceMainApplicationClass() {
        try {
            // 獲取當前的呼叫堆疊
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            // 對呼叫堆疊進行遍歷,找到 `main` 方法所在的 Class 類物件并回傳
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        } catch (ClassNotFoundException ex) {
            // Swallow and continue
        }
        return null;
    }
    

上面的第 45 步都是通過類加載器從 META-INF/spring.factories 檔案中分別獲取 ApplicationContextInitializerApplicationListener 型別的類名稱,然后進行實體化,這個兩種型別的物件都是對 Spring 的一種拓展,像很多框架整合 Spring Boot 都可以通過自定義的 ApplicationContextInitializer 對 ApplicationContext 進行一些初始化,通過 ApplicationListener 在 Spring 應用啟動的不同階段來織入一些功能

getSpringFactoriesInstances 方法

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    // <1> 獲取類加載器
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    // <2> 通過類加載器從 `META-INF/spring.factories` 檔案中獲取型別為 `type` 的類名稱
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // <3> 為上一步獲取到的所有類名稱創建對應的實體物件
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // <4> 通過 `@Order` 注解進行排序
    AnnotationAwareOrderComparator.sort(instances);
    // <5> 回傳排序后的 `type` 型別的實體物件
    return instances;
}

程序比較簡單,如下:

  1. 獲取類加載器

  2. 通過類加載器從所有 META-INF/spring.factories 檔案中獲取型別為 type 的類名稱,這里的 SpringFactoriesLoader 是 Spring 中的一個類

  3. 為上一步獲取到的所有類名稱創建對應的實體物件

    private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
            ClassLoader classLoader, Object[] args, Set<String> names) {
        List<T> instances = new ArrayList<>(names.size());
        // 遍歷所有的類名稱,依次創建實體物件,一并回傳
        for (String name : names) {
            try {
                // 獲取對應的 Class 類物件
                Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                // 對 Class 類物件進行校驗,判斷型別是否匹配
                Assert.isAssignable(type, instanceClass);
                // 獲取指定入參型別(ConfigurableApplicationContext)的構造器
                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;
    }
    
  4. 通過 @Order 注解進行排序

  5. 回傳排序后的 type 型別的實體物件

SpringApplication#run 方法

上面已經講述了 SpringApplication 的實體化程序,那么接下來就是呼叫它的 run(String... args) 方法來啟動 Spring 應用,該程序如下:

public ConfigurableApplicationContext run(String... args) {
    // <1> 創建 StopWatch 物件并啟動,主要用于統計當前方法執行程序的耗時
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // <2> 設定 `java.awt.headless` 屬性,和 AWT 相關,暫時忽略
    configureHeadlessProperty();
    // <3> 初始化所有 `SpringApplicationRunListener` 型別的物件,并全部封裝到 `SpringApplicationRunListeners` 物件中
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // <4> 啟動所有的 `SpringApplicationRunListener` 監聽器
    // 例如 `EventPublishingRunListener` 會廣播 ApplicationEvent 應用正在啟動的事件,
    // 它里面封裝了所有的 `ApplicationListener` 物件,那么此時就可以通過它們做一些初始化作業,進行拓展
    listeners.starting();
    try {
        // <5> 創建一個應用引數物件,將 `main(String[] args)` 方法的 `args` 引數封裝起來,便于后續使用
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // <6> 準備好當前應用 Environment 環境,這里會加載出所有的配置資訊,包括 `application.yaml` 和外部的屬性配置
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

        configureIgnoreBeanInfo(environment);
        // <7> 列印 banner 內容
        Banner printedBanner = printBanner(environment);
        // <8> 對 `context` (Spring 背景關系)進行實體化
        // 例如 Servlet(默認)會創建一個 AnnotationConfigServletWebServerApplicationContext 實體物件
        context = createApplicationContext();
        // <9> 獲取例外報告器,通過類加載器從 `META-INF/spring.factories` 檔案中獲取 SpringBootExceptionReporter 型別的類名稱,并進行實體化
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        // <10> 對 Spring 應用背景關系做一些初始化作業,例如執行 ApplicationContextInitializer#initialize(..) 方法
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        /**
         * <11> 重繪 Spring 應用背景關系,在這里會完成所有 Spring Bean 的初始化,同時會初始化好 Servlet 容器,例如 Tomcat
         * 這一步涉及到 Spring IoC 的所有內容,參考[死磕Spring之IoC篇 - Spring 應用背景關系 ApplicationContext](https://www.cnblogs.com/lifullmoon/p/14453083.html)
         * 在 {@link ServletWebServerApplicationContext#onRefresh()} 方法中會創建一個 Servlet 容器(默認為 Tomcat),也就是當前 Spring Boot 應用所運行的 Web 環境
         */
        refreshContext(context);
        // <12> 完成重繪 Spring 應用背景關系的后置操作,空實作,擴展點
        afterRefresh(context, applicationArguments);
        // <13> 停止 StopWatch,統計整個 Spring Boot 應用的啟動耗時,并列印
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // <14> 對所有的 SpringApplicationRunListener 監聽器進行廣播,發布 ApplicationStartedEvent 應用已啟動事件
        listeners.started(context);
        // <15> 回呼 IoC 容器中所有 ApplicationRunner 和 CommandLineRunner 型別的啟動器
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        // 處理例外,同時會將例外發送至上面第 `9` 步獲取到的例外報告器
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // <16> 對所有的 SpringApplicationRunListener 監聽器進行廣播,發布 ApplicationReadyEvent 應用已就緒事件
        listeners.running(context);
    }
    catch (Throwable ex) {
        // 處理例外,同時會將例外發送至上面第 `9` 步獲取到的例外報告器
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

整個啟動程序做了很多事情,主要程序如下:

  1. 創建 StopWatch 物件并啟動,主要用于統計當前方法執行程序的耗時

  2. 設定 java.awt.headless 屬性,和 AWT 相關,暫時忽略

  3. 呼叫 getRunListeners(..) 方法,初始化所有 SpringApplicationRunListener 型別的物件,并全部封裝到 SpringApplicationRunListeners 物件中

  4. 啟動所有的 SpringApplicationRunListener 監聽器,例如 EventPublishingRunListener 會廣播 ApplicationEvent 應用正在啟動的事件,它里面封裝了所有的 ApplicationListener 物件,那么此時就可以通過它們做一些初始化作業,進行拓展

  5. 創建一個 ApplicationArguments 應用引數物件,將 main(String[] args) 方法的 args 引數封裝起來,便于后續使用

  6. 呼叫 prepareEnvironment(..) 方法,準備好當前應用 Environment 環境,這里會加載出所有的配置資訊,包括 application.yaml 和外部的屬性配置

  7. 呼叫 printBanner(..) 方法,列印 banner 內容

  8. 呼叫 createApplicationContext() 方法, 對 context(Spring 背景關系)進行實體化,例如 Servlet(默認)會創建一個 AnnotationConfigServletWebServerApplicationContext 實體物件

  9. 獲取例外報告器,通過類加載器從 META-INF/spring.factories 檔案中獲取 SpringBootExceptionReporter 型別的類名稱,并進行實體化

  10. 呼叫 prepareContext(..) 方法,對 Spring 應用背景關系做一些初始化作業,例如執行 ApplicationContextInitializer#initialize(..) 方法

  11. 呼叫 refreshContext(..) 方法,重繪 Spring 應用背景關系,在這里會完成所有 Spring Bean 的初始化,同時會初始化好 Servlet 容器,例如 Tomcat

    這一步涉及到 Spring IoC 的所有內容,參考 《死磕Spring之IoC篇 - Spring 應用背景關系 ApplicationContext》

    ServletWebServerApplicationContext#onRefresh() 方法中會創建一個 Servlet 容器(默認為 Tomcat),也就是當前 Spring Boot 應用所運行的 Web 環境

  12. 呼叫 afterRefresh(..) 方法,完成重繪 Spring 應用背景關系的后置操作,空實作,擴展點

  13. 停止 StopWatch,統計整個 Spring Boot 應用的啟動耗時,并列印

  14. 對所有的 SpringApplicationRunListener 監聽器進行廣播,發布 ApplicationStartedEvent 應用已啟動事件,通常只有一個 EventPublishingRunListener 物件

  15. 回呼 IoC 容器中所有 ApplicationRunner 和 CommandLineRunner 型別的啟動器,默認情況下沒有,先暫時忽略

  16. 對所有的 SpringApplicationRunListener 監聽器進行廣播,發布 ApplicationReadyEvent 應用已就緒事件,通常只有一個 EventPublishingRunListener 物件

啟動 Spring 應用的整個主流程清晰明了,先準備好當前應用的 Environment 環境,然后創建 Spring ApplicationContext 應用背景關系,

該方法的整個程序更多的細節在于上面每一步呼叫的方法,抽絲剝繭,對于非常復雜的地方會另起文章進行分析

3. getRunListeners 方法

getRunListeners(String[] args) 方法,初始化所有 SpringApplicationRunListener 型別的物件,并全部封裝到 SpringApplicationRunListeners 物件中,如下:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    // 指定實體化物件所使用的構造器的入參型別
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    // 通過類加載器從 `META-INF/spring.factories` 檔案中獲取 SpringApplicationRunListener 型別的類名稱,并進行實體化
    return new SpringApplicationRunListeners(logger,
            getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

這里同樣呼叫上面講過的 getSpringFactoriesInstances(..) 方法,通過類加載器從 META-INF/spring.factories 檔案中獲取 SpringApplicationRunListener 型別的類名稱,并進行實體化

最后會將它們全部封裝到 SpringApplicationRunListeners 物件中,就是把一個 List 封裝到一個物件中,不過默認情況只有一個 EventPublishingRunListener 物件,其內部又封裝了 SpringApplication 中的所有 ApplicationListener 應用監聽器們,例如 ConfigFileApplicationListenerLoggingApplicationListener

6. prepareEnvironment 方法

prepareEnvironment(SpringApplicationRunListeners, ApplicationArguments) 方法,準備好當前應用 Environment 環境,加載出所有的配置資訊,包括 application.yaml 和外部的屬性配置,如下:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    // <1> 根據 Web 應用的型別創建一個 StandardEnvironment 物件 `environment`
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // <2> 為 `environment` 配置默認屬性(如果有)并設定需要激活的 `profile` 們
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // <3> 將當前 `environment` 的 MutablePropertySources 封裝成 SpringConfigurationPropertySources 添加到 MutablePropertySources 首部
    ConfigurationPropertySources.attach(environment);
    /**
     * <4> 對所有的 SpringApplicationRunListener 廣播 ApplicationEvent 應用環境已準備好的事件,這一步比較復雜
     * 例如 Spring Cloud 的 BootstrapApplicationListener 監聽到該事件會創建一個 ApplicationContext 作為當前 Spring 應用背景關系的父容器,同時會讀取 `bootstrap.yml` 檔案的資訊
     * {@link ConfigFileApplicationListener} 監聽到該事件然后去決議 `application.yml` 等應用組態檔的配置資訊
     */
    listeners.environmentPrepared(environment);
    // <5> 將 `environment` 系結到當前 SpringApplication 上
    bindToSpringApplication(environment);
    // <6> 如果不是自定義的 Environment 則需要根據 Web 應用型別轉換成對應 Environment 型別
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    // <7> 再次進行上面第 `3` 步的處理程序,防止上面幾步對上面的 PropertySources 有修改
    ConfigurationPropertySources.attach(environment);
    // <8> 回傳準備好的 `environment`
    return environment;
}

該程序如下:

  1. 根據 Web 應用的型別創建一個 StandardEnvironment 物件 environment

    private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        switch (this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
        }
    }
    
  2. environment 配置默認屬性(如果有)并設定需要激活的 profile

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            // <1> 獲取 ConversionService 型別轉換器并設定到 Environment 物件中
            ConversionService conversionService = ApplicationConversionService.getSharedInstance();
            environment.setConversionService((ConfigurableConversionService) conversionService);
        }
        // <2> 配置屬性源里面的屬性
        // 例如有默認屬性則將他們添加至最后,命令列中的引數則添加至最前面
        configurePropertySources(environment, args);
        // <3> 設定需要激活的 `profile` 們
        // 例如通過 `-Dspring.profiles.active=dev` 配置需要激活的環境
        configureProfiles(environment, args);
    }
    
    protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
        MutablePropertySources sources = environment.getPropertySources();
        // 如果默認配置屬性不為空則添加至最后
        if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
            sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
        }
        // 如果 `main` 方法有入參
        if (this.addCommandLineProperties && args.length > 0) {
            String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
            if (sources.contains(name)) {
                PropertySource<?> source = sources.get(name);
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(
                        new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
                composite.addPropertySource(source);
                sources.replace(name, composite);
            }
            else {
                // 將命令列中的入參添加至最前面
                // 例如 'java -jar xxx.jar --spring.profiles.active=dev',那么這里就可以獲取到
                sources.addFirst(new SimpleCommandLinePropertySource(args));
            }
        }
    }
    

    可以看到會將 main(String[] args) 方法入參中的 -- 開頭的引數設定到 Environment 中

  3. 將當前 environment 的 MutablePropertySources 封裝成 SpringConfigurationPropertySources 添加到 MutablePropertySources 首部

  4. 對所有的 SpringApplicationRunListener 廣播 ApplicationEvent 應用環境已準備好的事件,這一步比較復雜,例如 Spring Cloud 的 BootstrapApplicationListener 監聽到該事件會創建一個 ApplicationContext 作為當前 Spring 應用背景關系的父容器,同時會讀取 bootstrap.yml 檔案的資訊

    這里會有一個 ConfigFileApplicationListener 監聽到該事件然后去決議 application.yml 等應用組態檔的配置資訊

  5. environment 系結到當前 SpringApplication 上

  6. 如果不是自定義的 Environment 則需要根據 Web 應用型別轉換成對應 Environment 型別

  7. 再次進行上面第 3 步的處理程序,防止上面幾步對上面的 PropertySources 有修改

  8. 回傳準備好的 environment

該方法準備好了當前應用 Environment 環境,主要在于上面第 4 步,是 ApplicationListener 監聽器的一個擴展點,在這里會加載出所有的配置資訊,包括 application.yml 和外部配置,決議配置的程序比較復雜,在后面的文章中單獨分析

8. createApplicationContext 方法

createApplicationContext() 方法,對 context(Spring 背景關系)進行實體化,如下:

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            switch (this.webApplicationType) {
            case SERVLET:
                // org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
                contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                break;
            case REACTIVE:
                // org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext
                contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                break;
            default:
                // org.springframework.context.annotation.AnnotationConfigApplicationContext
                contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
            }
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                    "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
        }
    }
    // 實體化這個 Class 物件
    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

不同的應用型別創建不同的 Spring 應用背景關系物件:

  • SERVLET(默認是這個):AnnotationConfigServletWebServerApplicationContext
  • REACTIVEAnnotationConfigReactiveWebServerApplicationContext
  • DEFAULTAnnotationConfigApplicationContext

10. prepareContext 方法

prepareContext(ConfigurableApplicationContext, ConfigurableEnvironment, SpringApplicationRunListeners, ApplicationArguments, Banner) 方法,對 Spring 應用背景關系做一些初始化作業,如下:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
        SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    // <1> 為 Spring 應用背景關系設定 Environment 環境
    context.setEnvironment(environment);
    // <2> 將一些工具 Bean 設定到 Spring 應用背景關系中,供使用
    postProcessApplicationContext(context);
    // <3> 通知 ApplicationContextInitializer 對 Spring 應用背景關系進行初始化作業
    // 參考 SpringApplication 構造方法
    applyInitializers(context);
    // <4> 對所有 SpringApplicationRunListener 進行廣播,發布 ApplicationContextInitializedEvent 初始化事件
    listeners.contextPrepared(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }
    // Add boot specific singleton beans
    // <5> 向應用背景關系注冊 `main(String[])` 方法的引數 Bean 和 Banner 物件
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    if (beanFactory instanceof DefaultListableBeanFactory) {
        ((DefaultListableBeanFactory) beanFactory)
                .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }
    if (this.lazyInitialization) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }
    // Load the sources
    // <6> 獲取 `primarySources`(例如你的啟動類)和 `sources`(例如 Spring Cloud 中的 @BootstrapConfiguration)源物件
    Set<Object> sources = getAllSources();
    // 應用背景關系的源物件不能為空
    Assert.notEmpty(sources, "Sources must not be empty");
    // <7> 將上面的源物件加載成 BeanDefinition 并注冊
    load(context, sources.toArray(new Object[0]));
    // <8> 對所有的 SpringApplicationRunListener 廣播 ApplicationPreparedEvent 應用已準備事件
    // 會把 ApplicationListener 添加至 Spring 應用背景關系中
    listeners.contextLoaded(context);
}

該程序如下:

  1. 為 Spring 應用背景關系設定 Environment 環境

  2. 將一些工具 Bean 設定到 Spring 應用背景關系中,供使用

    protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
        if (this.beanNameGenerator != null) {
            // 注冊 Bean 名稱生成器
            context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
                    this.beanNameGenerator);
        }
        if (this.resourceLoader != null) {
            if (context instanceof GenericApplicationContext) {
                // 設定資源加載器
                ((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
            }
            if (context instanceof DefaultResourceLoader) {
                ((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
            }
        }
        if (this.addConversionService) {
            // 設定型別轉換器
            context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
        }
    }
    
  3. 通知 ApplicationContextInitializer 對 Spring 應用背景關系進行初始化作業

    protected void applyInitializers(ConfigurableApplicationContext context) {
        for (ApplicationContextInitializer initializer : getInitializers()) {
            Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
                    ApplicationContextInitializer.class);
            Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
            // 執行 Spring 應用背景關系初始器
            // 例如 ContextIdApplicationContextInitializer 會向 Spring 應用背景關系注冊一個 ContextId 物件
            initializer.initialize(context);
        }
    }
    
  4. 對所有 SpringApplicationRunListener 進行廣播,發布 ApplicationContextInitializedEvent 初始化事件

  5. 向 Spring 應用背景關系注冊 main(String[]) 方法的引數 Bean 和 Banner 物件

  6. 獲取 primarySources(例如你的啟動類)和 sources(例如 Spring Cloud 中的 @BootstrapConfiguration)源物件,沒有的話會拋出例外

  7. 將上面的源物件加載成 BeanDefinition 并注冊

  8. 對所有的 SpringApplicationRunListener 廣播 ApplicationPreparedEvent 應用已準備事件,會把 ApplicationListener 添加至 Spring 應用背景關系中

通過上面的第 6 步你就知道為什么我們的啟動類里面一定得有一個入參為啟動類的 Class 物件了

11. refreshContext 方法

refreshContext(ConfigurableApplicationContext) 方法,重繪 Spring 應用背景關系,在這里會完成所有 Spring Bean 的初始化,同時會初始化好 Servlet 容器,例如 Tomcat,該方法涉及到 Spring IoC 的所有內容,參考 《死磕Spring之IoC篇 - Spring 應用背景關系 ApplicationContext》

private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        try {
            // 為當前 Spring 應用背景關系注冊一個鉤子函式
            // 在 JVM 關閉時先關閉 Spring 應用背景關系
            context.registerShutdownHook();
        }
        catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
    // 呼叫 AbstractApplicationContext#refresh() 方法,重繪 Spring 背景關系
    refresh(context);
}

protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    ((AbstractApplicationContext) applicationContext).refresh();
}

該方法主要是呼叫 AbstractApplicationContext#refresh() 方法,重繪 Spring 應用背景關系,整個程序牽涉到 Spring 的所有內容,之前的一系列文章已經分析過,關于更多的細節這里不展開談論,當然,這個程序會有對 @SpingBootApplication 注解的決議

根據 8. createApplicationContext 方法 方法中講到,我們默認情況下是 SERVLET 應用型別,也就是創建一個 AnnotationConfigServletWebServerApplicationContext 物件,在其父類 ServletWebServerApplicationContext 中重寫了 onRefresh() 方法,會創建一個 Servlet 容器(默認為 Tomcat),也就是當前 Spring Boot 應用所運行的 Web 環境,這部分內容放在后面的文章單獨分析,

SpringApplicationRunListeners

org.springframework.boot.SpringApplicationRunListeners,對 SpringApplicationRunListener 陣列的封裝

class SpringApplicationRunListeners {

	private final Log log;

	/**
	 * 封裝的所有 SpringApplicationRunListener
	 * Spring Boot 在 META-INF/spring.factories 只配置 {@link EventPublishingRunListener} 監聽器
	 */
	private final List<SpringApplicationRunListener> listeners;

	SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
		this.log = log;
		this.listeners = new ArrayList<>(listeners);
	}

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

	void environmentPrepared(ConfigurableEnvironment environment) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.environmentPrepared(environment);
		}
	}

	void contextPrepared(ConfigurableApplicationContext context) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.contextPrepared(context);
		}
	}

	void contextLoaded(ConfigurableApplicationContext context) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.contextLoaded(context);
		}
	}

	void started(ConfigurableApplicationContext context) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.started(context);
		}
	}

	void running(ConfigurableApplicationContext context) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.running(context);
		}
	}

	void failed(ConfigurableApplicationContext context, Throwable exception) {
		for (SpringApplicationRunListener listener : this.listeners) {
			callFailedListener(listener, context, exception);
		}
	}

	private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,
			Throwable exception) {
		try {
			listener.failed(context, exception);
		}
		catch (Throwable ex) {
			if (exception == null) {
				ReflectionUtils.rethrowRuntimeException(ex);
			}
			if (this.log.isDebugEnabled()) {
				this.log.error("Error handling failed", ex);
			}
			else {
				String message = ex.getMessage();
				message = (message != null) ? message : "no error message";
				this.log.warn("Error handling failed (" + message + ")");
			}
		}
	}
}

比較簡單,就是封裝了多個 SpringApplicationRunListener 物件,對于不同型別的事件,呼叫其不同的方法

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

可以在 META-INF/spring.factories 檔案中看到,只有一個 EventPublishingRunListener 物件

EventPublishingRunListener

org.springframework.boot.context.event.EventPublishingRunListener,實作了 SpringApplicationRunListener 介面,事件廣播器,發布不同型別的事件

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

	/**
	 * Spring Boot 應用的啟動類
	 */
	private final SpringApplication application;

	/**
	 * 啟動類 `main(String[])` 方法的入參
	 */
	private final String[] args;

	/**
	 * 事件廣播器,包含了所有的 `META-INF/spring.factories` 檔案中配置的 {@link ApplicationListener} 監聽器
	 */
	private final SimpleApplicationEventMulticaster initialMulticaster;

	public EventPublishingRunListener(SpringApplication application, String[] args) {
		this.application = application;
		this.args = args;
		this.initialMulticaster = new SimpleApplicationEventMulticaster();
		// 在實體化 SpringApplication 的程序中會從 `META-INF/spring.factories` 檔案中獲取 ApplicationListener 型別的類名稱,并進行實體化
		// 這里會將他們添加至廣播器中
		for (ApplicationListener<?> listener : application.getListeners()) {
			this.initialMulticaster.addApplicationListener(listener);
		}
	}

	@Override
	public int getOrder() {
		return 0;
	}

	@Override
	public void starting() {
		// 廣播 Spring Boot 應用正在啟動事件
		this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
	}

	@Override
	public void environmentPrepared(ConfigurableEnvironment environment) {
		// 廣播 Spring Boot 應用的 Environment 環境已準備事件
		this.initialMulticaster
				.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
	}

	@Override
	public void contextPrepared(ConfigurableApplicationContext context) {
		// 廣播 Spring Boot 應用的 Spring 背景關系已初始化事件
		this.initialMulticaster
				.multicastEvent(new ApplicationContextInitializedEvent(this.application, this.args, context));
	}

	@Override
	public void contextLoaded(ConfigurableApplicationContext context) {
		// 將所有的 ApplicationListener 添加至 Spring 應用背景關系
		for (ApplicationListener<?> listener : this.application.getListeners()) {
			if (listener instanceof ApplicationContextAware) {
				((ApplicationContextAware) listener).setApplicationContext(context);
			}
			context.addApplicationListener(listener);
		}
		// 廣播 Spring Boot 應用的 Spring 背景關系已準備事件
		this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context));
	}

	@Override
	public void started(ConfigurableApplicationContext context) {
		// 廣播 Spring Boot 應用的 Spring 背景關系已啟動事件
		context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
	}

	@Override
	public void running(ConfigurableApplicationContext context) {
		// 廣播 Spring Boot 應用的 Spring 背景關系準備就緒事件
		context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
	}

	@Override
	public void failed(ConfigurableApplicationContext context, Throwable exception) {
		ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception);
		if (context != null && context.isActive()) {
			// Listeners have been registered to the application context so we should
			// use it at this point if we can
			context.publishEvent(event);
		}
		else {
			// An inactive context may not have a multicaster so we use our multicaster to
			// call all of the context's listeners instead
			if (context instanceof AbstractApplicationContext) {
				for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
						.getApplicationListeners()) {
					this.initialMulticaster.addApplicationListener(listener);
				}
			}
			this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
			this.initialMulticaster.multicastEvent(event);
		}
	}
}

比較簡單,關鍵在于內部的 SimpleApplicationEventMulticaster 事件廣播器,里面包含了所有的 META-INF/spring.factories 檔案中配置的 ApplicationListener 監聽器,不同的方法發布不同的事件,進行廣播

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
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.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

可以看到 Spring Boot 配置了許多個 ApplicationListener,后續文章會對 ConfigFileApplicationListener 和 LoggingApplicationListener 進行簡單的分析

總結

Spring Boot 應用打成 jar 包后的啟動都是通過 SpringApplication#run(String... args) 這個方法來啟動整個 Spring 應用的,流程大致如下:

  1. META-INF/spring.factories 檔案中加載出相關 Class 物件,并進行實體化,例如 ApplicationContextInitializerSpringApplicationRunListenerApplicationListener 物件
  2. 準備好當前 Spring 應用的 Environment 環境,這里會決議 application.yml 以及外部配置
  3. 創建一個 ApplicationContext 應用背景關系物件,默認 SERVLET 型別下創建 AnnotationConfigServletWebServerApplicationContext 物件
  4. 呼叫 AbstractApplication#refresh() 方法,重繪 Spring 應用背景關系,也就是之前一系列 Spring 相關的文章所講述的內容

整個程序有許多個擴展點是通過監聽器機制實作的,在不同階段廣播不同型別的事件,此時 ApplicationListener 就可進行相關的操作

在上面第 4 步中,SERVLET 應用型別下的 Spring 應用背景關系會創建一個 Servlet 容器(默認為 Tomcat)

更多的細節在后續文章依次進行分析

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288758.html

標籤:其他

上一篇:Python 行程互斥鎖 Lock - Python零基礎入門教程

下一篇:為什么不建議用 equals 判斷物件相等?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more