Spring Boot 2.6.0正式发布,循环引用终于被禁

开发 前端
北京时间2021年11月17日,Spring Boot 2.6.0正式发布。回忆一下上次发版还是上次,相比于2.5.0版本的打酱油,本次的升级点更猛些。

[[440016]]

前言

北京时间2021-11-17,Spring Boot 2.6.0正式发布。回忆一下上次发版还是上次,相比于2.5.0版本的打酱油,本次的升级点更猛些。

2.5.0版本的新特性在这里:【方向盘】Spring Boot 2.5.0正式发布,环境变量可指定前缀的功能很赞)

说明:Spring Boot 2.6.1随后作为补丁版本立马发布了,修复了若干问题。因此保持习惯,生产上请尽量保持最新的(小)版本

所属专栏

  • 【方向盘】-Spring Boot新特性

相关下载

  • 【本专栏源代码】:https://github.com/yourbatman/FXP-java-ee
  • 【技术专栏源代码大本营】:https://github.com/yourbatman/tech-column-learning
  • 【女娲Knife-Initializr工程】访问地址:http://152.136.106.14:8761
  • 【程序员专用网盘】公益上线啦,注册送1G超小容量,帮你实践做减法:https://wangpan.yourbatman.cn
  • 【Java开发软件包(Mac)】:https://wangpan.yourbatman.cn/s/rEH0 提取码:javakit

版本约定

Spring Boot 2.6.0

正文

关于版本号,从2.4.x 版本开始版本号不带 .RELEASE 后缀了!通过表格描述下Spring Boot各个版本现在的更新、维护状况:

Spring Boot每年会在5月份和11月份发布两个中型版本(一般都会有部分不向下兼容的情况,升级需谨慎),每个中型版本提供1年的支持(免费),提供2年+的商业支持(付费)。按此节奏可知:Spring Boot 2.6.0发布也宣布着2.4.x版本停止(免费)支持,而2.7.0版本预计会在2022年的5月份和大家见面。

2.6版本主要新特性

禁止循环引用

Spring Boot终究忍不住,禁止(Bean的)循环引用了!!!

注意:只是Spring Boot默认禁止了,但Spring Framework默认还是允许的哦

对于有代码洁癖的开发者来说,看到循环引用的代码是“不舒服”的。在业务开发中,有一种声音是:循环引用不可避免,但实际上应该思考:若出现了循环引用,必定是结构设计上不合理导致,有优化空间!若你是个有追求的程序员,是可以很容易发现这种不合理的。

什么是循环引用?

如图,循环引用一般指A引用B,B又引用了A。更极端一点的循环引用case可以是:A引用A,本文将以此为例进行代码演示。

什么是循环依赖?它是循环引用的一种具象形式,如Spring Bean之间的循环依赖就属于循环引用。大多数情况下,可认为循环依赖和循环引用语义上是相同的。

