Spring Boot是由Pivotal團隊提供的快速開發框架,基於SpringMVC經過註解+內置Http服務器如:tomcat-embed-core,簡化了XML配置,快速將一些經常使用的第三方依賴整合(經過Maven繼承依賴關係),最終實現以Java應用程序的方式進行執行。java
starter包含了搭建項目快速運行所需的依賴。它是一個依賴關係描述符的集合。當應用須要一種spring的其它服務時,不須要粘貼拷貝大量的依賴關係描述符。例如想在spring中使用redis,只須要在項目中包含 spring-boot-starter-redis 依賴就可使用了,全部的starters遵循一個類似的命名模式:spring-boot-starter-,在這裏是一種特殊類型的應用程序。該命名結構能夠幫你找到須要的starter。不少IDEs集成的Maven容許你經過名稱搜索依賴。react
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
這是一個簡單的SpringBoot的啓動類實現,下文章將圍繞這個demo進行擴展分析。web
如下是註解@SpringBootApplication的源碼實現redis
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {
能夠發現它是由衆多註解組合而成的,下面具體分析下這裏每一個註解所起到的做用。spring
@Retention Retention(保留)註解說明,這種類型的註解會被保留到那個階段. 有三個值:tomcat
<context:component-scan>
,可以使用basePackages屬性指定要掃描的包,及掃描的條件。若是不設置則默認掃描@ComponentScan註解所在類的同級類和同級目錄下的全部類,因此咱們的Spring Boot項目,通常會把入口類放在頂層目錄中,這樣就可以保證源碼目錄下的全部類都可以被掃描到。@EnableAutoConfiguration 這個註解是讓Spring Boot的配置可以如此簡化的關鍵性註解。我把EnableAutoConfiguration的實現端上來了,你們來鑑賞一下!服務器
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration {
關於註解的話題就先談到這裏,下面開啓擼代碼環節。併發
查看SpringApplication的源代碼能夠發現SpringApplication的啓動由兩部分組成:app
源碼以下:框架
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader;//一、初始化資源加載器 Assert.notNull(primarySources, "PrimarySources must not be null");//二、斷言資源加載類不能爲 null this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//三、初始化加載資源類集合並去重 this.webApplicationType = deduceWebApplicationType();//四、 推斷應用類型是Standard仍是Web setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class));//五、設置應用上下文初始化器 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//六、設置監聽器 this.mainApplicationClass = deduceMainApplicationClass();//七、推斷應用入口類 }
下面將針對源碼中的重要實現進行詳細的分析。
ResourceLoader接口,在 Spring 中用於加載資源,經過它能夠獲取一個Resouce 對象。使用spring的朋友都知道它加載資源的方式由多種,下面就挑兩個經常使用的繼承ResourceLoader的接口與實現類提及。
上面介紹過該類經過實現 ResourceLoader 接口實現了加載單個資源的功能。它的子類經過繼承它來實現具體的資源訪問策略。下面來探究下該類如何加載單個資源:
public Resource getResource(String location) {//這裏是三種識別location加載出Resource的方式。 Assert.notNull(location, "Location must not be null"); //1.先看有沒有自定義的ProtocolResolver,若是有則先根據自定義的ProtocolResolver解析location獲得Resource for (ProtocolResolver protocolResolver : this.protocolResolvers) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } //2.根據路徑是否匹配"/"或"classpath:"來解析獲得ClassPathResource if (location.startsWith("/")) { return getResourceByPath(location); }else if (location.startsWith(CLASSPATH_URL_PREFIX)) {//classpath return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); }else { try { //默認傳入的location是一個URL路徑,加載獲得一個UrlResource URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // 若是以上三種狀況都不知足,則按照「/」來處理 return getResourceByPath(location); } } }
掃盲:ProtocolResolver是解析location的自定義拓展類,有了它咱們才能隨意傳入不一樣格式的location,而後根據對應的格式去解析並得到咱們的Resource便可。
擴展:在Spring容器初始化過程當中,咱們能夠自定義一個類實現ProtocolResolver接口,而後實現該resolve方法,就能夠解析特定的location獲得Resoure。
該接口繼承了ResourceLoader接口,在其基礎上增長了同時對多個資源的訪問功能。
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; // 例如使用 ant 風格的路徑,匹配路徑下的多個資源 Resource[] getResources(String locationPattern) throws IOException; }
PathMatchingResourcePatternResolver是 ResourcePatternResolver 接口的直接實現類,它是基於模式匹配的,默認使用AntPathMatcher 進行路徑匹配,它除了支持 ResourceLoader 支持的前綴外,還額外支持 「classpath*」 ,下面查看源碼看看它如何實現對多個資源的訪問。
public Resource[] getResources(String locationPattern) throws IOException { Assert.notNull(locationPattern, "Location pattern must not be null"); //首先判斷資源路徑是不是類路徑下的資源(以 「classpath*:」 開頭) if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { // 經過 getPathMatcher 方法取得 PathMatcher ,默認只有 AntPathMatcher 一個實現類 // 經過 isPattern 方法判斷路徑是否容許存在多個匹配的資源(路徑中包含 「*」 或 「?」) if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { //找到全部匹配路徑(ant 風格)的資源 return findPathMatchingResources(locationPattern); } else { //經過類加載器查找 return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(':') + 1); // 判斷資源路徑 ":" 以後的部分是否包含 "*" 或 "?" if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { // a file pattern return findPathMatchingResources(locationPattern); } else { // 若不存在表示是單個資源,則經過從構造函數傳入的 ResourceLoader 取得 return new Resource[] {getResourceLoader().getResource(locationPattern)}; } } }
擴展:查看源碼發現ApplicationContext接口也繼承了 ResourcePatternResolver 接口,說明它也集成了對對單個或多個資源的訪問功能。
Resource res = ctx.getResource("some/resource/path/myTemplate.txt);
Spring採用和 ApplicationContext 相同的策略來訪問資源。也就是說:
也就是說 ApplicationContext 將會肯定具體的資源訪問策略,從而將應用程序和具體的資源訪問策略分離開來,這就體現了策略模式的優點。
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext>{ void initialize(C applicationContext); }
}方法得到ApplicationContextInitializer接口所有實現類的完整名稱,最後經過反射的機制得到ApplicationContextInitializer實現類。
ApplicationContextInitializer.class)方法得到實現類
listeners成員變量,是一個ApplicationListener<?>類型對象的集合。能夠看到獲取該成員變量內容使用的是跟成員變量initializers同樣的方法,只不過傳入的類型從ApplicationContextInitializer.class變成了ApplicationListener.class。
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { void onApplicationEvent(E event); }
這個接口基於JDK中的EventListener接口,實踐了觀察者模式。對於Spring框架的觀察者模式實現,它限定感興趣的事件類型須要是ApplicationEvent類型的子類,而這個類一樣是繼承自JDK中的EventObject類。
該方法經過構造一個運行時異常,經過異常棧中方法名爲main的棧幀來獲得main()所在類的名字。
private Class<?> deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if ("main".equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { // Swallow and continue } return null; }
源碼以下:
public ConfigurableApplicationContext run(String... args) { //一、計時監控類 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 二、初始化應用上下文和異常報告集合 ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 三、設置系統屬性java.awt.headless的值,默認值爲:true(沒有圖形化界面) configureHeadlessProperty(); // 四、建立全部 Spring 運行監聽器併發出開始執行的事件 SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { // 五、初始化默認應用參數類 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); // 六、根據SpringApplicationRunListeners和應用參數來準備 Spring 環境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); // 七、準備Banner打印器 - 就是啓動Spring Boot的時候打印在console上的ASCII藝術字體 Banner printedBanner = printBanner(environment); // 八、建立Spring上下文 context = createApplicationContext(); // 九、準備異常報告器 exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 十、Spring上下文前置處理 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 十一、刷新Spring上下文 refreshContext(context); // 十二、Spring上下文後置處理 afterRefresh(context, applicationArguments); // 1三、中止計時監控類 stopWatch.stop(); // 1四、輸出日誌記錄執行主類名、時間信息 if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } // 1五、發佈應用上下文啓動完成事件 listeners.started(context); // 1六、執行全部 Runner 運行器 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // 1七、發佈應用上下文就緒事件 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } // 1八、返回應用上下文 return context; }
下面針對run方法中提到的一些重要步驟進行闡述:
啓動方法以下:
public void start() throws IllegalStateException { start(""); } public void start(String taskName) throws IllegalStateException { if (this.currentTaskName != null) { throw new IllegalStateException("Can't start StopWatch: it's already running"); } this.currentTaskName = taskName; this.startTimeMillis = System.currentTimeMillis(); }
你能夠看到它傳入了一個空字符串給當前任務做爲任務名稱,而後記錄當前Spring Boot應用啓動的開始時間。而且它會判斷當前任務名是否存在,用於保證Spring Boot應用不重複啓動。
這裏僅初始化一個應用上下文對象context和一個空的異常報告集合,具體用途看下文。
configureHeadlessProperty的方法實現以下:
private void configureHeadlessProperty() { System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty( SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless))); } private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";
該方法設置了一個名爲java.awt.headless的系統屬性,觀其源碼能夠發現它給屬性設值System.setProperty(),而它的值來源於System.getProperty(),這並不奇怪,由於System中getProperty()有2個重載方法,其中getProperty()有單參和雙參兩個方法,這裏調用的是雙參數方法,該方法在沒有的時候會返回一個調用者指定的默認值,因此這裏先獲取後設置。這個設置的目的是保證即便沒有檢測到顯示器(服務器不須要顯示器),也容許程序啓動。
實現以下:
//根據args獲取全部SpringApplicationRunListeners監聽器 SpringApplicationRunListeners listeners = getRunListeners(args); //這段代碼你們應該已經熟悉了,獲取SpringApplicationRunListeners擴展 private SpringApplicationRunListeners getRunListeners(String[] args) { Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class}; return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)); }
經過Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};類型加載對應的監聽器,並建立SpringApplicationRunListener實例
下面的內容和以前實例化初始化器的流程是同樣,經過getSpringFactoriesInstances方法從spring.factories中獲取與SpringApplicationRunListener.class相關的實例類名列表。
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
args是啓動Spring應用的命令行參數,該參數能夠在Spring應用中被訪問。
如:--server.port=9000
建立並配置當前SpringBoot應用將要使用的Environment(包括配置要使用的PropertySource以及Profile(其做用就是指定激活的配置文件,能夠區分環境來加載不一樣的配置)),並遍歷調用全部的SpringApplicationRunListener的environmentPrepared()方法,廣播Environment準備完畢。
源碼以下:
private ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { //獲取或建立環境(存在就直接返回,不存在建立一個再返回) ConfigurableEnvironment environment = getOrCreateEnvironment(); //配置環境:配置PropertySources和activeProfiles configureEnvironment(environment, applicationArguments.getSourceArgs()); //listeners環境準備(就是廣播ApplicationEnvironmentPreparedEvent事件)。 listeners.environmentPrepared(environment); //將環境綁定到SpringApplication bindToSpringApplication(environment); //若是非web環境,將環境轉換成StandardEnvironment if (this.webApplicationType == WebApplicationType.NONE) { environment = new EnvironmentConverter(getClassLoader()) .convertToStandardEnvironmentIfNecessary(environment); } //配置PropertySources對它本身的遞歸依賴 ConfigurationPropertySources.attach(environment); return environment; }
prepareEnvironment的做用:加載外部化配置資源到environment,包括命令行參數、servletConfigInitParams、servletContextInitParams、systemProperties、sytemEnvironment、random、application.yml(.yaml/.xml/.properties)等;初始化日誌系統。
該功能僅供自娛自樂,不作過多解讀,看源碼即是。
private Banner printBanner(ConfigurableEnvironment environment) { if (this.bannerMode == Banner.Mode.OFF) { return null; } ResourceLoader resourceLoader = (this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader(getClassLoader())); SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter( resourceLoader, this.banner); if (this.bannerMode == Mode.LOG) { return bannerPrinter.print(environment, this.mainApplicationClass, logger); } return bannerPrinter.print(environment, this.mainApplicationClass, System.out); }
源碼以下:
public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext"; public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext"; public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context.annotation.AnnotationConfigApplicationContext"; protected ConfigurableApplicationContext createApplicationContext() { // 先判斷有沒有指定的實現類 Class<?> contextClass = this.applicationContextClass; // 若是沒有,則根據應用類型選擇 if (contextClass == null) { try { //根據webApplicationType的類型去反射建立ConfigurableApplicationContext的具體實例。 switch (this.webApplicationType) { case SERVLET: contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { throw new IllegalStateException( "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass", ex); } } // 經過反射獲取對應類的實例 return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }
createApplicationContext()該方法的邏輯較爲簡單,共兩個分支:
這一步的邏輯和實例化初始化器和監聽器的同樣,都是經過調用 getSpringFactoriesInstances 方法來獲取配置的異常類名稱並實例化全部的異常處理類。
源碼以下:
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { //設置容器環境,包括各類變量 context.setEnvironment(environment); //設置上下文的 bean 生成器和資源加載器 postProcessApplicationContext(context); //執行容器中的ApplicationContextInitializer(包括 spring.factories和自定義的實例) applyInitializers(context); //觸發全部 SpringApplicationRunListener 監聽器的 contextPrepared 事件方法 listeners.contextPrepared(context); //記錄啓動日誌 if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // 添加特定於引導的單例bean context.getBeanFactory().registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { context.getBeanFactory().registerSingleton("springBootBanner", printedBanner); } // 加載全部資源 Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty"); //加載咱們的啓動類,將啓動類注入容器 load(context, sources.toArray(new Object[0])); //觸發全部 SpringApplicationRunListener 監聽器的 contextLoaded 事件方法 listeners.contextLoaded(context); }
這塊會對整個上下文進行一個預處理,好比觸發監聽器的響應事件、加載資源、設置上下文環境等等。
源碼以下:
private void refreshContext(ConfigurableApplicationContext context) { refresh(context); if (this.registerShutdownHook) { try { //向JVM運行時註冊一個關機鉤子,在JVM關機時關閉這個上下文 context.registerShutdownHook(); } catch (AccessControlException ex) { // Not allowed in some environments. } } }
這塊主要作了兩件事:
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) { }
該方法沒有實現,能夠根據須要作一些定製化的操做。
源碼以下:
public void stop() throws IllegalStateException { if (this.currentTaskName == null) { throw new IllegalStateException("Can't stop StopWatch: it's not running"); } long lastTime = System.currentTimeMillis() - this.startTimeMillis; this.totalTimeMillis += lastTime; this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime); if (this.keepTaskList) { this.taskList.add(this.lastTaskInfo); } ++this.taskCount; this.currentTaskName = null; }
該方法主要是作計時監聽器中止操做,並統計一些任務執行信息。
源碼以下:
if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); }
用於打印主類信息和時間信息。
源碼以下:
public void started(ConfigurableApplicationContext context) { for (SpringApplicationRunListener listener : this.listeners) { listener.started(context); } }
執行全部SpringApplicationRunListener實現的started方法。
Runner 運行器用於在服務啓動時進行一些業務初始化操做,這些操做只在服務啓動後執行一次。
Spring Boot提供了ApplicationRunner和CommandLineRunner兩種服務接口CommandLineRunner、ApplicationRunner
對比:
相同點
不一樣點
源碼以下:
private void callRunners(ApplicationContext context, ApplicationArguments args) { List<Object> runners = new ArrayList<>(); // 從Spring容器中獲取ApplicationRunner實現類 runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); // 從Spring容器中獲取CommandLineRunner實現類 runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); //排序 AnnotationAwareOrderComparator.sort(runners); //回調 for (Object runner : new LinkedHashSet<>(runners)) { if (runner instanceof ApplicationRunner) { callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); } } }
ApplicationRunner仍是CommandLineRunner,都是在應用啓動完成後執行一次業務初始化代碼,達到的效果也相似,因爲ApplicationRunner的方法參數是ApplicationArguments對象,使用起來更加方便,因此更推薦使用。
源碼以下
public void running(ConfigurableApplicationContext context) { for (SpringApplicationRunListener listener : this.listeners) { listener.running(context); } }
觸發全部 SpringApplicationRunListener 監聽器的 running 事件方法。
SpirngBoot啓動流程到這裏就分析完了、接下來的文章會針對SpringBoot的特定場景進行分析,但願每一個看到這篇文章的朋友都能有所收穫,同時也歡迎你們多提寶貴意見共同成長。