生命太短暫,不要去做一些根本沒有人想要的東西。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
前言
各位小夥伴大家好,我是A哥。上篇文章瞭解了static關鍵字 + @Bean方法的使用,知曉了它能夠提升Bean的優先順序,在@Bean方法前標註static關鍵字,特定情況下可以避免一些煩人的“警告”日誌的輸出,排除隱患讓工程變得更加安全。我們知道static關鍵字它不僅可使用在方法上,那麼本文將繼續挖掘static在Spring環境下的用處。
根據所學的JavaSE基礎,static關鍵字除了能夠修飾方法外,還能使用在這兩個地方:
- 修飾類。確切的說,應該叫修飾內部類,所以它叫靜態內部類
- 修飾成員變數
其實static還可以修飾程式碼塊、static靜態導包等,但很明顯,這些與本文無關
接下來就以這為兩條主線,分別研究static在對應場景下的作用,本文將聚焦在靜態內部類上。
版本約定
本文內容若沒做特殊說明,均基於以下版本:
- JDK:
1.8
- Spring Framework:
5.2.2.RELEASE
正文
說到Java裡的static關鍵字,這當屬最基礎的入門知識,是Java中常用的關鍵字之一。你平時用它來修飾變數和方法了,但是對它的瞭解,即使放在JavaSE情景下知道這些還是不夠的,問題雖小但這往往反映了你對Java基礎的瞭解程度。
當然嘍,本文並不討論它在JavaSE下使用,畢竟我們們還是有一定逼格的專欄,需要進階一把,玩玩它在Spring環境下到底能夠迸出怎麼樣的火花呢?比如靜態內部類~
Spring下的靜態內部類
static修飾類只有一種情況:那就是這個類屬於內部類,這就是我們津津樂道的靜態內部類,形如這樣:
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 靜態內部類傻傻分不清,為了方便後續講解,本處把關鍵要素做簡要對比說明:
- 靜態內部類可以宣告靜態or例項成員(屬性和方法);而普通內部類則不可以宣告靜態成員(屬性和方法)
- 靜態內部類例項的建立不依賴於外部類;而普通外部類例項建立必須先有外部類例項才行(繫結關係拿捏得死死的,不信你問鄭凱)
- 靜態內部類不能訪問外部類的例項成員;而普通內部類可以隨意訪問(不管靜態or非靜態) --> 我理解這是普通內部類能 “存活” 下來的最大理由了吧?
總之,普通內部類和外部類的關係屬於強繫結,而靜態內部類幾乎不會受到外部類的限制,可以遊離單獨使用。既然如此,那為何還需要static靜態內部類呢,直接單獨寫個Class類豈不就好了嗎?存在即合理,這麼使用的原因我個人覺得有如下兩方面思考,供以你參考:
- 靜態內部類是弱關係並不是沒關係,比如它還是可以訪問外部類的static的變數的不是(即便它是private的)
- 高內聚的體現
在傳統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哥舉例領你一探究竟...
static靜態配置類提升配置優先順序
自己先構造一個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();
}
}
}
我先解釋下我這麼做的意圖:
- 增加一個字母P開頭的內部類,自然順序P在O(外部類)後面,消除影響
- P開頭的內部類在原始碼擺放順序上故意放在了I開頭的內部類的上面,同樣為了消除字母表順序帶來的影響
- 目的:看看是按照位元組碼順序,還是字母表順序呢?
- PInnerConfig裡面的@Bean例項為Son,字母表順序是三者中最為靠後的,但位元組碼卻在中間,這樣也能夠消除影響
執行程式,結果輸出:
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...
這個結果大而全,是有說服力的,通過這幾個示例可以總結出如下結論:
- 垮.java檔案 (垮配置類)之間的順序,是由自然順序來保證的(字母表順序)
- 如上:下載入A打頭的配置類(含靜態內部類),再是O打頭的,再是Z打頭的
- 同一.java檔案內部,static靜態內部類優先於外部類初始化。若有多個靜態內部類,那麼按照類名自然排序初始化(並非按照定義順序哦,請務必注意)
- 說明:一般內部類只可能與外部類“發生關係”,與兄弟之間不建議有任何聯絡,否則順序控制上你就得當心了。畢竟靠自然順序去保證是一種弱保證,容錯性太低
- 同一.java檔案內,不同類內的@Bean方法之間的執行順序,保持同2一致(也就說你的@Bean所在的@Configuration配置類先載入,那你就優先被初始化嘍)
- 同一Class內多個@Bean方法的執行順序,上篇文章static關鍵字真能提高Bean的優先順序嗎?答:真能 就已經說過了哈,請移步參見
總的來說,當static標註在class類上時,在同.java檔案內它是能夠提升優先順序的,這對於Spring Boot
的自動配置非常有意義,主要體現在如下兩個方法:
- static靜態內部類配置優先於外部類載入,從而靜態內部類裡面的@Bean也優先於外部類的@Bean先載入
- 既然這樣,那麼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
配置類的順序問題,事前需強調兩點:
- 不同 .java檔案 之間的載入順序是不重要的,Spring官方也強烈建議使用者不要去依賴這種順序
- 因為無狀態性,因此你在使用過程中可以認為垮
@Configuration
檔案之前的初始化順序是不確定的
- 因為無狀態性,因此你在使用過程中可以認為垮
- 同一.javaw檔案內也可能存在多個
@Configuration
配置類(比如靜態內部類、普通內部類等),它們之間的順序是我們需要關心的,並且需要強依賴於這個順序程式設計(比如Spring Boot)
@Configuration
配置類只有是被@ComponentScan
掃描進來(或者被Spring Boot自動配置載入進來)才需要討論順序(倘若是構建上下文時自己手動指好的,那順序就已經定死了嘛),實際開發中的配置類也確實是醬紫的,一般都是通過掃描被載入。接下來我們看看@ComponentScan
是如何掃描的,把此註解的解析步驟(虛擬碼)展示如下:
說明:本文並不會著重分析@ComponentScan它的解析原理,只關注本文“感興趣”部分
1、解析配置類上的@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靜態內部類並不屬於它,這點很重要哦
2、解析該註解上的basePackages/basePackageClasses等屬性值得到一些掃描的基包,委託給ClassPathBeanDefinitionScanner去完成掃描
ComponentScanAnnotationParser#parse
// 使用ClassPathBeanDefinitionScanner掃描,基於類路徑哦
scanner.doScan(StringUtils.toStringArray(basePackages));
3、遍歷每個基包,從檔案系統中定位到資源,把符合條件的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);
}
}
}
這段程式碼的資訊量是很大的,分解為如下兩大步:
- 通過ResourcePatternResolver從磁碟裡載入到所有的 .class資源Resource[]。這裡面順序資訊就出現了,載入磁碟Resource資源的過程很複雜,總而言之它依賴於你os檔案系統。所以關於資源的順序可簡單理解為:你磁碟檔案裡是啥順序它就按啥順序載入進來
注意:不是看.java原始碼順序,也不是看你
target
目錄下的檔案順序(該目錄是經過了IDEA反編譯的結果,無法反應真實順序),而是編譯後看你的磁碟上的.class檔案的檔案順序
- 遍歷每一個Resource資源,並不是每個資源都會成為candidates候選,它有個雙重過濾(對應兩個isCandidateComponent()方法):
- 過濾一:使用TypeFilter執行過濾,看看是否被排除;再看看是否滿足
@Conditional
條件 - 過濾二:它有兩種case能滿足條件(任意滿足一個case即可)
isIndependent()
是獨立類(top-level類 or 靜態內部類屬於獨立類) 並且 isConcrete()是具體的(非介面非抽象類)isAbstract()
是抽象類 並且 類記憶體在標註有@Lookup
註解的方法
- 過濾一:使用TypeFilter執行過濾,看看是否被排除;再看看是否滿足
基於以上例子,磁碟中的.class檔案情況如下:
看著這個順序,再結合上面的列印結果,是不是感覺得到了解釋呢?既然@Configuration類(外部類和內部類)的順序確定了,那麼@Bean就跟著定了嘍,因為畢竟配置類也得遍歷一個一個去執行嘛(有依賴關係的case除外)。
特別說明:理論上不同的作業系統(如windows和Linux)它們的檔案系統是有差異的,對檔案存放的順序是可能不同的(比如$xxx內部類可能放在後面),但現實狀況它們是一樣的,因此各位同學對此無需擔心跨平臺問題哈,這由JVM底層來給你保證。
什麼,關於此解析步驟你想要張流程圖?好吧,你知道的,這個A哥會放到本專欄的總結篇裡統一供以你白嫖,關注我公眾號吧~
靜態內部類在容器內的beanName是什麼?
看到這個截圖你就懂了:在不同.java檔案內,靜態內部類是不用擔心重名問題的,這不也就是內聚性的一種體現麼。
說明:beanName的生成其實和你註冊Bean的方式有關,比如@Import、Scan方式是不一樣的,這裡就不展開討論了,知道有這個差異就成。
進階:Spring下普通內部類表現如何?
我們知道,從內聚性上來說,普通內部類似乎也可以達到目的。但是相較於靜態內部類在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
),所以它並不能像靜態內部類那樣預先就被掃描進去,如圖結果展示:
- 普通內部類初始化之前,一定得先初始化外部類,所以類本身的優先順序是低於外部類的(不包含@Bean方法哦)
- 普通內部類屬於外部類的memberClasses,因此它會在解析當前外部類的第一步
processMemberClasses()
時被解析 - 普通內部類的beanName和靜態內部類是有差異的,如下截圖:
思考題:
請思考:為何使用普通內部類得到的是這個結果呢?建議copy我的demo,自行走一遍流程,多動手總是好的
總結
本文一如既往的很乾哈。寫本文的原動力是因為真的太多小夥伴在看Spring Boot自動配置類的時候,無法理解為毛它有些@Bean配置要單獨寫在一個static靜態類裡面,感覺挺費事;方法前直接價格static不香嗎?通過這篇文章 + 上篇文章的解讀,相信A哥已經給了你答案了。
static關鍵字在Spring中使用的這個專欄,下篇將進入到可能是你更關心的一個話題:為毛static欄位不能使用@Autowired注入的分析,下篇見~