Spring Boot 2.6.0正式發(fā)布,循環(huán)引用終于被禁
前言
北京時間2021-11-17,Spring Boot 2.6.0正式發(fā)布?;貞浺幌律洗伟l(fā)版還是上次,相比于2.5.0版本的打醬油,本次的升級點更猛些。
2.5.0版本的新特性在這里:【方向盤】Spring Boot 2.5.0正式發(fā)布,環(huán)境變量可指定前綴的功能很贊)
說明:Spring Boot 2.6.1隨后作為補丁版本立馬發(fā)布了,修復了若干問題。因此保持習慣,生產(chǎn)上請盡量保持最新的(小)版本
所屬專欄
- 【方向盤】-Spring Boot新特性
相關(guān)下載
- 【本專欄源代碼】:https://github.com/yourbatman/FXP-java-ee
- 【技術(shù)專欄源代碼大本營】:https://github.com/yourbatman/tech-column-learning
- 【女媧Knife-Initializr工程】訪問地址:http://152.136.106.14:8761
- 【程序員專用網(wǎng)盤】公益上線啦,注冊送1G超小容量,幫你實踐做減法:https://wangpan.yourbatman.cn
- 【Java開發(fā)軟件包(Mac)】:https://wangpan.yourbatman.cn/s/rEH0 提取碼:javakit
版本約定
Spring Boot 2.6.0
正文
關(guān)于版本號,從2.4.x 版本開始版本號不帶 .RELEASE 后綴了!通過表格描述下Spring Boot各個版本現(xiàn)在的更新、維護狀況:
Spring Boot每年會在5月份和11月份發(fā)布兩個中型版本(一般都會有部分不向下兼容的情況,升級需謹慎),每個中型版本提供1年的支持(免費),提供2年+的商業(yè)支持(付費)。按此節(jié)奏可知:Spring Boot 2.6.0發(fā)布也宣布著2.4.x版本停止(免費)支持,而2.7.0版本預(yù)計會在2022年的5月份和大家見面。
2.6版本主要新特性
禁止循環(huán)引用
Spring Boot終究忍不住,禁止(Bean的)循環(huán)引用了!!!
注意:只是Spring Boot默認禁止了,但Spring Framework默認還是允許的哦
對于有代碼潔癖的開發(fā)者來說,看到循環(huán)引用的代碼是“不舒服”的。在業(yè)務(wù)開發(fā)中,有一種聲音是:循環(huán)引用不可避免,但實際上應(yīng)該思考:若出現(xiàn)了循環(huán)引用,必定是結(jié)構(gòu)設(shè)計上不合理導致,有優(yōu)化空間!若你是個有追求的程序員,是可以很容易發(fā)現(xiàn)這種不合理的。
什么是循環(huán)引用?
如圖,循環(huán)引用一般指A引用B,B又引用了A。更極端一點的循環(huán)引用case可以是:A引用A,本文將以此為例進行代碼演示。
什么是循環(huán)依賴?它是循環(huán)引用的一種具象形式,如Spring Bean之間的循環(huán)依賴就屬于循環(huán)引用。大多數(shù)情況下,可認為循環(huán)依賴和循環(huán)引用語義上是相同的。
在Spring Boot場景下,準備Bean循環(huán)依賴的基礎(chǔ)代碼:
- /**
- * 在此處添加備注信息
- *
- * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a>
- * @site https://yourbatman.cn
- * @date 2021/12/11 20:43
- * @since 0.0.1
- */
- @Service
- public class AService {
- @Autowired
- private AService aService;
- @PostConstruct
- private void init() {
- System.out.println("循環(huán)依賴:" + (this == aService));
- }
- }
2.6.0之前版本(以2.5.x為例)
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.5.7</version>
- </parent>
啟動Spring Boot應(yīng)用,控制臺輸出:
結(jié)果:正常啟動。這便是我們口頭上常說的:Spring已經(jīng)解決了Bean的循環(huán)依賴問題
2.6.0及之后版本
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.6.0</version>
- </parent>
啟動Spring Boot應(yīng)用,控制臺輸出:
結(jié)果:啟動失敗。這便是從Spring Boot 2.6.0版本起禁止了循環(huán)引用的結(jié)果
如何解決循環(huán)引用?
文上有說到,循環(huán)引用屬于不合理的設(shè)計,但并非不能正常工作。這就像每個程序員都吐槽過屎山代碼依舊能正常work同一個道理:它不好,但有意義。
既然“不合理”,那就有理由規(guī)避。針對循環(huán)引用的解決方案,總結(jié)一下主要有兩種:
確保循環(huán)引用不再存在:整改/優(yōu)化業(yè)務(wù)邏輯
允許循環(huán)引用:無需改代碼
方案一:確保循環(huán)引用不再存在
好,這很好!難,這很難!本方案是最好的,也是最難的,Spring團隊當然最喜歡你這么去做,做難事必有所得嘛!
從Spring Boot 2.6.0開始的這個默認行為(不允許循環(huán)引用)能感受到:循環(huán)引用的編碼方式是不被推薦的,是壞味道的代碼。為此,期望正在看本文的coder給自己立個flag哈:不再寫循環(huán)引用的代碼,盡量吧😄。
奈何,好的東西/方案實現(xiàn)起來一般都很難,循環(huán)引用亦是如此。在筆者認為難點主要在程序員本身,主要表現(xiàn)在這三點:
思考不足。提起需求就開工看起來效率很高,實則往往相反
眼光不遠。這是短期利益和長期收益的PK,短期利益更具誘惑性,然而長期收益才具備更高價值
追求不夠。明明知道這么做不太好,但就是這么做了??朔щy好比打怪升級,過關(guān)斬將方能提高自己的上限
從A點到B點,若距離只有10m,走路的方式是最快的;若有1km,自行車是最佳;若超過10km,就是小汽車;若超過1000km,當選火車/飛機!總而言之:能夠積累才叫多,不用重來才叫快!
方案二:允許循環(huán)引用
此方案更像是繞過問題而非解決問題本身!!!
它是一種妥協(xié)方案而非最佳實踐。在Spring Boot 2.6.0之前版本無需擔心此問題(默認允許循環(huán)引用),若你準備使用2.6.x但現(xiàn)實情況依舊必須允許循環(huán)引用那該怎么辦呢?
有哪些現(xiàn)實情況呢?諸如:老項目升級Spring Boot版本需要保持向下兼容性;公司coder的水平不一,強制高標準的要求將會嚴重影響到生產(chǎn)效率等等
為此,做法只有一個:禁用默認行為(允許循環(huán)引用)。具體做法也很簡單,其實在文上啟動失敗的報錯詳情里Spring Boot已非常貼心的告訴你了:
所以只需在配置文件application.properties里加上這個屬性:
- spring.main.allow-circular-references = true
再次啟用Spring Boot 2.6.0版本的應(yīng)用:正常啟動。
除了加屬性這個方法之外,也可以通過啟動類API的方式來設(shè)置,能達到同樣效果:
- public static void main(String[] args) {
- new SpringApplicationBuilder(Application.class)
- .allowCircularReferences(true) // 允許循環(huán)引用
- .run(args);
- }
我們知道,允許循環(huán)引用與否其實是Spring Framework的能力,Spring Boot只是將其暴露為屬性參數(shù)方便開發(fā)者來控制而已。那么問題來了,如果是一個構(gòu)建在純Spring Framework上的應(yīng)用,如何禁止循環(huán)引用呢?你知道怎么做嗎?歡迎在留言區(qū)討論作答,或私聊我探討學習~
加餐:允許循環(huán)引用了但依舊報錯
也許你一直認為Spring已經(jīng)解決循環(huán)引用問題了,所以在使用過程中可以“毫無顧忌”。非也,某些“特殊”場景下可能依舊會碰壁,并且問題還很隱蔽不好定位,不信你看我層層遞進的給你描述這個場景:
說明:以下代碼在允許循環(huán)引用的Spring Boot場景下演示運行
基礎(chǔ)代碼:
本例使用@PostConstruct來模擬觸發(fā)方法調(diào)用,效果和Controller里調(diào)Service方法一樣哈
- @Service
- public class AService {
- @PostConstruct
- private void init() {
- String threadName = Thread.currentThread().getName();
- System.out.printf("線程號為%s,開始調(diào)用業(yè)務(wù)fun方法\n", threadName);
- fun();
- }
- public void fun() {
- String threadName = Thread.currentThread().getName();
- System.out.printf("線程號為%s,開始處理業(yè)務(wù)\n", threadName);
- }
- }
啟動應(yīng)用即觸發(fā)動作,控制臺輸出為:
- 線程名為main,開始調(diào)用業(yè)務(wù)fun方法
- 線程名為main,fun方法開始處理業(yè)務(wù)
完美!此時,你發(fā)現(xiàn)fun方法執(zhí)行時間太長,需要做異步化處理。你就立馬想到了使用Spring提供的@Async注解輕松搞定:
- @Async
- public void fun() {
- ...
- }
再次運行,控制臺輸出:
- 線程名為main,開始調(diào)用業(yè)務(wù)fun方法
- 線程名為main,fun方法開始處理業(yè)務(wù)
what?木有生效呀!這時你靈機一動,原因是沒用開啟該模塊嘛。所以你迅速的使用@EnableAsync注解啟用Spring的異步模塊,滿懷期待的再次運行應(yīng)用,控制臺輸出:
- 線程名為main,開始調(diào)用業(yè)務(wù)fun方法
- 線程名為main,fun方法開始處理業(yè)務(wù)
what a ...?怎么還是不行。你撓了撓頭,想起來之前踩過的“事務(wù)不生效的坑”,場景和這類似,所以你模仿著采用了相同的方式來解決:自己注入自己(循環(huán)依賴)
- @Autowired
- private AService aService; // 自己注入自己
- @PostConstruct
- private void init() {
- ...
- aService.fun(); // 通過代理對象調(diào)用而非this調(diào)用
- }
這次滿懷信心的再次運行,沒想到,啟動拋出BeanCurrentlyInCreationException異常
- org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Bean with name 'AService' has been injected into other beans [AService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
- at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:649) ~[spring-beans-5.3.13.jar:5.3.13]
- ...
異常關(guān)鍵字:circular reference循環(huán)引用!!!不是說好了允許循環(huán)引用的嗎?怎么肥四?怎么破???
至此,筆者將此問題拋出,有興趣的同學可思考一下問題根因、解決方案哈。最終的效果應(yīng)該是不同線程異步執(zhí)行的:
- 線程名為main,開始調(diào)用業(yè)務(wù)fun方法
- 線程名為task-1,fun方法開始處理業(yè)務(wù)
Tips:筆者在之前的文章里對此問題有過非常非常詳細的敘述,感興趣的可自行向前翻哈!!!主動學習😄
更加靈活的自定義脫敏規(guī)則
對于/env和/configprops這兩個端點,常常會有敏感信息存在,比如:數(shù)據(jù)庫密碼等等。為了避免敏感信息外泄,一般做法是禁用這兩個端點,但粒度太粗,在很多時候是不合適的,因為這可能大大增加調(diào)試程序、定位問題的復雜程度,所以對該端點的某些信息脫敏不失為一個折中的好辦法。
Spring Boot使用Sanitizer(中文意思:消毒殺菌劑)來進行脫敏。比如屬性配置有如下配置:
- mysql.password = 123456
- redis.pwd = 654321
這時候訪問端點/actuator/env,得到的結(jié)果是這樣子的:
如圖所示,感覺有點厚此薄彼有木有???其實一切事出有因,EnvironmentEndpoint使用Sanitizer進行脫敏處理,而它自帶一些默認行為:
若不再這個范圍內(nèi)的key(比如上面的redis.pwd)也需要脫敏,很簡單,價格配置項即可:
- management.endpoint.env.additional-keys-to-sanitize = redis.pwd
- #management.endpoint.env.additional-keys-to-sanitize = pwd # 脫敏范圍更大
效果如下:
完美脫敏!!!這么做可以搞定絕大部分場景,但是某些特殊情況下,通過這種配置不是很好做,比如:同一個key,在不同的屬性源里表現(xiàn)不一樣。在application.properties里的話脫敏,而在application-dev.properties里不需要脫敏(開發(fā)環(huán)境嘛,明文裸奔更有助于調(diào)試程序)。
這個case若適用上面配置的方式不可處理,確切點說很不方便吧。Spring Boot意識到了這個“難點”,在2.6.0版本了新增了更靈活的自定義脫敏規(guī)則的能力,做法很簡單:自定義SanitizingFunction類型的Bean即可。
- // Since: 2.6.0
- @FunctionalInterface
- public interface SanitizingFunction {
- SanitizableData apply(SanitizableData data);
- }
比如關(guān)于Redis的配置項放redis.properties文件里,然后讀進來:
- @PropertySource("classpath:redis.properties")
- @Configuration(proxyBeanMethods = false)
- public class AppConfiguration {}
- redis.properties文件內(nèi)容:
- redis.pwd = 654321
要求:redis.properties文件里面所有包含pwd的key的值都做脫敏處理,而其它屬性源不管。這時使用上面配置方式就無法實現(xiàn)了(或者說很難實現(xiàn)吧),Spring Boot 2.6.0新增的特性,API方式可以非常靈活方便的搞定:
- @Bean
- public SanitizingFunction pwdSanitizingFunction() {
- return data -> {
- org.springframework.core.env.PropertySource<?> propertySource = data.getPropertySource();
- String key = data.getKey();
- // 僅對redis.properties里面的某些key做脫敏
- if (propertySource.getName().contains("redis.properties")) {
- if (key.equals("redis.pwd")) {
- return data.withValue(SANITIZED_VALUE);
- }
- }
- return data;
- };
- }
再次請求/actuator/env端點,結(jié)果如下:
Spring MVC默認使用全新匹配策略
在Spring Framework 5之前,關(guān)于路徑匹配一直以來有且只有一種方式:基于Ant風格的url匹配,也就是熟悉的AntPathMatcher。在5.0版本之后引入了全新的路徑匹配器:PathPattern。
關(guān)于它倆都啥意思,怎么用,有什么區(qū)別,不是本文的重點。筆者前面文章有詳細介紹,建議閱讀哈。這里給個電梯直達:Spring5新寵:PathPattern,AntPathMatcher:那我走?
Spring Boot從2.0.0版本開始構(gòu)建在Spring Framework 5之上,但它直到2.6.0版本才徹底的將Spring MVC的默認匹配從AntPathMatcher切換為了PathPattern,這也是本次版本升級的一大特征之一。代碼上體現(xiàn)在這里:
- // 2.5.7
- public static class Pathmatch {
- private MatchingStrategy matchingStrategy = MatchingStrategy.ANT_PATH_MATCHER;
- }
- // 2.6.0
- public static class Pathmatch {
- private MatchingStrategy matchingStrategy = MatchingStrategy.PATH_PATTERN_PARSER;
- }
若你需要回到Ant的匹配方式上(比如擔心兼容性),只需加上一行簡單配置就成:
- spring.mvc.pathmatch.matching-strategy = ant-path-matcher
Redis自動開啟連接池
現(xiàn)在,只要classpath里存在commons-pool2這個jar,就會自動為Redis開啟連接池(包括Jedis和Lettuce哦)。
在2.6.0之前的版本,配置Redis時是否啟用連接池是由使用者顯示來決定的,現(xiàn)在自動了,說明Spring Boot是推薦使用Redis時用連接池的哦。
從源代碼的角度,區(qū)別主要在這(以現(xiàn)在更為常用的Lettuce為例):
LettuceConnectionConfiguration
下面代碼是2.6.0版本做的改動:
可以看到策略是有變化的:之前默認關(guān)閉連接池需要顯示開啟,2.6.0之后是默認開啟需要顯示關(guān)閉。
Spring Boot 2.4.x停止維護
按照Spring Boot現(xiàn)在版本規(guī)則:官方只免費維護當前主線版本和次版本,發(fā)布新版本后上上個版本自然就停止維護嘍,倒逼開發(fā)者保持升級,用新版本產(chǎn)品,享受技術(shù)紅利呀!
說明:這里指的停止維護是官方免費維護,不包含商業(yè)付費維護
依賴升級
這部分一般不用太關(guān)心,稍微留一下主要的組件版本即可。
- Spring Data 2021.1
- Spring Kafka 2.8
- Apache Kafka 3.0(Spring果然站在最前沿呀)
- Commons Pool 2.11
- Elasticsearch 7.15
- Hibernate 5.6
- Mockito 4.0
- ...
刪除和棄用
按照規(guī)約,在Spring Boot 2.4.0里被標注為棄用@Deprecated的類在此版本將會被刪除?;貞?.4.0版本棄用了哪些?
Spring Boot 2.4.0最大升級就是對ConfigFileApplicationListener的升級。
電梯直達:Spring Boot 2.4.0正式發(fā)布,全新的配置文件加載機制(不向下兼容)
那時壯志雄心計劃下下個版本(也就是2.6.0版本)就可以移除此類,但Spring團隊這次還是擔心步子邁得太大扯著dan,留下了它并改口將在3.0里移除掉。
棄用類:
- JDBC的AbstractDataSourceInitializer體系,使用DataSourceScriptDatabaseInitializer體系替代
- Hibernate的SpringPhysicalNamingStrategy,使用CamelCaseToUnderscoresNamingStrategy替代
- 測試框架的AbstractApplicationContextRunner類的幾個方法被啟用,使用新的RunnerConfiguration類替代
官網(wǎng)新增SUPPORT標簽頁
由于Spring Boot的更新迭代速度非???,每個版本的發(fā)版時間、維護周期一直困擾著廣大開發(fā)者,為此隨著2.6.0版本的發(fā)布,官網(wǎng)上非常暖心的提供了一個SUPPORT標簽來展示各個版本的情況:
以及當天所處的一個狀態(tài):
地址:https://spring.io/projects/spring-boot#support
總結(jié)
Spring Boot 2.6.0的更新點還是比較多的,值得肯定,當然也值得升級。
Java領(lǐng)域的云原生時代,雖然受到了挑戰(zhàn),但毫無疑問在未來的5年甚至10年,Spring Boot依舊是標準的腳手架,是云原生應(yīng)用的基礎(chǔ)設(shè)施。它的能力能解放開發(fā)者的精力,時間用于業(yè)務(wù)設(shè)計、開發(fā)上。
最后,多分享一句。筆者從中覺得的每次版本升級符合Spring的決策哲學:先服從,再引領(lǐng)。畢竟對于龐大的Spring體系來說,每個重要決策都并非拍腦袋就可以,背后需要宏觀思想作為指導。
拿循環(huán)引用這個例子來講,Spring Framework最初默認允許循環(huán)依賴:設(shè)計上似乎留下了“不和諧”,但那會Spring初出茅廬,話語權(quán)不夠,所以擁抱大眾,活下來才是第一位。Spring技術(shù)棧發(fā)展到現(xiàn)在成為了實際的開發(fā)標準,在Java領(lǐng)域可謂已有絕對的話語權(quán),因此它開始引領(lǐng):默認不允許循環(huán)引用。
【編輯推薦】