static關鍵字有何魔法?竟讓Spring Boot搞出那麼多靜態內部類

生命過短暫,不要去作一些根本沒有人想要的東西。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以避免費學習。關注公衆號【BAT的烏托邦】逐個擊破,深刻掌握,拒絕淺嘗輒止。java

前言

各位小夥伴你們好,我是A哥。上篇文章瞭解了static關鍵字 + @Bean方法的使用,知曉了它可以提高Bean的優先級,在@Bean方法前標註static關鍵字,特定狀況下能夠避免一些煩人的「警告」日誌的輸出,排除隱患讓工程變得更加安全。咱們知道static關鍵字它不只可以使用在方法上,那麼本文將繼續挖掘static在Spring環境下的用處。程序員

根據所學的JavaSE基礎,static關鍵字除了可以修飾方法外,還能使用在這兩個地方:web

  1. 修飾類。確切的說,應該叫修飾內部類,因此它叫靜態內部類
  2. 修飾成員變量

其實static還能夠修飾代碼塊、static靜態導包等,但很明顯,這些與本文無關spring

接下來就以這爲兩條主線,分別研究static在對應場景下的做用,本文將聚焦在靜態內部類上。
編程


版本約定

本文內容若沒作特殊說明,均基於如下版本:windows

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

正文

說到Java裏的static關鍵字,這當屬最基礎的入門知識,是Java中經常使用的關鍵字之一。你平時用它來修飾變量和方法了,可是對它的瞭解,即便放在JavaSE情景下知道這些仍是不夠的,問題雖小但這每每反映了你對Java基礎的瞭解程度。tomcat

固然嘍,本文並不討論它在JavaSE下使用,畢竟我們仍是有必定逼格的專欄,須要進階一把,玩玩它在Spring環境下到底可以迸出怎麼樣的火花呢?好比靜態內部類~安全


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 靜態內部類傻傻分不清,爲了方便後續講解,本處把關鍵要素作簡要對比說明:

  1. 靜態內部類能夠聲明靜態or實例成員(屬性和方法);而普通內部類則不能夠聲明靜態成員(屬性和方法)
  2. 靜態內部類實例的建立不依賴於外部類;而普通外部類實例建立必須先有外部類實例才行(綁定關係拿捏得死死的,不信你問鄭凱)
  3. 靜態內部類不能訪問外部類的實例成員;而普通內部類能夠隨意訪問(無論靜態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();
        }
    }
}

我先解釋下我這麼作的意圖:

  1. 增長一個字母P開頭的內部類,天然順序P在O(外部類)後面,消除影響
  2. P開頭的內部類在源碼擺放順序上故意放在了I開頭的內部類的上面,一樣爲了消除字母表順序帶來的影響
    1. 目的:看看是按照字節碼順序,仍是字母表順序呢?
  3. 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...

這個結果大而全,是有說服力的,經過這幾個示例能夠總結出以下結論:

  1. 垮.java文件 (垮配置類)之間的順序,是由天然順序來保證的(字母表順序)
    1. 如上:下加載A打頭的配置類(含靜態內部類),再是O打頭的,再是Z打頭的
  2. 同一.java文件內部,static靜態內部類優先於外部類初始化。如有多個靜態內部類,那麼按照類名天然排序初始化(並不是按照定義順序哦,請務必注意)
    1. 說明:通常內部類只可能與外部類「發生關係」,與兄弟之間不建議有任何聯繫,不然順序控制上你就得小心了。畢竟靠天然順序去保證是一種弱保證,容錯性過低
  3. 同一.java文件內,不一樣類內的@Bean方法之間的執行順序,保持同2一致(也就說你的@Bean所在的@Configuration配置類先加載,那你就優先被初始化嘍)
    1. 同一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配置類的順序問題,事前需強調兩點:

  1. 不一樣 .java文件 之間的加載順序是不重要的,Spring官方也強烈建議使用者不要去依賴這種順序
    1. 由於無狀態性,所以你在使用過程當中能夠認爲垮@Configuration文件以前的初始化順序是不肯定的
  2. 同一.javaw文件內也可能存在多個@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);
			}
		}
	}

這段代碼的信息量是很大的,分解爲以下兩大步:

  1. 經過ResourcePatternResolver從磁盤裏加載到全部的 .class資源Resource[]。這裏面順序信息就出現了,加載磁盤Resource資源的過程很複雜,總而言之它依賴於你os文件系統。因此關於資源的順序可簡單理解爲:你磁盤文件裏是啥順序它就按啥順序加載進來

注意:不是看.java源代碼順序,也不是看你target目錄下的文件順序(該目錄是通過了IDEA反編譯的結果,沒法反應真實順序),而是編譯後看你的磁盤上的.class文件的文件順序

  1. 遍歷每個Resource資源,並非每一個資源都會成爲candidates候選,它有個雙重過濾(對應兩個isCandidateComponent()方法):
    1. 過濾一:使用TypeFilter執行過濾,看看是否被排除;再看看是否知足@Conditional條件
    2. 過濾二:它有兩種case能知足條件(任意知足一個case便可)
      1. isIndependent()是獨立類(top-level類 or 靜態內部類屬於獨立類) 而且 isConcrete()是具體的(非接口非抽象類)
      2. isAbstract()是抽象類 而且 類內存在標註有@Lookup註解的方法

基於以上例子,磁盤中的.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哥不用再作詳盡分析了,看似比較複雜其實有了上面的分析仍是比較容易理解的。主要有以下兩點須要注意:

  1. 普通內部類它不是一個獨立的類(也就是說isIndependent() = false),因此它並不能像靜態內部類那樣預先就被掃描進去,如圖結果展現:
  2. 普通內部類初始化以前,必定得先初始化外部類,因此類自己的優先級是低於外部類的(不包含@Bean方法哦)
  3. 普通內部類屬於外部類的memberClasses,所以它會在解析當前外部類的第一步processMemberClasses()時被解析
  4. 普通內部類的beanName和靜態內部類是有差別的,以下截圖:

思考題:

請思考:爲什麼使用普通內部類獲得的是這個結果呢?建議copy個人demo,自行走一遍流程,多動手老是好的


總結

本文一如既往的很乾哈。寫本文的原動力是由於真的太多小夥伴在看Spring Boot自動配置類的時候,沒法理解爲毛它有些@Bean配置要單獨寫在一個static靜態類裏面,感受挺費事;方法前直接價格static不香嗎?經過這篇文章 + 上篇文章的解讀,相信A哥已經給了你答案了。

static關鍵字在Spring中使用的這個專欄,下篇將進入到多是你更關心的一個話題:爲毛static字段不能使用@Autowired注入的分析,下篇見~

相關文章
相關標籤/搜索