生命過短暫,不要去作一些根本沒有人想要的東西。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以避免費學習。關注公衆號【BAT的烏托邦】逐個擊破,深刻掌握,拒絕淺嘗輒止。java
各位小夥伴你們好,我是A哥。上篇文章瞭解了static關鍵字 + @Bean方法的使用,知曉了它可以提高Bean的優先級,在@Bean方法前標註static關鍵字,特定狀況下能夠避免一些煩人的「警告」日誌的輸出,排除隱患讓工程變得更加安全。咱們知道static關鍵字它不只可以使用在方法上,那麼本文將繼續挖掘static在Spring環境下的用處。程序員
根據所學的JavaSE基礎,static關鍵字除了可以修飾方法外,還能使用在這兩個地方:web
其實static還能夠修飾代碼塊、static靜態導包等,但很明顯,這些與本文無關spring
接下來就以這爲兩條主線,分別研究static在對應場景下的做用,本文將聚焦在靜態內部類上。
編程
本文內容若沒作特殊說明,均基於如下版本:windows
1.8
5.2.2.RELEASE
說到Java裏的static關鍵字,這當屬最基礎的入門知識,是Java中經常使用的關鍵字之一。你平時用它來修飾變量和方法了,可是對它的瞭解,即便放在JavaSE情景下知道這些仍是不夠的,問題雖小但這每每反映了你對Java基礎的瞭解程度。tomcat
固然嘍,本文並不討論它在JavaSE下使用,畢竟我們仍是有必定逼格的專欄,須要進階一把,玩玩它在Spring環境下到底可以迸出怎麼樣的火花呢?好比靜態內部類~安全
static修飾類只有一種狀況:那就是這個類屬於內部類,這就是咱們津津樂道的靜態內部類,形如這樣:ide
public class Outer { private String name; private static Integer age; // 靜態內部類 private static class Inner { private String innerName; private static Integer innerAge; public void fun1() { // 沒法訪問外部類的成員變量 //System.out.println(name); System.out.println(age); System.out.println(innerName); System.out.println(innerAge); } } public static void main(String[] args) { // 靜態內部類的實例化並不須要依賴於外部類的實例 Inner inner = new Inner(); } }
在實際開發中,靜態內部類的使用場景是很是之多的。源碼分析
因爲一些小夥伴對普通內部類 vs 靜態內部類傻傻分不清,爲了方便後續講解,本處把關鍵要素作簡要對比說明:
總之,普通內部類和外部類的關係屬於強綁定,而靜態內部類幾乎不會受到外部類的限制,能夠遊離單獨使用。既然如此,那爲什麼還須要static靜態內部類呢,直接單獨寫個Class類豈不就行了嗎?存在即合理,這麼使用的緣由我我的以爲有以下兩方面思考,供以你參考:
在傳統Spirng Framework
的配置類場景下,你可能鮮有接觸到static關鍵字使用在類上的場景,但這在Spring Boot下使用很是頻繁,好比屬性配置類的典型應用:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { // server.port = xxx // server.address = xxx private Integer port; private InetAddress address; ... // tomcat配置 public static class Tomcat { // server.tomcat.protocol-header = xxx private String protocolHeader; ... // tomcat內的log配置 public static class Accesslog { // server.tomcat.accesslog.enabled = xxx private boolean enabled = false; ... } } }
這種嵌套case使得代碼(配置)的key 內聚性很是強,使用起來更加方便。試想一下,若是你不使用靜態內部類去集中管理這些配置,每一個配置都單獨書寫的話,像這樣:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties { } @ConfigurationProperties(prefix = "server.tomcat", ignoreUnknownFields = true) public class TomcatProperties { } @ConfigurationProperties(prefix = "server.tomcat.accesslog", ignoreUnknownFields = true) public class AccesslogProperties { }
這代碼,就問你,若是是你同事寫的,你罵不罵吧!用臃腫來形容仍是個中意詞,層次結構體現得也很是的不直觀嘛。所以,對於這種屬性類裏使用靜態內部類是很是適合,內聚性一會兒高不少~
除了在內聚性上的做用,在Spring Boot中的@Configuration
配置類下(特別常見於自動配置類)也能常常看到它的身影:
@Configuration(proxyBeanMethods = false) public class WebMvcAutoConfiguration { // web MVC個性化定製配置 @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { ... } @Configuration(proxyBeanMethods = false) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { ... } }
利用靜態內部類把類似配置類歸併在一個 .java文件 內,這樣多個static類還可公用外部類的屬性、方法,也是一種高內聚的體現。同時static關鍵字提高了初始化的優先級,好比本例中的EnableWebMvcConfiguration
它會優先於外部類加載~
關於static靜態內部類優先級相關是重點,靜態內部類的優先級會更高嗎?使用普通內部能達到一樣效果嗎?拍腦殼直接回答是沒用的,帶着這兩個問題,接下來A哥舉例領你一探究竟...
本身先構造一個Demo,場景以下:
@Configuration class OuterConfig { OuterConfig() { System.out.println("OuterConfig init..."); } @Bean static Parent parent() { return new Parent(); } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("InnerConfig init..."); } @Bean Daughter daughter() { return new Daughter(); } } }
測試程序:
@ComponentScan public class TestSpring { public static void main(String[] args) { new AnnotationConfigApplicationContext(TestSpring.class); } }
啓動程序,結果輸出:
InnerConfig init... OuterConfig init... Daughter init... Parent init...
結果細節:彷佛都是按照字母表的順序來執行的。I在前O在後;D在前P在後;
看到這個結果,若是你就過早的得出結論:靜態內部類優先級高於外部類,那麼就太隨意了,圖樣圖森破啊。大膽猜測,當心求證 應該是程序員應有的態度,那麼繼續往下看,在此基礎上我新增長一個靜態內部類:
@Configuration class OuterConfig { OuterConfig() { System.out.println("OuterConfig init..."); } @Bean static Parent parent() { return new Parent(); } @Configuration private static class PInnerConfig { PInnerConfig() { System.out.println("PInnerConfig init..."); } @Bean Son son() { return new Son(); } } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("InnerConfig init..."); } @Bean Daughter daughter() { return new Daughter(); } } }
我先解釋下我這麼作的意圖:
運行程序,結果輸出:
InnerConfig init... PInnerConfig init... OuterConfig init... Daughter init... son init... Parent init...
結果細節:外部類貌似老是滯後於內部類初始化;同一類的多個內部類之間順序是按照字母表順序(天然排序)初始化而非字節碼順序;@Bean方法的順序依照了類的順序
請留意本結果和上面結果是否有區別,你應該如有所思。
這是單.java文件的case(全部static類都在同一個.java文件內),接下來我在同目錄下增長 2個.java文件(請自行留意類名第一個字母,我將再也不贅述個人設計意圖):
// 文件一: @Configuration class A_OuterConfig { A_OuterConfig() { System.out.println("A_OuterConfig init..."); } @Bean String a_o_bean(){ System.out.println("A_OuterConfig a_o_bean init..."); return new String(); } @Configuration private static class PInnerConfig { PInnerConfig() { System.out.println("A_OuterConfig PInnerConfig init..."); } @Bean String a_p_bean(){ System.out.println("A_OuterConfig a_p_bean init..."); return new String(); } } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("A_OuterConfig InnerConfig init..."); } @Bean String a_i_bean(){ System.out.println("A_OuterConfig a_i_bean init..."); return new String(); } } } // 文件二: @Configuration class Z_OuterConfig { Z_OuterConfig() { System.out.println("Z_OuterConfig init..."); } @Bean String z_o_bean(){ System.out.println("Z_OuterConfig z_o_bean init..."); return new String(); } @Configuration private static class PInnerConfig { PInnerConfig() { System.out.println("Z_OuterConfig PInnerConfig init..."); } @Bean String z_p_bean(){ System.out.println("Z_OuterConfig z_p_bean init..."); return new String(); } } @Configuration private static class InnerConfig { InnerConfig() { System.out.println("Z_OuterConfig InnerConfig init..."); } @Bean String z_i_bean(){ System.out.println("Z_OuterConfig z_i_bean init..."); return new String(); } } }
運行程序,結果輸出:
A_OuterConfig InnerConfig init... A_OuterConfig PInnerConfig init... A_OuterConfig init... InnerConfig init... PInnerConfig init... OuterConfig init... Z_OuterConfig InnerConfig init... Z_OuterConfig PInnerConfig init... Z_OuterConfig init... A_OuterConfig a_i_bean init... A_OuterConfig a_p_bean init... A_OuterConfig a_o_bean init... Daughter init... son init... Parent init... Z_OuterConfig z_i_bean init... Z_OuterConfig z_p_bean init... Z_OuterConfig z_o_bean init...
這個結果大而全,是有說服力的,經過這幾個示例能夠總結出以下結論:
總的來講,當static標註在class類上時,在同.java文件內它是可以提高優先級的,這對於Spring Boot
的自動配置很是有意義,主要體如今以下兩個方法:
@Conditional
條件判斷了。這裏我舉個官方的例子,你便能感覺到它的魅力所在:@Configuration public class FeignClientsConfiguration { ... @Bean @Scope("prototype") @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } @Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled") public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } } }
由於HystrixFeign.builder()
它屬於靜態內部類,因此這個@Bean確定是優先於外部的Feign.builder()
先加載的。因此這段邏輯可解釋爲:優先使用HystrixFeign.builder()
(若條件知足),不然使用Feign.builder().retryer(retryer)
做爲兜底。經過此例你應該再一次感覺到Bean的加載順序之於Spring應用的重要性,特別在Spring Boot/Cloud下此特性尤其凸顯。
你覺得記住這幾個結論就完事了?不,這明顯不符合A哥的逼格嘛,下面咱們就來繼續挖一挖吧。
關於@Configuration
配置類的順序問題,事前需強調兩點:
@Configuration
文件以前的初始化順序是不肯定的@Configuration
配置類(好比靜態內部類、普通內部類等),它們之間的順序是咱們須要關心的,而且須要強依賴於這個順序編程(好比Spring Boot)@Configuration
配置類只有是被@ComponentScan
掃描進來(或者被Spring Boot自動配置加載進來)才須要討論順序(假若是構建上下文時本身手動指好的,那順序就已經定死了嘛),實際開發中的配置類也確實是醬紫的,通常都是經過掃描被加載。接下來咱們看看@ComponentScan
是如何掃描的,把此註解的解析步驟(僞代碼)展現以下:
說明:本文並不會着重分析@ComponentScan它的解析原理,只關注本文「感興趣」部分
一、解析配置類上的@ComponentScan
註解(們):本例中TestSpring
做爲掃描入口,會掃描到A_OuterConfig/OuterConfig等配置類們
ConfigurationClassParser#doProcessConfigurationClass: // **最早判斷** 該配置類是否有成員類(普通內部類) // 若存在普通內部類,最早把普通內部類給解析嘍(注意,不是靜態內部類) if (configClass.getMetadata().isAnnotated(Component.class.getName())) { processMemberClasses(configClass, sourceClass); } ... // 遍歷該配置類上全部的@ComponentScan註解 // 使用ComponentScanAnnotationParser一個個解析 for (AnnotationAttributes componentScan : componentScans) { Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan,...); // 繼續判斷掃描到的bd是不是配置類,遞歸調用 ... }
細節說明:關於最早解析內部類時須要特別注意,Spring經過
sourceClass.getMemberClasses()
來獲取內部類們:只有普通內部類屬於這個,static靜態內部類並不屬於它,這點很重要哦
二、解析該註解上的basePackages/basePackageClasses等屬性值獲得一些掃描的基包,委託給ClassPathBeanDefinitionScanner去完成掃描
ComponentScanAnnotationParser#parse // 使用ClassPathBeanDefinitionScanner掃描,基於類路徑哦 scanner.doScan(StringUtils.toStringArray(basePackages));
三、遍歷每一個基包,從文件系統中定位到資源,把符合條件的Spring組件(強調:這裏只指外部@Configuration配置類,還沒涉及到裏面的@Bean這些)註冊到BeanDefinitionRegistry註冊中心
ComponentScanAnnotationParser#doScan for (String basePackage : basePackages) { // 這個方法是本文最須要關注的方法 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ... // 把該配置**類**(並不是@Bean方法)註冊到註冊中心 registerBeanDefinition(definitionHolder, this.registry); } }
到這一步就完成了Bean定義的註冊,此處能夠驗證一個結論:多個配置類之間,誰先被掃描到,就先註冊誰,對應的就是誰最早被初始化。那麼這個順序究竟是咋樣界定的呢?那麼就要來到這中間最爲重要(本文最關心)的一步嘍:findCandidateComponents(basePackage)
。
說明:Spring 5.0開始增長了
@Indexed
註解爲雲原生作了準備,可讓scan掃描動做在編譯期就完成,但這項技術還不成熟,暫時幾乎無人使用,所以本文仍舊只關注經典模式的實現
ClassPathScanningCandidateComponentProvider#scanCandidateComponents // 最終返回的候選組件們 Set<BeanDefinition> candidates = new LinkedHashSet<>(); // 獲得文件系統的路徑,好比本例爲classpath*:com/yourbatman/**/*.class String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; // 從文件系統去加載Resource資源文件進來 // 這裏Resource表明的是一個本地資源:存在你硬盤上的.class文件 Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); for (Resource resource : resources) { if (isCandidateComponent(metadataReader)) { if (isCandidateComponent(sbd)) { candidates.add(sbd); } } }
這段代碼的信息量是很大的,分解爲以下兩大步:
注意:不是看.java源代碼順序,也不是看你
target
目錄下的文件順序(該目錄是通過了IDEA反編譯的結果,沒法反應真實順序),而是編譯後看你的磁盤上的.class文件的文件順序
@Conditional
條件isIndependent()
是獨立類(top-level類 or 靜態內部類屬於獨立類) 而且 isConcrete()是具體的(非接口非抽象類)isAbstract()
是抽象類 而且 類內存在標註有@Lookup
註解的方法基於以上例子,磁盤中的.class文件狀況以下:
看着這個順序,再結合上面的打印結果,是否是感受獲得瞭解釋呢?既然@Configuration類(外部類和內部類)的順序肯定了,那麼@Bean就跟着定了嘍,由於畢竟配置類也得遍歷一個一個去執行嘛(有依賴關係的case除外)。
特別說明:理論上不一樣的操做系統(如windows和Linux)它們的文件系統是有差別的,對文件存放的順序是可能不一樣的(好比$xxx內部類可能放在後面),但現實情況它們是同樣的,所以各位同窗對此無需擔憂跨平臺問題哈,這由JVM底層來給你保證。
什麼,關於此解析步驟你想要張流程圖?好吧,你知道的,這個A哥會放到本專欄的總結篇裏統一供以你白嫖,關注我公衆號吧~
看到這個截圖你就懂了:在不一樣.java文件內,靜態內部類是不用擔憂重名問題的,這不也就是內聚性的一種體現麼。
說明:beanName的生成其實和你註冊Bean的方式有關,好比@Import、Scan方式是不同的,這裏就不展開討論了,知道有這個差別就成。
咱們知道,從內聚性上來講,普通內部相似乎也能夠達到目的。可是相較於靜態內部類在Spring容器內對優先級的問題,它的表現可就沒這麼好嘍。基於以上例子,把全部的static關鍵字去掉,就是本處須要的case。
reRun測試程序,結果輸出:
A_OuterConfig init... OuterConfig init... Z_OuterConfig init... A_OuterConfig InnerConfig init... A_OuterConfig a_i_bean init... A_OuterConfig PInnerConfig init... A_OuterConfig a_p_bean init... A_OuterConfig a_o_bean init... InnerConfig init... Daughter init... PInnerConfig init... son init... Parent init... Z_OuterConfig InnerConfig init... Z_OuterConfig z_i_bean init... Z_OuterConfig PInnerConfig init... Z_OuterConfig z_p_bean init... Z_OuterConfig z_o_bean init...
對於這個結果A哥不用再作詳盡分析了,看似比較複雜其實有了上面的分析仍是比較容易理解的。主要有以下兩點須要注意:
isIndependent() = false
),因此它並不能像靜態內部類那樣預先就被掃描進去,如圖結果展現:processMemberClasses()
時被解析請思考:爲什麼使用普通內部類獲得的是這個結果呢?建議copy個人demo,自行走一遍流程,多動手老是好的
本文一如既往的很乾哈。寫本文的原動力是由於真的太多小夥伴在看Spring Boot自動配置類的時候,沒法理解爲毛它有些@Bean配置要單獨寫在一個static靜態類裏面,感受挺費事;方法前直接價格static不香嗎?經過這篇文章 + 上篇文章的解讀,相信A哥已經給了你答案了。
static關鍵字在Spring中使用的這個專欄,下篇將進入到多是你更關心的一個話題:爲毛static字段不能使用@Autowired注入的分析,下篇見~