前言
本篇文章包含Springboot配置文件解釋、熱部署、自動裝配原理源碼級剖析、內嵌tomcat源碼級剖析、緩存深入、多環境部署等等,如果能耐心看完,想必會有不少收獲。
一、Spring Boot基礎應用
Spring Boot特征
概念:
約定優于配置,簡單來說就是你所期待的配置與約定的配置一致,那么就可以不做任何配置,約定不符合期待時才需要對約定進行替換配置。
特征:
1. SpringBoot Starter:他將常用的依賴分組進行了整合,將其合并到一個依賴中,這樣就可以一次性添加到項目的Maven或Gradle構建中。
2,使編碼變得簡單,SpringBoot采用 JavaConfig的方式對Spring進行配置,并且提供了大量的注解,極大的提高了工作效率,比如@Configuration和@bean注解結合,基于@Configuration完成類掃描,基于@bean注解把返回值注入IOC容器。
3.自動配置:SpringBoot的自動配置特性利用了Spring對條件化配置的支持,合理地推測應用所需的bean并自動化配置他們。
4.使部署變得簡單,SpringBoot內置了三種Servlet容器,Tomcat,Jetty,undertow.我們只需要一個Java的運行環境就可以跑SpringBoot的項目了,SpringBoot的項目可以打成一個jar包。
Spring Boot創建
Spring Boot項目結構圖:
Spring Boot熱部署
通過引入spring-bootdevtools插件,可以實現不重啟服務器情況下,對項目進行即時編譯。引入熱部署插件的步驟如下:
1. 在pom.xml添加熱部署依賴
2. IDEA熱部署工具設置
3. 在項目任意頁面中使用組合快捷鍵“Ctrl+Shift+Alt+/”打開Maintenance選項框,選中并打開Registry頁面,列表中找到“compiler.automake.allow.when.app.running”,將該選項后的Value值勾選,用于指定IDEA工具在程序運行過程中自動編譯,最后單擊【Close】按鈕完成設置。
熱部署原理:
基本原理就是我們在編輯器上啟動項目,然后改動相關的代碼,然后編輯器自動觸發編譯,替換掉歷史的.class文件后,項目檢測到有文件變更后會重啟srpring-boot項目。內部主要是通過引入的插件對我們的classpath資源變化進行監聽,當classpath有變化,才會觸發重啟。
從官方文檔可以得知,其實這里對類加載采用了兩種類加載器,對于第三方jar包采用baseclassloader來加載,對于開發人員自己開發的代碼則使用restartClassLoader來進行加載,這使得比停掉服務重啟要快的多,因為使用插件只是重啟開發人員編寫的代碼部分。
排除資源:
默認情況下,改變資源 /META-INF/maven , /META-INF/resources , /resources , /static , /public ,或 /templates 不觸發重新啟動,但確會觸發現場重裝。如果要自定義這些排除項,則可以使用該spring.devtools.restart.exclude 屬性。例如,僅排除 /static , /public 在application.properties設置以下屬性。
spring.devtools.restart.exclude=static/**,public/**,config/**
全局配置文件優先級
優先級:以下圖順序號代表配置文件的優先級,并且相同配置文件按順序加載可以實現互補,但是不會被覆蓋。
注意:Spring Boot 有application.properties 和 application.yaml 兩種配置文件的方式,yaml是一種JSON超文本格式文件,如果是2.4.0之前版本,優先級properties>yaml;但是如果是2.4.0的版本,優先級yaml>properties。
自定義application.properties 配置文件注入IOC容器
填加相應依賴配置可以實現在自定義配置properties配置提示
@ConfigurationProperties(prefix = “person”)注解的作用是將配置文件中以person開頭的屬性值通過setXX()方法注入到實體類對應屬性中。
@Component注解的作用是將當前注入屬性值的Person類對象作為Bean組件放到Spring容器中,只有這樣才能被@ConfigurationProperties注解進行賦值。
application.yaml配置文件
YAML文件格式是Spring Boot支持的一種JSON超集文件格式,以數據為中心,比properties、xml等更
適合做配置文件.
1.yml和xml相比,少了一些結構化的代碼,使數據更直接,一目了然
2.相比properties文件更簡潔
3.yaml文件的擴展名可以使用.yml或者.yaml。
4.application.yml文件使用 “key:(空格)value”格式配置屬性,使用縮進控制層級關系。
屬性注入
如果配置屬性是Spring Boot已有屬性,例如服務端口server.port,那么Spring Boot內部會自動掃描并讀取這些配置文件中的屬性值并覆蓋默認屬性。
@Configuration:聲明一個類作為配置類。
@Bean:聲明在方法上,將方法的返回值加入Bean容器。
@Value:屬性注入
@ConfigurationProperties(prefix = “jdbc”):批量屬性注入。
@PropertySource(“classpath:/jdbc.properties”)指定外部屬性文件,在類上添加。
第三方配置:
@ConfigurationProperties 用于注釋類之外,您還可以在公共 @Bean 方法上使用它。將屬性綁定到控件之外的第三方組件
松散綁定:
Spring Boot使用一些寬松的規則將環境屬性綁定到@ConfigurationProperties bean,因此環境屬性名和bean屬性名之間不需要完全匹配,比如在application.properties文件里定義一個first-name=tom,在對應bean類中使用firstName也能獲取到對應的值,這就是松散綁定。
Spring Boot日志框架
SLF4J 的使用:
注意:由于每一個日志的實現框架都有自己的配置文件,所以在使用 SLF4j 之后,配置文件還是要使用實現日志框架的配置文件。
統一日志框架使用:
實現步驟
1. 排除系統中的其他日志框架。
2. 使用中間包替換要替換的日志框架。
3. 導入我們選擇的 SLF4J 實現。
從圖中我們得到一種統一日志框架使用的方式,可以使用一種要替換的日志框架類完全一樣的 jar 進行替換,這樣不至于原來的第三方 jar 報錯,而這個替換的 jar 其實使用了 SLF4J API. 這樣項目中的日志就都可以通過 SLF4J API 結合自己選擇的框架進行日志輸出。
Spring Boot 的日志關系:
Spring Boot 默認已經使用了 SLF4J + LogBack . 所以我們在不進行任何額外操作的情況下就可以使用 SLF4J + Logback 進行日志輸出。SLF4J 日志級別從小到大trace,debug,info,warn,error,默認是info級別。
自定義日志輸出:
可以在配置文件編寫日志相關配置實現自定義日志輸出。
替換日志框架:
二、Spring Boot源碼分析
spring-boot-starter-parent
Spring Boot項目的統一版本父項目依賴管理。
在底層源文件定義了工程的Java版本;工程代碼的編譯源文件編碼格式;工程編譯后的文件編碼格式;Maven打包編譯的版本。
接著在build節點做了資源過濾
接著從spring-boot-starter-parent找到他父依賴 spring-boot-dependencies,從里面就可以發現里面定義了各種版本聲明,通過這里聲明可以讓部分依賴不需要寫版本號,一些沒有引入的第三方jar包仍然需要自己聲明版本號。
spring-boot-starter-web
Spring Boot項目的所依賴jar包進行打包起步依賴管理
在spring-boot-starter-web的父依賴spring-boot-starters包中,可以發現在他的dependencies標簽有著各種依賴包引入,點進去就是具體包的導入配置管理。
注意:Spring Boot官方并不是針對所有場景開發的技術框架都提供了場景啟動器,例如阿里巴巴的Druid數據源等,Spring Boot官方就沒有提供對應的依賴啟動器。為了充分利用Spring Boot框架的優勢,在Spring Boot官方沒有整合這些技術框架的情況下,Druid等技術框架所在的開發團隊主動與Spring Boot框架進行了整合,實現了各自的依賴啟動器,例如druid-spring-boot-starter等。我們在pom.xml文件中引入這些第三方的依賴啟動器時,切記要配置對應的版本號。
自動配置@SpringBootApplication
他是一個組合注解,核心代碼:
自動配置@SpringBootConfiguration
通過上面可以發現我們的核心啟動類注解源碼中含此注解,這個注解標注在某個類上,表示這是一個 Spring Boot的配置類。他的核心代碼中,內部有一個核心注解@Configuration來表明當前類是配置類,并且可以被組件掃描器掃到,所以@SpringBootConfiguration與@Configuration具有相同作用,只是前者又做了一次封裝。
自動配置@ EnableAutoConfiguration
Spring 中有很多以 Enable 開頭的注解,其作用就是借助 @Import 來收集并注冊特定場景相關的Bean ,并加載到 IOC 容器。而這個注解就是借助@Import來收集所有符合自動配置條件的bean定義,并加載到IoC容器,他的核心源碼如下:
通過@AutoConfigurationPackage注解進入類別,發現他通過import引入了一個AutoConfigurationPackages.Registrar.class,在Registrar.class中就重寫了一個registerBeanDefinitions方法,在方法內部調用了一個register方法來實現將注解標注的元信息傳入,獲取到相應的包名。通俗點就是注冊bean,然后根據 @AutoConfigurationPackage找到需要注冊bean的類路徑,這個路徑就被自動保存了下來,后面需要使用bean,就直接獲取使用,比如Spring Boot整合JPA可以完成一些注解掃描。
自動配置@Import(AutoConfigurationImportSelector.class)
該注解是Spring boot的底層注解,AutoConfigurationImportSelector類可以幫助 Springboot 應用將所有符合條件的 @Configuration配置都加載到當前Spring Boot創建并使用的IOC容器( ApplicationContext )中。
該注解實現了實現了 DeferredImportSelector 接口和各種Aware 接口,在源碼中截圖中,通過四個接口回調,把值返回給了
定義的四個成員變量。
1.自動配置邏輯相關的入口方法在 DeferredImportSelectorGrouping 類的 getImports 方法。
2.自動配置的相關的絕大部分邏輯全在第一處也就是this.group.proces方法里,主要做的事就是在方法中,傳入的 AutoConfigurationImportSelector對象來選擇一些符合條件的自動配置類,過濾掉一些不符合條件的自動配置類,而第二處的this.group.selectImports的方法主要是針對前面的process方法處理后的自動配置類再進一步有選擇的選擇導入。
3.進入getAutoConfigurationEntry方法,這個方法主要是用來獲取自動配置類有關,承擔了自動配置的主要邏輯。AutoConfigurationEntry 方法主要做的事情就是獲取符合條件的自動配置類,避免加載不必要的自動配置類從而造成內存浪費
4.進入getCandidateConfigurations方法,里面有一個重要方法 loadFactoryNames ,這個方法是讓 SpringFactoryLoader 去加載一些組件的名字。
5.進入 loadFactoryNames方法,獲取到出入的鍵factoryClassName。
6.進入loadSpringFactories方法,加載配置文件,而這個配置文件就是spring.factories文件
由此我們可以知道,在這個方法中會遍歷整個ClassLoader中所有jar包下的spring.factories文件。spring.factories里面保存著springboot的默認提供的自動配置類。
AutoConfigurationEntry 方法主要做的事情:
【1】從 spring.factories 配置文件中加載 EnableAutoConfiguration 自動配置類),獲取的自動配置類如圖所示。
【2】若 @EnableAutoConfiguration 等注解標有要 exclude 的自動配置類,那么再將這個自動配置類排除掉;
【3】排除掉要 exclude 的自動配置類后,然后再調用 filter 方法進行進一步的過濾,再次排除一些不符合條件的自動配置類;
【4】經過重重過濾后,此時再觸發 AutoConfigurationImportEvent 事件,告訴ConditionEvaluationReport 條件評估報告器對象來記錄符合條件的自動配置類;
【5】 最后再將符合條件的自動配置類返回。
AutoConfigurationImportSelector 的 filter 方法
主要做的事情就是調用AutoConfigurationImportFilter 接口的 match 方法來判斷每一個自動配置類上的條件注解(若有的話) @ConditionalOnClass , @ConditionalOnBean 或 @ConditionalOnWebApplication 是否滿足條件,若滿足,則返回true,說明匹配,若不滿足,則返回false說明不匹配。其實就是排除自動配置類,因為全部加載出來的類太多,不需要全部都反射成對象,而這個方法就是通過注解進行該自動配置類是否有相應匹配的類的判斷,存在即加入,不存在即過濾。
@Conditional是Spring4新提供的注解,它的作用是按照一定的條件進行判斷,滿足條件給容器注冊bean。
@ConditionalOnBean:僅僅在當前上下文中存在某個對象時,才會實例化一個Bean。
@ConditionalOnClass:某個class位于類路徑上,才會實例化一個Bean。
@ConditionalOnExpression:當表達式為true的時候,才會實例化一個Bean。基于SpEL表達式的條件判斷。
@ConditionalOnMissingBean:僅僅在當前上下文中不存在某個對象時,才會實例化一個Bean。
@ConditionalOnMissingClass:某個class類路徑上不存在的時候,才會實例化一個Bean。
@ConditionalOnNotWebApplication:不是web應用,才會實例化一個Bean。
@ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。
@ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。
@ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。
@ConditionalOnJava:當JVM版本為指定的版本范圍時觸發實例化。
@ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。
@ConditionalOnJndi:在JNDI存在的條件下觸發實例化。
@ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發實例化。
有選擇的導入自動配置類
在第一步最后的一個方法this.group.selectImports主要是針對經過排除掉 exclude 的和被AutoConfigurationImportFilter 接口過濾后的滿足條件的自動配置類再進一步排除 exclude 的自動配置類,然后再排序,至此實現了自動配置。
綜合以上總結:
自動配置HttpEncodingAutoConfiguration實例
1. SpringBoot 啟動會加載大量的自動配置類
2.我們看我們需要實現的功能有沒有 SpringBoot 默認寫好的自動配置類
3.我們再來看這個自動配置類中到底配置了哪些組件;(只要我們有我們要用的組件,我們就不需要再來配置了)
4.給容器中自動配置類添加組件的時候,會從 properties 類中獲取某些屬性,我們就可以在配置文件中指定這些屬性的值。
xxxAutoConfiguration :自動配置類,用于給容器中添加組件從而代替之前我們手動完成大量繁瑣的配置。
xxxProperties : 封裝了對應自動配置類的默認屬性值,如果我們需要自定義屬性值,只需要根據xxxProperties 尋找相關屬性在配置文件設值即可。
@ComponentScan注解
主要作用是從定義的掃描路徑中,找出標識了需要裝配的類自動裝配到spring 的bean容器中。默認掃描路徑是為@ComponentScan注解的類所在的包為基本的掃描路徑(也就是標注了@SpringBootApplication注解的項目啟動類所在的路徑),所以這里就解釋了之前spring boot為什么只能掃描自己所在類的包及其子包。
常用屬性:
basePackages、value:指定掃描路徑,如果為空則以@ComponentScan注解的類所在的包為基本的掃描路徑。
basePackageClasses:指定具體掃描的類。
includeFilters:指定滿足Filter條件的類。
excludeFilters:指定排除Filter條件的類。
SpringApplication() 構造方法
第二步getSpringFactoriesInstances方法解析
主要就是 loadFactoryNames()方法,這個方法是spring-core中提供的從META-INF/spring.factories中獲取指定的類(key)的同一入口方法,獲取的是key為 org.springframework.context.ApplicationContextInitializer 的類,是Spring框架的類, 這個類的主要目的就是在ConfigurableApplicationContext 調用refresh()方法之前,回調這個類的initialize方法。通過 ConfigurableApplicationContext 的實例獲取容器的環境Environment,從而實現對配置文件的修改完善等工作。
源碼剖析Run方法整體流程
重要六步:
第一步:獲取并啟動監聽器
第二步:構造應用上下文環境
第三步:初始化應用上下文
第四步:刷新應用上下文前的準備階段
第五步:刷新應用上下文
第六步:刷新應用上下文后的擴展接口
Run方法第一步:獲取并啟動監聽器
事件機制在Spring是很重要的一部分內容,通過事件機制我們可以監聽Spring容器中正在發生的一些事件,同樣也可以自定義監聽事件。Spring的事件為Bean和Bean之間的消息傳遞提供支持。當一個對象處理完某種任務后,通知另外的對象進行某些處理,常用的場景有進行某些操作后發送通知,消息、郵件等情況。
通過getRunListeners方法來獲取監聽器,在getRunListeners方法內部調用了一個getSpringFactoriesInstances方法,返回值是一個SpringApplicationRunListeners有參構造的監聽器類,這個方法加載SpringApplicationRunListener類,把這個類當做key,這個類的作用就是負責在SpringBoot啟動的不同階段,廣播出不同的消息,傳遞給ApplicationListener監聽器實現類。
getSpringFactoriesInstances方法被重復使用。
總結:如何獲取到監聽器并進行啟動開啟監聽。
Run方法第二步:構造應用上下文環境
應用上下文環境包括什么呢?包括計算機的環境,Java環境,Spring的運行環境,Spring項目的配置(在SpringBoot中就是那個熟悉的application.properties/yml)等等。
通過prepareEnvironment方法創建并按照相應的應用類型配置相應的環境,然后根據用戶的配置,配置系統環境,然后啟動監聽器,并加載系統配置文件。
主要步驟方法:
getOrCreateEnvironment方法
configureEnvironment方法
listeners.environmentPrepared方法
總結:最終目的就是把環境信息封裝到environment對象中,方便后面使用。
Run方法第三步:初始化應用上下文
通過createApplicationContext方法構建應用上下文對象context,而context中有一個屬性beanFactory他是一個DefaultListableBeanFactory類,這就是我們所說的IoC容器。應用上下文對象初始化的同時IOC容器也被創建了。
在SpringBoot工程中,應用類型分為三種
通過反射拿到配置類的字節碼對象并通過BeanUtils.instantiateClass方法進行實例化并返回。
總結:就是創建應用上下文對象同時創建了IOC容器。
Run方法第四步:刷新應用上下文前的準備階段
主要的目的就是為前面的上下文對象context進行一些屬性值的設置,在執行過程中還要完成一些Bean對象的創建,其中就包含核心啟動類的創建。
屬性設置
Bean對象創建
Spring容器在啟動的時候,會將類解析成Spring內部的beanDefintion結構,并將beanDefintion存儲到DefaultListableBeanFactory的Map中。BeanDefinitionLoader方法就是完成賦值。
總結:就是應用上下文屬性的設置并把核心啟動類生成實例化對象存儲到容器中。
Run方法第五步:刷新應用上下文
Spring Boot的自動配置原理:通常來說主要就是依賴核心啟動類上面的@SpringBootApplication注解,這個注解是一個組合注解,他組合了@EnableAutoConfiguration這個注解,在run方法啟動會執行getImport方法,最終找到process方法,進行注解的掃描,通過注解組合關系,在底層借助@Import注解向容器導入AutoConfigurationImportSelector.class組件類,這個類在執行過程中他會去加載WEB-INF下名稱為spring.factories的文件,從這個文件中根據EnableAutoConfiguration這個key來加載pom.xml引入的所有對應自動配置工廠類的全部路徑配置,在經過過濾,選出真正生效的自動配置工廠類去生成實例存到容器中,從而完成自動裝配。如果從Main方法的Run方法出發,了解實際實現的原理,就能知道他是怎么通過Main方法找到主類,然后再掃描主類注解,完成一系列操作。而在刷新應用上下文這步就是根據找到的主類來執行解析注解,完成自動裝配的一系列過程。
通過refreshContext()方法一路跟下去,最終來到AbstractApplicationContext類的refresh()方法,其中最重要的方法就是invokeBeanFactoryPostProcessors方法,他就是在上下文中完成Bean的注冊。
運行步驟:
1.prepareRefresh()刷新上下文
2.obtainFreshBeanFactory()在第三步初始化應用上下文中我們創建了應用的上下文,并觸發了GenericApplicationContext類的構造方法如下所示,創建了beanFactory,也就是創建了DefaultListableBeanFactory類,這里就是拿到之前創建的beanFactory。
3.prepareBeanFactory()對上面獲取的beanFactory,準備bean工廠,以便在此上下文中使用。
4.postProcessBeanFactory()向上下文中添加了一系列的Bean的后置處理器。
接著就進入到我們最重要的invokeBeanFactoryPostProcessors()方法,完成了IoC容器初始化過程的三個步驟:
1) 第一步:Resource定位
2) 第二步:BeanDefinition的載入
3) 第三個過程:注冊BeanDefinition
總結:spring啟動過程中,就是通過各種掃描,獲取到對應的類,然后將類解析成spring內部的BeanDefition結構,存到容器中(注入到ConCurrentHashMap中),也就是最后的beanDefinitionMap中。
第一步分析:
主要方法,從invokeBeanFactoryPostProcessors方法一直往下跟,直到ConfigurationClassPostProcessor類的parse方法,會發現他把核心啟動類傳入了這個方法中。
在這個方法內部,他判斷這個類上知否存在注解,如果存在繼續進入下一個方法,直到真正做事的doProcessConfigurationClass方法,在這個方法類,他就開始處理@ComponentScan注解,獲取到componentScans對象,然后調用this.componentScanParser.parse方法對他進行解析。
在方法內部根據basePackages獲取對應類全限定集合,如果集合為空,就把當前的核心啟動類全限定名的包名即com.lg加入,設置為basePackages(掃描的包范圍),這里就完成了第一步,獲取掃描路徑。
第二步分析
接著再跳到doScan方法,開始把他轉成BeanDefition并注入IOC容器。
在doScan方法中第一個關鍵點findCandidateComponents方法,根據傳入的初始路徑地址掃描該包及其子包所有的class,并封裝成BeanDefinition并存入一個Set集合中,完成第二步。
第三步分析
有了BeanDefinition集合之后,對他進行遍歷,在遍歷的最后調用了一個registerBeanDefinition方法進行注冊BeanDefinition。
在方法內部,執行到他的實現類DefaultListableBeanFactory中的registerBeanDefinition方法,就直接通過put方式把BeanDefinition注冊進了beanDefinitionMap中。
@Import注解指定類解析
解析完主類掃描包之后,接著又開始解析@import注解指定類。
首先參數里面有一個getImports方法,他作用就是根據@import注解來獲取到要導入到容器中的組件類。他從核心啟動類中找到對應的@Import注解。在內部最終要的collectImports方法中,進行遞歸調用一直找到有@import注解的全類名,最后返回所有有@Import注解的組件類。
獲取到注解組件類之后,就需要去執行組件類了,回到ConfigurationClassParser類的parse方法,執行this.deferredImportSelectorHandler.process方法。
接著往下走最后到processGroupImports方法內,里面有非常重要的一步grouping.getImports()。
先通過grouping.getImports()方法里面調用了process方法,加載spring.factories文件配置所有類,一步步過濾,最后封裝成AutoConfigurationEntry對象返回,把這些對象放入Map<String, AnnotationMetadata> entries集合中。
最后通過this.group.selectImports()方法再進行過濾排序,返回要生效的自動裝配對象全路徑集合,最后通過this.reader.loadBeanDefinitions(configClasses)方法使這些自動裝配類全部生效。
Run方法第六步:刷新應用上下文后的擴展接口
afterRefresh方法,他其實就是一個擴展接口,設計模式中的模板方法,默認為空實現。如果有自定義需求,可以重寫該方法。比如打印一些啟動結束log,或者一些其它后置處理。
自定義Starter
Spring Boot中的starter是一種非常重要的機制,能夠拋棄以前繁雜的配置,將其統一集成進starter,應用者只需要在maven中引入starter依賴,Spring Boot就能自動掃描到要加載的信息并啟動相應的默認配置。starter讓我們擺脫了各種依賴庫的處理,需要配置各種信息的困擾。Spring Boot會自動通過classpath路徑下的類發現需要的Bean,并注冊進IOC容器。Spring Boot提
供了針對日常企業應用研發各種場景的spring-boot-starter依賴模塊。所有這些依賴模塊都遵循著約定成俗的默認配置,并允許我們調整這些配置,即遵循“約定大于配置”的理念。簡而言之,starter就是一個外部的項目,我們需要使用它的時候就可以在當前Spring Boot項目中引入它,Spring Boot會自動完成裝配。
使用場景
比如動態數據源、登陸模塊、AOP日志切面等等就可以將這些功能封裝成一個starter,復用的時候只需要在pom.xml引入即可,比如阿里的Druid數據源,就是阿里自己實現了一個第三方starter,因此Spring Boot就可以直接引入并使用這個數據庫。命令規則SpringBoot提供的starter以 spring-boot-starter-xxx 的方式命名的。官方建議自定義的starter使用 xxx-spring-boot-starter命名規則。以區分SpringBoot生態提供的starter,比如阿里的druid-spring-boot-starter。
自定義starter代碼實現:
1. 新建maven jar工程,工程名為zdy-spring-boot-starter,導入依賴
2. 編寫JavaBean
3. 編寫配置類MyAutoConfiguration
4. resources下創建/META-INF/spring.factories
使用自定義starter
1. 對應項目導入自定義starter的依賴
2. 在全局配置文件中配置屬性值
自定義Starter熱插拔技術
@Enablexxx注解就是一種熱拔插技術,加了這個注解就可以啟動對應的starter,當不需要對應的starter的時候只需要把這個注解注釋掉就行。
1. 新增標記類ConfigMarker
2. 新增EnableRegisterServer注解,將@Import引入的組件類生成實例,添加進容器
3. 改造 MyAutoConfiguration 新增條件注解 @ConditionalOnBean(ConfigMarker.class) ,@ConditionalOnBean 這個是條件注解,前面的意思代表只有當期上下文中含有 ConfigMarker對象,被標注的類才會被實例化,才能讓自動配置類生效。
4. 在啟動類上新增上面自定義@EnableRegisterServer注解,根據之前的源碼分析可以知道他在執行過程中會解析這個注解,再去解析里面的@Import注解,從而拿到組件類生成實例化對象存入容器,滿足自動裝配條件。
關于條件注解的講解:
@ConditionalOnBean:僅僅在當前上下文中存在某個對象時,才會實例化一個Bean。
@ConditionalOnClass:某個class位于類路徑上,才會實例化一個Bean。
@ConditionalOnExpression:當表達式為true的時候,才會實例化一個Bean。基于SpEL表達式的條件判斷。
@ConditionalOnMissingBean:僅僅在當前上下文中不存在某個對象時,才會實例化一個Bean。
@ConditionalOnMissingClass:某個class類路徑上不存在的時候,才會實例化一個Bean。
@ConditionalOnNotWebApplication:不是web應用,才會實例化一個Bean。
@ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。
@ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。
@ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。
@ConditionalOnJava:當JVM版本為指定的版本范圍時觸發實例化。
@ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。
@ConditionalOnJndi:在JNDI存在的條件下觸發實例化。
@ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發實例化。
內嵌tomcat原理
Spring Boot默認支持Tomcat,Jetty,和Undertow作為底層容器。而Spring Boot默認使用Tomcat,一旦引入spring-boot-starter-web模塊,就默認使用Tomcat容器。
通過前面的源碼分析我們可以知道核心啟動類在啟動的時候,進入AutoConfigurationImportSelector類中的getAutoConfigurationEntry方法去各個模塊WEB-INF下的spring.factories配置文件中加載相關配置類,獲取到ServletWebServerFactoryAutoConfiguration自動配置類,也就是tomcat自動配置。
通過spring.factories配置文件找到ServletWebServerFactoryAutoConfiguration注解類分析上面的注解。
通過@Import發現他將EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow等嵌入式容器類加載進來了,進入EmbeddedTomcat.class類,通過TomcatServletWebServerFactory類的getWebServer方法,實現了tomcat的實例化,最后調用 getTomcatWebServer方法進入下一步操作。
根據上面方法繼續往下走,到了initialize,就開始啟動了tomcat。
總結:@Import引入的EmbeddedTomcat類里面TomcatServletWebServerFactory工廠類的getWebServer方法一旦被啟動他就會創建并啟動內嵌tomcat。
getWebServer方法被調用點分析
調用的地方其實就是之前分析過的refresh方法中的onRefresh方法
在方法里面進入到ServletWebServerApplicationContext類的createWebServer方法,他會獲取嵌入式的Servlet容器工廠,并通過工廠來獲取Servlet容器,當獲取到了容器工廠之后就通過工廠來調用getWebServer方法,也就是上面那個,來完成內嵌tomcat的創建和啟動。
自動配置Spring MVC源碼分析
SpringBoot項目里面是可以直接使用諸如 @RequestMapping 這類的SpringMVC的注解,在以前的項目中除了引入包之外還需要在web.xml配置一個前端控制器org.springframework.web.servlet.DispatcherServlet。spring Boot通過自動裝配就實現了相同的效果, IOC容器的注入后,最重要的一點就是需要把DispatcherServlet再注冊進servletContext中,也就是servlet容器中。
自動配置DispatcherServlet加載
和前面tomcat分析一樣,我們還是要先去找到DispatcherServlet的自動配置類
接著到META-INF/spring.factories下,找到我們對應的DispatcherServletAutoConfiguration全限定名路徑,進入對應注解類查看源碼。
最下面那個類點進去發現他就是tomcat的那個注解類,也就是先需要tomcat完成自動裝配再來解析DispatcherServletAutoConfiguration類。
在DispatcherServletAutoConfiguration類里面有兩個內部類DispatcherServletConfiguration類和DispatcherServletRegistrationCondition類。
在第一個內部類中創建了DispatcherServlet實例化對象設值存到IOC容器,但是還未添加到servletContext。
在第二個內部類中就判斷存在DispatcherServlet類對象再執行,而這個類對象已經在第一個內部類已經創建存在于上下文中。接著他構建了DispatcherServletRegistrationBean對象,并進行了返回。由此我們得到了兩個對象,第一個前端控制器DispatcherServlet,第二個DispatcherServletRegistrationBean是DispatcherServlet的注冊類,他的作用就是把他注冊進servletContext中。
自動配置DispatcherServlet注冊
通過類圖分析最上面是一個ServletContextInitializer接口。我們可以知道,實現該接口意味著是用來初始化ServletContext的。
進入onStartup方法內部,根據類圖一直往下直到ServletRegistrationBean類中的addRegistration方法,發現他把DispatcherServlet給add進了servletContext完成了注冊。
而這個addRegistration方法的觸發跟上面tomcat的觸發是相同地方,通過getWebServer方法里面的getSelfInitializer方法在方法內部繼續調用selfInitialize方法,通過getServletContextInitializerBeans拿到所有的ServletContextInitializer 集合,而集合中就包含了我們需要的DispatcherServlet控制器,接著遍歷這個集合,使用遍歷結果集調用onStartup方法,就完成了DispatcherServlet的注冊。
三、Spring Boot高級進階
Spring Boot數據源自動配置
application.properties文件中數據源指定的命名規則是因為底層源碼設定了需要這樣才能獲取,注入DataSourceProperties類對象中。
數據源自動配置連接池默認指定Hikari,通過指定TYPE就可以實現更換連接池。
在引入spring-boot-starter-jdbc包后就把HikariCP也一并引入進來了,滿足條件,而其他連接池未引入,不滿足因此默認是HikariCP連接池。
如果多個連接池都滿足的情況下,按照配置的數組順序取值,第一個仍然是HikariCP,所以在不指定的情況下,默認永遠是他。
Mybatis自動配置源碼分析
與前面的tomcat自動配置,DispatcherServlet自動配置是一樣的,都是通過run方法解析EnableAutoConfiguration注解,進入AutoConfigurationImportSelector類,然后去WEB-INF下的spring.factories配置文件中加載相關配置類,完成自動配置類組裝。
1.通過application.properties文件設置mybatis屬性可以注解注入到對應的MybatisProperties類使用。
2…這個類里面第一個方法sqlSessionFactory,通過他實現了創建sqlSessionFactory類、 Configuration類并添加容器,內部實現其實就是先解析配置,封裝Configuration對象,完成準備工作,然后調用MyBatis的初始化流程。
3. 這個類第二個方法sqlSessionTemplate,作用是與mapperProoxy代理類有關。SqlSessionTemplate是線程安全的,可以被多個Dao持有。
4. 得到了SqlSessionFactory了,接下來就是如何掃描到相關的Mapper接口了,通過注解@MapperScan(basePackages = “com.mybatis.mapper”)實現,在內部@Import引入MapperScannerRegistrar類調用registerBeanDefinitions方法進行注冊。
通俗點就是:@MapperScan(basePackages = “com.mybatis.mapper”)這個定義,掃描指定包下的mapper接口,通過動態代理生成了實現類,然后設置每個mapper接口的beanClass屬性為MapperFactoryBean類型并加入到spring的bean容器中。使用者就可以通過@Autowired或者getBean等方式,從spring容器中獲取。
SpringBoot + Mybatis實現動態數據源切換
業務背景
電商訂單項目分正向和逆向兩個部分:其中正向數據庫記錄了訂單的基本信息,包括訂單基本信息、訂單商品信息、優惠卷信息、發票信息、賬期信息、結算信息、訂單備注信息、收貨人信息等;逆向數據庫主要包含了商品的退貨信息和維修信息。數據量超過500萬行就要考慮分庫分表和讀寫分離,那么我們在正向操作和逆向操作的時候,就需要動態的切換到相應的數據庫,進行相關的操作。
解決思路
現在項目的結構設計基本上是基于MVC的,那么數據庫的操作集中在dao層完成,主要業務邏輯在service層處理,controller層處理請求。假設在執行dao層代碼之前能夠將數據源(DataSource)換成我們想要執行操作的數據源,那么這個問題就解決了。
實現原理
Spring內置了一個AbstractRoutingDataSource抽象類,它可以把多個數據源配置成一個Map,然后,根據不同的key返回不同的數據源。因為AbstractRoutingDataSource也是一個DataSource接口,因此,應用程序可以先設置好key, 訪問數據庫的代碼就可以從AbstractRoutingDataSource拿到對應的一個真實的數據源,從而訪問指定的數據庫。
通過內部的getConnection方法獲取數據源,在連接數據庫之前會執行determineCurrentLookupKey()方法,這個方法返回的數據作為key去targetDataSources中查找相應的值,如果查到相對應的DataSource,那么就使用此DataSource獲取數據庫連接。
環境準備工作
實體類:
Mapper類:
Service類
Controller類:
具體實現步驟
多數據源配置:
讀取初始數據源類:
創造一個RoutingDataSourceContext類指定數據源
創建一個繼承AbstractRoutingDataSource類的RoutingDataSource類,重寫determineCurrentLookupKey方法
創建一個primaryDataSource方法,引入兩個數據源bean對象,獲取數據源,設置key存入繼承類RoutingDataSource中。
改造Controller類,在具體業務邏輯執行前,進行數據源綁定確認。
測試結果,分別調用兩個方法,返回了不同數據庫的結果
動態數據源優化
需要讀數據庫的地方,在對應controller類方法里面就需要加上一大段RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);代碼,這里可以通過自定義注解@RoutingWith(“slaveDataSource”),來實現這個效果,取消大量重復代碼編寫。
新增RoutingWith注解類:
借助AOP的動態代理對方法攔截實現橫切邏輯增強,添加Maven依賴
切面類RoutingAspect
自定義注解定義完畢,把controller類改造成注解方式就完成了優化改造。
四、Spring Boot緩存深入
JSR107
關于如何使用緩存的規范,是java提供的一個接口規范,類似于JDBC規范。
五個核心接口:
CachingProvider(緩存提供者):創建、配置、獲取、管理和控制多個CacheManager。
CacheManager(緩存管理器):創建、配置、獲取、管理和控制多個唯一命名的Cache,Cache存在于CacheManager的上下文中。一個CacheManager僅對應一個CachingProvider。
Cache(緩存):是由CacheManager管理的,CacheManager管理Cache的生命周期,Cache存在于CacheManager的上下文中,是一個類似map的數據結構,并臨時存儲以key為索引的值。一個Cache僅被一個CacheManager所擁有
Entry(緩存鍵值對):是一個存儲在Cache中的key-value對。
Expiry(緩存時效):每一個存儲在Cache中的條目都有一個定義的有效期。一旦超過這個時間,條目就自動過期,過期后,條目將不可以訪問、更新和刪除操作。緩存有效期可以通過ExpiryPolicy設置。
一個應用里面可以有多個緩存提供者(CachingProvider),一個緩存提供者可以獲取到多個緩存管理器(CacheManager),一個緩存管理器管理著不同的緩存(Cache),緩存中是一個個的緩存鍵值對(Entry),每個entry都有一個有效期(Expiry)。緩存管理器和緩存之間的關系有點類似于數據庫中連接池和連接的關系。
緩存概念及緩存注解
Spring從3.1開始定義了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口來統一不同的緩存技術;并支持使用JavaCaching(JSR-107)注解簡化我們進行緩存開發。
Spring Cache 只負責維護抽象層,具體的實現由自己的技術選型來決定。將緩存處理和緩存技術解除耦合。
每次調用需要緩存功能的方法時,Spring會檢查指定參數的指定的目標方法是否已經被調用過,如果有就直接從緩存中獲取方法調用后的結果,如果沒有就調用方法并緩存結果后返回給用戶。下次調用直接從緩存中獲取。
兩個接口:
Cache:緩存抽象的規范接口,緩存實現有:RedisCache、EhCache、ConcurrentMapCache等。
CacheManager:緩存管理器,管理Cache的生命周期。
1.@Cacheable標注在方法上,表示該方法的結果需要被緩存起來,緩存的鍵由keyGenerator的策略決定,緩存的值的形式則由serialize序列化策略決定(序列化還是json格式);標注上該注解之后,在緩存時效內再次調用該方法時將不會調用方法本身而是直接從緩存獲取結果。
2.@CachePut也標注在方法上,和@Cacheable相似也會將方法的返回值緩存起來,不同的是標注@CachePut的方法每次都會被調用,而且每次都會將結果緩存起來,適用于對象的更新。
緩存注解@Cacheable實現
1. 在核心啟動類開啟基于注解的緩存功能:主啟動類標注@EnableCaching
2. 標注緩存相關注解:@Cacheable、CacheEvict、CachePut
@Cacheable 開啟緩存查詢 會將查詢出來的值存到緩存中
* value/cacheNames 指定緩存的名稱,cacheManager是管理多個cache,以名稱進行區分
* key:緩存數據時指定key值,(key,value),value就是查詢的結果,key默認方法的參數值,也可以去使用spEl去計key值
* keyGenerator:key的生成策略,和key二選一,通過他可以自定義keyGenerator
* cacheManager:指定緩存管理器,比如redis和ehcache
* cacheResolver:功能和cacheManager相同 二選一
* condition:條件屬性,必須要滿足這個條件才會進行緩存
*unless: 否定條件,滿足這個條件不進行緩存
*sync : 是否使用異步模式進行緩存
注意:既滿足condition又滿足unless條件的也不進行緩存;使用異步模式進行緩存時(sync=true):unless條件將不被支持
可用的SpEL表達式:
自動配置緩存實現源碼分析
Spring Boot中所有的自動配置都是基于AutoConfigurationImportSelector類,和前面的自動配置一樣,都是在getAutoConfigurationEntry方法中在WEB-INF下的spring.factories配置文件中加載相關配置類,找到org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,這就是對應的緩存配置類。
進入CacheAutoConfiguration緩存配置類。
進入引入的組件類CacheConfigurationImportSelector,發現他實現了ImportSelector接口重寫了selectImports方法,返回了所有的緩存組件類數組。這些加載順序是自上而下的。
打開RedisCacheConfiguration組件類,通過上面的注解條件分析,在沒有引入redis的情況下,這些組件類是不會生效的。
而SimpleCacheConfiguration類里面的注解條件滿足,因此默認的緩存組件類就是SimpleCacheConfiguration。
分析源碼得到通過cacheManager方法,會創建ConcurrentMapCacheManager對象并返回,在這個對象類里面實現了CacheManager接口,在里面有一個getCache方法,通過雙重校驗鎖機制,先從緩存中取緩存對象,如果有直接返回,如果沒得,就創建,并且添加到cacheMap,也就是緩存池中。而我們的這個cacheMap他的底層結構就是通過createConcurrentMapCache方法創建并返回的Cache,里面定義了Cache的屬性以及操作Cache的方法,比如lookup、put方法,在執行查詢的時候,他會先調用lookup方法查詢是否有緩存結果,如果沒有查詢數據庫,就把結果拿到再調用put方法存入緩存。
@CachePut&@CacheEvict&@CacheConfig
@CachePut
既調用方法,又更新緩存數據,一般用于更新操作,在更新緩存時一定要和想更新的緩存有相同的緩存名稱和相同的key(可類比同一張表的同一條數據)。
1.先調用目標方法
2.將目標方法的結果緩存起來
@CacheEvict
緩存清除,清除緩存時要指明緩存的名字和key,相當于告訴數據庫要刪除哪個表中的哪條數據,key默認為參數的值
value/cacheNames:緩存的名字
key:緩存的鍵
allEntries:是否清除指定緩存中的所有鍵值對,默認為false,設置為true時會清除緩存中的所有鍵值對,與key屬性二選一使用,就是是否清除所有。
beforeInvocation:在@CacheEvict注解的方法調用之前清除指定緩存,默認為false,即在方法調用之后清除緩存,設置為true時則會在方法調用之前清除緩存(在方法調用之前還是之后清除緩存的區別在于方法調用時是否會出現異常,若不出現異常,這兩種設置沒有區別,若出現異常,設置為在方法調用之后清除緩存將不起作用,因為方法調用失敗了)。
@CacheConfig
標注在類上,抽取緩存相關注解的公共配置,可抽取的公共配置有緩存名字、主鍵生成器等(如注解中的屬性所示)
就是把下面方法上的注解抽取上來統一配置,避免繁瑣的重復配置代碼。
基于Redis的緩存實現
SpringBoot默認開啟的緩存管理器是ConcurrentMapCacheManager,創建緩存組件是ConcurrentMapCache,將緩存數據保存在一個個的ConcurrentHashMap<Object, Object>中。開發時我們可以使用緩存中間件:redis、memcache、ehcache等,這些緩存中間件的啟用很簡單——只要向容器中加入相關的bean就會啟用,可以啟用多個緩存中間件。
1. 引入Redis的starter
引入相關Bean之后,Spring Boot的自動裝配就會在初始化時,找到Redis自定義的RedisAutoConfiguration進行裝配,里面有兩個返回類型,RedisTemplate和StringRedisTemplate(用來操作字符串:key和value都是字符串),template中封裝了操作各種數據類型的操作(stringRredisTemplate.opsForValue()、stringRredisTemplate.opsForList()等)。
2. 配置redis:只需要配置redis的主機地址spring.redis.host=127.0.0.1,
注意:如何要使用Redis緩存數據,對應操作的實體類一定要實現Serializable接口。
在前面自定義配置緩存上說了緩存的九大組件,并且自上而下加載順序,當Redis被引入之后,他的條件先被滿足完成自動裝配并創建了CacheManager類對象,那么下面的SimpleCacheConfiguration類,因為CacheManager.class已經存在,就不會生效。
自定義RedisCacheManager
SpringBoot默認采用的是JDK的對象序列化方式,我們可以切換為使用JSON格式進行對象的序列化操作,這時需要我們自定義序列化規則。
RedisConfig配置類中使用@Bean注解注入了一個默認名稱為方法名的cacheManager組件。在定義的Bean組件中,通過RedisCacheConfiguration對緩存數據的key和value分別進行了序列化方式的定制,其中緩存數據的key定制為StringRedisSerializer(即String格式),而value定制為了Jackson2JsonRedisSerializer(即JSON格式),同時還使用entryTtl(Duration.ofDays(1))方法將緩存數據有效期設置為1天完成基于注解的Redis緩存管理器RedisCacheManager定制后,可以對該緩存管理器的效果進行測試(使用自定義序列化機制的RedisCacheManager測試時,實體類可以不用實現序列化接口)
五、Spring Boot部署與監控
JAR包部署
1. 項目打包
2. 項目啟動
WAR包部署
1. 修改pom.xml配置
2. 添加依賴
3. 排除Spring Boot內置Tomcat
4. 改造啟動類
5. 打包交給外置Tomcat運行
JAR包和WAR包部署差異:
1.jar更加簡單方便,使用 java -jar xx.jar 就可以啟動。所以打成 jar 包的最多。而 war包可以部署到tomcat的 webapps 中,隨Tomcat的啟動而啟動。具體使用哪種方式,應視應用場景而定。
2、打jar包時不會把src/main/webapp 下的內容打到jar包里 (你認為的打到jar包里面,路徑是不行的會報404)打war包時會把src/main/webapp 下的內容打到war包里。
3.打成什么文件包進行部署與項目業務有關,就像提供 rest 服務的項目需要打包成 jar文件,用命令運行很方便。。。而有大量css、js、html,且需要經常改動的項目,打成 war 包去運行比較方便,因為改動靜態資源可以直接覆蓋,很快看到改動后的效果,這是 jar 包不能比的。
多環境部署
線上環境prod(product)、開發環境dev(development)、測試環境test、提測環境qa、單元測試unitest等等多種環境進行不同配置。
1. 多環境配置文件
2. 指定要加載的配置文件
監控插件-Acturator
Spring boot作為微服務框架,除了它強大的快速開發功能外,還有就是它提供了actuator模塊,引入該模塊能夠自動為Spring boot應用提供一系列用于監控的端點。Spring Boot Actuator提供了對單個Spring Boot的監控,信息包含:應用狀態、內存、線程、堆棧等等,比較全面的監控了Spring Boot應用的整個生命周期。
Actuator 的 REST 接口
Actuator 監控分成兩類:原生端點和用戶自定義端點;自定義端點主要是指擴展性,用戶可以根據自己的實際應用,定義一些比較關心的指標,在運行期進行監控。
原生端點是在應用程序里提供眾多 Web 接口,通過它們了解應用程序運行時的內部狀況。
原生端點又可以分成三類:
應用配置類:可以查看應用在運行期的靜態信息:例如自動配置信息、加載的 springbean信息、yml 文件配置信息、環境信息、請求映射信息;
度量指標類:主要是運行期的動態信息,例如堆棧、請求鏈、一些健康指標、metrics 信息等;
操作控制類:主要是指 shutdown,用戶可以發送一個請求將應用的監控功能關閉。
原生端點十三個接口:
引入依賴
注意:保證actuator 暴露的監控接口的安全性,需要添加安全控制的依賴 spring-boot-startsecurity 依賴,訪問應用監控端點時,都需要輸入驗證信息。
屬性詳解
在 Spring Boot 2.x 中為了安全期間,Actuator 只開放了兩個端點 /actuator/health 和/actuator/info。
開啟所有的監控點,也可部分:
Health:
health 主要用來檢查應用的運行狀態,這是我們使用最高頻的一個監控點。通常使用此接口提醒我們應用實例的運行狀態,以及應用不”健康“的原因,比如數據庫連接、磁盤空間不夠等,UP表示程序健康運行。
Info:
info 就是我們自己配置在配置文件中以 info 開頭的配置信息。
Beans:
展示了 bean 的別名、類型、是否單例、類的地址、依賴等信息。
Conditions:
Spring Boot 的自動配置功能非常便利,但有時候也意味著出問題比較難找出具體的原因。使用conditions 可以在應用運行時查看代碼了某個配置在什么條件下生效,或者某個自動配置為什么沒有生效。
Heapdump:
返回一個 GZip 壓縮的 JVM 堆 dump,我們可以使用 JDK 自帶的 Jvm 監控工具 VisualVM 打開此文件查看內存快照。
Mappings:
描述全部的 URI 路徑,以及它們和控制器的映射關系。
Threaddump:
生成當前線程活動的快照。這個功能非常好,方便我們在日常定位問題的時候查看線程的情況。 主要展示了線程名、線程ID、線程的狀態、是否等待鎖資源等信息。
Shutdown:
開啟接口優雅關閉 Spring Boot 應用,要使用這個功能首先需要在配置文件中開啟。
management.endpoint.shutdown.enabled=true
監控插件-Spring Boot Admin
Spring Boot Admin 是一個針對spring-boot的actuator接口進行UI美化封裝的監控工具。他可以返回在列表中瀏覽所有被監控spring-boot項目的基本信息比如:Spring容器管理的所有的bean、詳細的Health信息、內存信息、JVM信息、垃圾回收信息、各種配置信息(比如數據源、緩存列表和命中率)等,Threads 線程管理,Environment 管理等。
1. 搭建Server端
2. 搭建client端
3. 通過地址http://localhost:8080/applications測試結果