在Spring Boot场景下,准备Bean循环依赖的基础代码:

  1. /** 
  2.  * 在此处添加备注信息 
  3.  * 
  4.  * @author YourBatman. <a href=mailto:yourbatman@aliyun.com>Send email to me</a> 
  5.  * @site https://yourbatman.cn 
  6.  * @date 2021/12/11 20:43 
  7.  * @since 0.0.1 
  8.  */ 
  9. @Service 
  10. public class AService { 
  11.  
  12.     @Autowired 
  13.     private AService aService; 
  14.  
  15.     @PostConstruct 
  16.     private void init() { 
  17.         System.out.println("循环依赖:" + (this == aService)); 
  18.     } 
  19.  

2.6.0之前版本(以2.5.x为例)

  1. <parent> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-parent</artifactId> 
  4.     <version>2.5.7</version> 
  5. </parent> 

启动Spring Boot应用,控制台输出:

结果:正常启动。这便是我们口头上常说的:Spring已经解决了Bean的循环依赖问题

2.6.0及之后版本

  1. <parent> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-parent</artifactId> 
  4.     <version>2.6.0</version> 
  5. </parent> 

启动Spring Boot应用,控制台输出:

结果:启动失败。这便是从Spring Boot 2.6.0版本起禁止了循环引用的结果

如何解决循环引用?

文上有说到,循环引用属于不合理的设计,但并非不能正常工作。这就像每个程序员都吐槽过屎山代码依旧能正常work同一个道理:它不好,但有意义。

既然“不合理”,那就有理由规避。针对循环引用的解决方案,总结一下主要有两种:

确保循环引用不再存在:整改/优化业务逻辑

允许循环引用:无需改代码

方案一:确保循环引用不再存在

好,这很好!难,这很难!本方案是最好的,也是最难的,Spring团队当然最喜欢你这么去做,做难事必有所得嘛!

从Spring Boot 2.6.0开始的这个默认行为(不允许循环引用)能感受到:循环引用的编码方式是不被推荐的,是坏味道的代码。为此,期望正在看本文的coder给自己立个flag哈:不再写循环引用的代码,尽量吧😄。

奈何,好的东西/方案实现起来一般都很难,循环引用亦是如此。在笔者认为难点主要在程序员本身,主要表现在这三点:

思考不足。提起需求就开工看起来效率很高,实则往往相反

眼光不远。这是短期利益和长期收益的PK,短期利益更具诱惑性,然而长期收益才具备更高价值

追求不够。明明知道这么做不太好,但就是这么做了。克服困难好比打怪升级,过关斩将方能提高自己的上限

从A点到B点,若距离只有10m,走路的方式是最快的;若有1km,自行车是最佳;若超过10km,就是小汽车;若超过1000km,当选火车/飞机!总而言之:能够积累才叫多,不用重来才叫快!

方案二:允许循环引用

此方案更像是绕过问题而非解决问题本身!!!

它是一种妥协方案而非最佳实践。在Spring Boot 2.6.0之前版本无需担心此问题(默认允许循环引用),若你准备使用2.6.x但现实情况依旧必须允许循环引用那该怎么办呢?

有哪些现实情况呢?诸如:老项目升级Spring Boot版本需要保持向下兼容性;公司coder的水平不一,强制高标准的要求将会严重影响到生产效率等等

为此,做法只有一个:禁用默认行为(允许循环引用)。具体做法也很简单,其实在文上启动失败的报错详情里Spring Boot已非常贴心的告诉你了:

所以只需在配置文件application.properties里加上这个属性:

  1. spring.main.allow-circular-references = true 

再次启用Spring Boot 2.6.0版本的应用:正常启动。

除了加属性这个方法之外,也可以通过启动类API的方式来设置,能达到同样效果:

  1. public static void main(String[] args) { 
  2.     new SpringApplicationBuilder(Application.class) 
  3.             .allowCircularReferences(true) // 允许循环引用 
  4.             .run(args); 

我们知道,允许循环引用与否其实是Spring Framework的能力,Spring Boot只是将其暴露为属性参数方便开发者来控制而已。那么问题来了,如果是一个构建在纯Spring Framework上的应用,如何禁止循环引用呢?你知道怎么做吗?欢迎在留言区讨论作答,或私聊我探讨学习~

加餐:允许循环引用了但依旧报错

也许你一直认为Spring已经解决循环引用问题了,所以在使用过程中可以“毫无顾忌”。非也,某些“特殊”场景下可能依旧会碰壁,并且问题还很隐蔽不好定位,不信你看我层层递进的给你描述这个场景:

说明:以下代码在允许循环引用的Spring Boot场景下演示运行

基础代码:

本例使用@PostConstruct来模拟触发方法调用,效果和Controller里调Service方法一样哈

  1. @Service 
  2. public class AService { 
  3.  
  4.     @PostConstruct 
  5.     private void init() { 
  6.         String threadName = Thread.currentThread().getName(); 
  7.         System.out.printf("线程号为%s,开始调用业务fun方法\n", threadName); 
  8.         fun(); 
  9.  
  10.     } 
  11.  
  12.     public void fun() { 
  13.         String threadName = Thread.currentThread().getName(); 
  14.         System.out.printf("线程号为%s,开始处理业务\n", threadName); 
  15.     } 
  16.  

启动应用即触发动作,控制台输出为:

  1. 线程名为main,开始调用业务fun方法 
  2. 线程名为main,fun方法开始处理业务 

完美!此时,你发现fun方法执行时间太长,需要做异步化处理。你就立马想到了使用Spring提供的@Async注解轻松搞定:

  1. @Async 
  2. public void fun() { 
  3.  ... 

再次运行,控制台输出:

  1. 线程名为main,开始调用业务fun方法 
  2. 线程名为main,fun方法开始处理业务 

what?木有生效呀!这时你灵机一动,原因是没用开启该模块嘛。所以你迅速的使用@EnableAsync注解启用Spring的异步模块,满怀期待的再次运行应用,控制台输出:

  1. 线程名为main,开始调用业务fun方法 
  2. 线程名为main,fun方法开始处理业务 

what a ...?怎么还是不行。你挠了挠头,想起来之前踩过的“事务不生效的坑”,场景和这类似,所以你模仿着采用了相同的方式来解决:自己注入自己(循环依赖)

  1. @Autowired 
  2. private AService aService; // 自己注入自己 
  3.  
  4. @PostConstruct 
  5. private void init() { 
  6.  ... 
  7.     aService.fun(); // 通过代理对象调用而非this调用 
  8.  

这次满怀信心的再次运行,没想到,启动抛出BeanCurrentlyInCreationException异常

  1. 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 offfor example. 
  2.  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:649) ~[spring-beans-5.3.13.jar:5.3.13] 
  3.  ... 

异常关键字:circular reference循环引用!!!不是说好了允许循环引用的吗?怎么肥四?怎么破???

至此,笔者将此问题抛出,有兴趣的同学可思考一下问题根因、解决方案哈。最终的效果应该是不同线程异步执行的:

  1. 线程名为main,开始调用业务fun方法 
  2. 线程名为task-1,fun方法开始处理业务 

Tips:笔者在之前的文章里对此问题有过非常非常详细的叙述,感兴趣的可自行向前翻哈!!!主动学习😄

更加灵活的自定义脱敏规则

对于/env和/configprops这两个端点,常常会有敏感信息存在,比如:数据库密码等等。为了避免敏感信息外泄,一般做法是禁用这两个端点,但粒度太粗,在很多时候是不合适的,因为这可能大大增加调试程序、定位问题的复杂程度,所以对该端点的某些信息脱敏不失为一个折中的好办法。

Spring Boot使用Sanitizer(中文意思:消毒杀菌剂)来进行脱敏。比如属性配置有如下配置:

  1. mysql.password = 123456 
  2. redis.pwd = 654321 

这时候访问端点/actuator/env,得到的结果是这样子的:

如图所示,感觉有点厚此薄彼有木有???其实一切事出有因,EnvironmentEndpoint使用Sanitizer进行脱敏处理,而它自带一些默认行为:

若不再这个范围内的key(比如上面的redis.pwd)也需要脱敏,很简单,价格配置项即可:

  1. management.endpoint.env.additional-keys-to-sanitize = redis.pwd 
  2. #management.endpoint.env.additional-keys-to-sanitize = pwd # 脱敏范围更大 

效果如下:

完美脱敏!!!这么做可以搞定绝大部分场景,但是某些特殊情况下,通过这种配置不是很好做,比如:同一个key,在不同的属性源里表现不一样。在application.properties里的话脱敏,而在application-dev.properties里不需要脱敏(开发环境嘛,明文裸奔更有助于调试程序)。

这个case若适用上面配置的方式不可处理,确切点说很不方便吧。Spring Boot意识到了这个“难点”,在2.6.0版本了新增了更灵活的自定义脱敏规则的能力,做法很简单:自定义SanitizingFunction类型的Bean即可。

  1. // Since: 2.6.0 
  2. @FunctionalInterface 
  3. public interface SanitizingFunction { 
  4.  SanitizableData apply(SanitizableData data); 

比如关于Redis的配置项放redis.properties文件里,然后读进来:

  1. @PropertySource("classpath:redis.properties"
  2. @Configuration(proxyBeanMethods = false
  3. public class AppConfiguration {} 
  4.  
  5. redis.properties文件内容: 
  6. redis.pwd = 654321 

要求:redis.properties文件里面所有包含pwd的key的值都做脱敏处理,而其它属性源不管。这时使用上面配置方式就无法实现了(或者说很难实现吧),Spring Boot 2.6.0新增的特性,API方式可以非常灵活方便的搞定:

  1. @Bean 
  2. public SanitizingFunction pwdSanitizingFunction() { 
  3.     return data -> { 
  4.         org.springframework.core.env.PropertySource<?> propertySource = data.getPropertySource(); 
  5.         String key = data.getKey(); 
  6.          
  7.         // 仅对redis.properties里面的某些key做脱敏 
  8.         if (propertySource.getName().contains("redis.properties")) { 
  9.             if (key.equals("redis.pwd")) { 
  10.                 return data.withValue(SANITIZED_VALUE); 
  11.             } 
  12.         } 
  13.         return data; 
  14.     }; 

再次请求/actuator/env端点,结果如下:

Spring MVC默认使用全新匹配策略

在Spring Framework 5之前,关于路径匹配一直以来有且只有一种方式:基于Ant风格的url匹配,也就是熟悉的AntPathMatcher。在5.0版本之后引入了全新的路径匹配器:PathPattern。

关于它俩都啥意思,怎么用,有什么区别,不是本文的重点。笔者前面文章有详细介绍,建议阅读哈。这里给个电梯直达:Spring5新宠:PathPattern,AntPathMatcher:那我走?

Spring Boot从2.0.0版本开始构建在Spring Framework 5之上,但它直到2.6.0版本才彻底的将Spring MVC的默认匹配从AntPathMatcher切换为了PathPattern,这也是本次版本升级的一大特征之一。代码上体现在这里:

  1. // 2.5.7 
  2. public static class Pathmatch { 
  3.  private MatchingStrategy matchingStrategy = MatchingStrategy.ANT_PATH_MATCHER; 
  4.  
  5. // 2.6.0 
  6. public static class Pathmatch { 
  7.  private MatchingStrategy matchingStrategy = MatchingStrategy.PATH_PATTERN_PARSER; 

若你需要回到Ant的匹配方式上(比如担心兼容性),只需加上一行简单配置就成:

  1. spring.mvc.pathmatch.matching-strategy = ant-path-matcher 

Redis自动开启连接池

现在,只要classpath里存在commons-pool2这个jar,就会自动为Redis开启连接池(包括Jedis和Lettuce哦)。

在2.6.0之前的版本,配置Redis时是否启用连接池是由使用者显示来决定的,现在自动了,说明Spring Boot是推荐使用Redis时用连接池的哦。

从源代码的角度,区别主要在这(以现在更为常用的Lettuce为例):

LettuceConnectionConfiguration

下面代码是2.6.0版本做的改动:

可以看到策略是有变化的:之前默认关闭连接池需要显示开启,2.6.0之后是默认开启需要显示关闭。

Spring Boot 2.4.x停止维护

按照Spring Boot现在版本规则:官方只免费维护当前主线版本和次版本,发布新版本后上上个版本自然就停止维护喽,倒逼开发者保持升级,用新版本产品,享受技术红利呀!

说明:这里指的停止维护是官方免费维护,不包含商业付费维护

依赖升级

这部分一般不用太关心,稍微留一下主要的组件版本即可。

  • 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
  • ...

删除和弃用

按照规约,在Spring Boot 2.4.0里被标注为弃用@Deprecated的类在此版本将会被删除。回忆2.4.0版本弃用了哪些?

Spring Boot 2.4.0最大升级就是对ConfigFileApplicationListener的升级。

电梯直达:Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)

那时壮志雄心计划下下个版本(也就是2.6.0版本)就可以移除此类,但Spring团队这次还是担心步子迈得太大扯着dan,留下了它并改口将在3.0里移除掉。

弃用类:

  • JDBC的AbstractDataSourceInitializer体系,使用DataSourceScriptDatabaseInitializer体系替代
  • Hibernate的SpringPhysicalNamingStrategy,使用CamelCaseToUnderscoresNamingStrategy替代
  • 测试框架的AbstractApplicationContextRunner类的几个方法被启用,使用新的RunnerConfiguration类替代

官网新增SUPPORT标签页

由于Spring Boot的更新迭代速度非常快,每个版本的发版时间、维护周期一直困扰着广大开发者,为此随着2.6.0版本的发布,官网上非常暖心的提供了一个SUPPORT标签来展示各个版本的情况:

以及当天所处的一个状态:

地址:https://spring.io/projects/spring-boot#support

总结

Spring Boot 2.6.0的更新点还是比较多的,值得肯定,当然也值得升级。

Java领域的云原生时代,虽然受到了挑战,但毫无疑问在未来的5年甚至10年,Spring Boot依旧是标准的脚手架,是云原生应用的基础设施。它的能力能解放开发者的精力,时间用于业务设计、开发上。

最后,多分享一句。笔者从中觉得的每次版本升级符合Spring的决策哲学:先服从,再引领。毕竟对于庞大的Spring体系来说,每个重要决策都并非拍脑袋就可以,背后需要宏观思想作为指导。

拿循环引用这个例子来讲,Spring Framework最初默认允许循环依赖:设计上似乎留下了“不和谐”,但那会Spring初出茅庐,话语权不够,所以拥抱大众,活下来才是第一位。Spring技术栈发展到现在成为了实际的开发标准,在Java领域可谓已有绝对的话语权,因此它开始引领:默认不允许循环引用。

【编辑推荐】

 

责任编辑:姜华 来源: Java方向盘
相关推荐

2018-08-13 16:15:41

2018-05-30 14:56:24

Spring轻量化Java 8

2018-06-20 15:42:09

2022-11-26 00:00:03

Spring指南体系

2019-05-17 15:26:06

Spring BootSpring BootJava

2022-12-12 08:34:57

SpringJava

2015-07-30 22:57:02

华为公有云/云计算

2022-12-05 16:02:56

iOSiOS 16信号

2022-12-01 16:01:39

iOSiOS 16信号

2023-10-07 11:18:23

iOS 17苹果

2023-11-08 14:08:41

iOS 17苹果

2017-03-31 14:31:08

QQ浏览器修复功能特性

2011-12-26 09:06:16

Java

2009-12-17 10:25:39

Spring 3.0

2012-08-31 13:49:32

2024-12-03 10:46:48

Spring优化开发

2009-05-04 17:47:13

LinuxMandrivaSpring

2011-12-14 09:14:46

JavaJ2EESpring

2021-11-29 08:13:41

Spring Boot环境变量Spring技术

2021-03-31 06:05:08

微信朋友圈腾讯
点赞
收藏

51CTO技术栈公众号