✍正文
关于版本号,从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循环依赖的基础代码:
/** * 在此处添加备注信息 * * @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("循环依赖:" + (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应用,控制台输出:
结果:正常启动。这便是我们口头上常说的:Spring已经解决了Bean的循环依赖问题
2.6.0及之后版本
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.0</version> </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
里加上这个属性:
spring.main.allow-circular-references = true
再次启用Spring Boot 2.6.0版本的应用:正常启动。
除了加属性这个方法之外,也可以通过启动类API的方式来设置,能达到同样效果:
public static void main(String[] args) { new SpringApplicationBuilder(Application.class) .allowCircularReferences(true) // 允许循环引用 .run(args); }
我们知道,允许循环引用与否其实是Spring Framework的能力,Spring Boot只是将其暴露为属性参数方便开发者来控制而已。那么问题来了,如果是一个构建在纯Spring Framework上的应用,如何禁止循环引用呢?你知道怎么做吗?欢迎在留言区讨论作答,或私聊我探讨学习~
加餐:允许循环引用了但依旧报错
也许你一直认为Spring已经解决循环引用问题了,所以在使用过程中可以“毫无顾忌”。非也,某些“特殊”场景下可能依旧会碰壁,并且问题还很隐蔽不好定位,不信你看我层层递进的给你描述这个场景:
说明:以下代码在允许循环引用的Spring Boot场景下演示运行
基础代码:
本例使用@PostConstruct来模拟触发方法调用,效果和Controller里调Service方法一样哈
@Service public class AService { @PostConstruct private void init() { String threadName = Thread.currentThread().getName(); System.out.printf("线程号为%s,开始调用业务fun方法\n", threadName); fun(); } public void fun() { String threadName = Thread.currentThread().getName(); System.out.printf("线程号为%s,开始处理业务\n", threadName); } }
启动应用即触发动作,控制台输出为:
线程名为main,开始调用业务fun方法 线程名为main,fun方法开始处理业务
完美!此时,你发现fun方法执行时间太长,需要做异步化处理。你就立马想到了使用Spring提供的@Async
注解轻松搞定:
@Async public void fun() { ... }
再次运行,控制台输出:
线程名为main,开始调用业务fun方法 线程名为main,fun方法开始处理业务
what?木有生效呀!这时你灵机一动,原因是没用开启该模块嘛。所以你迅速的使用@EnableAsync注解启用Spring的异步模块,满怀期待的再次运行应用,控制台输出:
线程名为main,开始调用业务fun方法 线程名为main,fun方法开始处理业务
what a …?怎么还是不行。你挠了挠头,想起来之前踩过的“事务不生效的坑”,场景和这类似,所以你模仿着采用了相同的方式来解决:自己注入自己(循环依赖)
@Autowired private AService aService; // 自己注入自己 @PostConstruct private void init() { ... aService.fun(); // 通过代理对象调用而非this调用 }
这次满怀信心的再次运行,没想到,启动抛出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] ...
异常关键字:circular reference
循环引用!!!不是说好了允许循环引用的吗?怎么肥四?怎么破???
至此,笔者将此问题抛出,有兴趣的同学可思考一下问题根因、解决方案哈。最终的效果应该是不同线程异步执行的:
线程名为main,开始调用业务fun方法 线程名为task-1,fun方法开始处理业务
Tips:笔者在之前的文章里对此问题有过非常非常详细的叙述,感兴趣的可自行向前翻哈!!!主动学习😄
✌更加灵活的自定义脱敏规则
对于/env和/configprops这两个端点,常常会有敏感信息存在,比如:数据库密码等等。为了避免敏感信息外泄,一般做法是禁用这两个端点,但粒度太粗,在很多时候是不合适的,因为这可能大大增加调试程序、定位问题的复杂程度,所以对该端点的某些信息脱敏不失为一个折中的好办法。
Spring Boot使用Sanitizer(中文意思:消毒杀菌剂)来进行脱敏。比如属性配置有如下配置:
mysql.password = 123456 redis.pwd = 654321
这时候访问端点/actuator/env,得到的结果是这样子的:
如图所示,感觉有点厚此薄彼有木有???其实一切事出有因,EnvironmentEndpoint使用Sanitizer进行脱敏处理,而它自带一些默认行为:
若不再这个范围内的key(比如上面的redis.pwd)也需要脱敏,很简单,价格配置项即可:
management.endpoint.env.additional-keys-to-sanitize = redis.pwd #management.endpoint.env.additional-keys-to-sanitize = pwd # 脱敏范围更大
效果如下:
完美脱敏!!!这么做可以搞定绝大部分场景,但是某些特殊情况下,通过这种配置不是很好做,比如:同一个key,在不同的属性源里表现不一样。在application.properties里的话脱敏,而在application-dev.properties里不需要脱敏(开发环境嘛,明文裸奔更有助于调试程序)。
这个case若适用上面配置的方式不可处理,确切点说很不方便吧。Spring Boot意识到了这个“难点”,在2.6.0版本了新增了更灵活的自定义脱敏规则的能力,做法很简单:自定义SanitizingFunction类型的Bean即可。
// Since: 2.6.0 @FunctionalInterface public interface SanitizingFunction { SanitizableData apply(SanitizableData data); }
比如关于Redis的配置项放redis.properties文件里,然后读进来:
@PropertySource("classpath:redis.properties") @Configuration(proxyBeanMethods = false) public class AppConfiguration {} redis.properties文件内容: redis.pwd = 654321
要求:redis.properties文件里面所有包含pwd的key的值都做脱敏处理,而其它属性源不管。这时使用上面配置方式就无法实现了(或者说很难实现吧),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端点,结果如下:
✌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,这也是本次版本升级的一大特征之一。代码上体现在这里:
// 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自动开启连接池
现在,只要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领域可谓已有绝对的话语权,因此它开始引领:默认不允许循环引用。