当前位置: 首页 > 科技观察

SpringBoot2.6.0正式发布,循环引用终于被禁止

时间:2023-03-13 20:49:49 科技观察

前言北京时间2021-11-17,SpringBoot2.6.0正式发布。回想一下,最后一次发布是最后一次。与打酱油的2.5.0版本相比,这次的升级点更加激进。2.5.0版本的新特性在这里:【方向盘】SpringBoot2.5.0正式发布,为环境变量指定前缀的功能很棒)说明:SpringBoot2.6.1作为补丁版本发布立即解决了几个问题。所以请继续习惯它,并请尽量在生产中保持最新(小)版本。【方向盘】-SpringBoot新特性相关下载【本专栏源码】:https://github.com/yourbatman/FXP-java-ee【技术专栏源码大本营】:https://github.com/yourbatman/tech-column-learning[NuwaKnife-InitializrProject]访问地址:http://152.136.106.14:8761[程序员专用网盘]公益上线了,注册送1G超小容量,求助你练减法:https://wangpan.yourbatman.cn[Java开发软件包(Mac)]:https://wangpan.yourbatman.cn/s/rEH0提取码:javakit版本协议SpringBoot2.6.0文中有关版本号,版本号从2.4.x版本开始没有.RELEASE后缀!用表格来描述目前SpringBoot各个版本的更新维护情况:SpringBoot每年5月5日发布1月和11月发布两个中型版本(一般会有一些不兼容,升级需要请谨慎),每个中型版本提供1年支持(免费)和2年+商业支持(付费)。按照这个节奏,我们可以看到SpringBoot2.6.0的发布也宣布了2.4.x版本将停止(免费)支持,2.7.0版本有望在2022年5月与大家见面。2.6版本的主要新特性禁止循环引用。毕竟SpringBoot也没办法,禁止(Bean的)循环引用!!!注意:SpringBoot默认只是禁止,但SpringFramework默认还是允许的。对于代码整洁的开发者而言说到代码,看到代码中的循环引用是“不舒服的”。在业务开发中,有一种声音认为:循环引用是不可避免的,但其实你应该想:如果出现循环引用,那一定是结构设计不合理造成的,还有优化的空间!如果你是一个有追求的程序员,是很容易发现这种不合理性的。什么是循环引用?如图所示,循环引用一般是指A引用B,B引用A。更极端的循环引用情况可以是:A引用A,本文以此为例进行代码演示。什么是循环依赖?它是循环引用的一种具体形式,比如SpringBeans之间的循环依赖就是循环引用。在大多数情况下,循环依赖和循环引用在语义上可以被认为是相同的。SpringBoot场景下,准备Bean循环依赖的基础代码:.cn*@date2021/12/1120:43*@since0.0.1*/@ServicepublicclassAService{@AutowiredprivateAServiceaService;@PostConstructprivatevoidinit(){System.out.println("循环依赖:"+(this==aService));}}2.6.0之前的版本(以2.5.x为例)org.springframework.bootspring-boot-starter-parent2.5。7启动SpringBoot应用,控制台输出:结果:正常启动。这就是我们口头上常说的:Spring解决了Bean循环依赖问题2.6.0及以后的版本org.springframework.bootspring-boot-starter-parent2.6.0启动SpringBoot应用,控制台输出:Result:Failedtostart。这是从SpringBoot2.6.0开始禁止循环引用的结果。如何解决循环引用?文中提到,循环引用是一种不合理的设计,但并不能正常工作。就像每个抱怨狗屎山代码的程序员都能正常工作一样,原因都是一样的:不好,但是有道理。既然“不合理”,就有理由避免。总结一下,循环引用的解决方案主要有两种:确保循环引用不再存在:整改/优化业务逻辑允许循环引用:无需更改代码解决方案一:确保循环引用不再存在好,太好了!难,这个难!这个解决方案是最好的,也是最困难的。当然,Spring团队最喜欢你来做。做困难的事,你就会有所收获!这种从SpringBoot2.6.0开始的默认行为(不允许循环引用)我能感觉到循环引用的编码方式是不推荐的,而且是难闻的代码。为此,希望正在看这篇文章的码农给自己立个flag:不再写循环引用的代码,尽力而为😄。然而,好的事情/解决方案一般很难实现,循环引用也是如此。在笔者看来,难点主要在于程序员自身,主要在于这三点:缺乏思考。看似提需求就开工效率很高,但实际上往往适得其反。这就是短期利益和长期利益的PK。短期利益更有诱惑力,但长期利益价值更高,不够追求。他知道这样做不好,但他还是做了。克服困难就像打怪升级一样。只有通过关卡才能提升上限。从A点到B点,如果距离只有10m,步行是最快的方式;如果是1km,自行车最好;超过10km就选择汽车;超过1000km就选择火车/飞机!妥协而不是最佳实践。在SpringBoot2.6.0之前,不需要担心这个问题(默认允许循环引用)。如果你打算使用2.6.x但现实仍然必须允许循环引用,你应该怎么做?现实是什么?如:旧项目升级SpringBoot版本需要保持向后兼容;公司码农水平不一,强制性的高标准要求会严重影响生产效率等,为此只有一个办法:禁用默认行为(允许循环引用)。具体方法也很简单。其实SpringBoot在启动失败的错误详情一文中已经很贴心的告诉你了:所以只需要在配置文件application.properties中添加这个属性:spring.main.allow-circular-references=true即可启用应用再次SpringBoot2.6.0版本:正常启动。除了添加属性的方法外,还可以通过启动类API来设置,可以达到同样的效果:publicstaticvoidmain(String[]args){newSpringApplicationBuilder(Application.class).allowCircularReferences(true)//允许循环references.run(args);}我们知道,是否允许循环引用其实是SpringFramework的能力,而SpringBoot只是将其暴露为一个属性参数,供开发者控制。那么问题来了,如果是纯Spring框架构建的应用,如何禁止循环引用呢?你知道怎么做吗?欢迎在留言区讨论解答,或者私聊我一起讨论学习~加餐:允许循环引用但是还是报错可能你一直认为Spring已经解决了循环引用的问题,所以你可以使用它“毫无顾忌”。不,有些“特殊”的场景可能还是会碰壁,问题还是比较隐蔽,难以定位。如果你不相信我,我将这个场景一步一步地描述给你:说明:下面的代码是在允许循环引用的SpringBoot中。演示在场景中运行基础代码:本例中使用@PostConstruct模拟触发方法调用,效果与调用Controller中的Service方法是一样的。@ServicepublicclassAService{@PostConstructprivatevoidinit(){StringthreadName=Thread.currentThread().getName();System.out.printf("线程号为%s,开始调用业务fun方法\n",threadName);fun();}publicvoidfun(){StringthreadName=Thread.currentThread().getName();System.out.printf("线程号为%s,开始处理业务\n",threadName);}}开始应用并触发动作,控制台输出为:线程名为main,开始调用业务fun方法,线程名为main,fun方法开始完美处理业务!此时你发现fun方法执行时间过长,需要做异步处理。你马上想到用Spring提供的@Async注解来搞定:@Asyncpublicvoidfun(){...}再次运行,控制台输出:线程名为main,开始调用业务fun方法线程名为main,而fun方法开始处理业务是什么?不生效!这时候,你就有了主意。原因是打开这个模块没有用。于是你赶紧使用@EnableAsync注解开启Spring的异步模块,满怀期待地再次运行应用。控制台输出:线程名为main,业务fun方法启动时线程名称为main,fun方法开始处理业务whata...?为什么还是不行。你挠了挠头,想起之前踩过的“交易不生效的坑”。场景和这个差不多,于是你模仿,采用了同样的方法解决:injectyourself(circulardependency)@AutowiredprivateAServiceaService;//injectyourselfMyown@PostConstructprivatevoidinit(){...aService.fun();//通过代理对象调用,而不是这个调用}这一次我满怀信心地跑了一遍,没想到在org.springframework.beans.factory启动的时候抛出了BeanCurrentlyInCreationException。BeanCurrentlyInCreationException:Errorcreatingbeanwithname'AService':Beanwithname'AService'hasbeeninjectedintootherbeans[AService]initsrawversionaspartofacircularreference,buthaseventuallybeenwrapped.Thismeansthatsaidotherbeansdonotusethefinalversionofthebean.Thisisoftentheresultofover-eagertypematching-considerusing'getBeanNamesForType'withthe'allowEagerInit'flagturnedoff,forexample.atorg.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:649)~[spring-beans-5.3.13.jar:5.3.13]...异常关键字:循环引用!!!你不同意允许循环引用吗?怎么胖四?怎么破???至此,作者抛出了这个疑问。有兴趣的同学可以想想问题的根源和解决办法。最后的效果应该是不同线程异步执行的:线程名为main,线程名为业务fun方法,线程名为task-1,fun方法开始处理业务。叙事,有兴趣的可以自行转发!!!主动学习😄更灵活的自定义脱敏规则对于/env和/configprops这两个端点,经常会有敏感信息,比如:数据库密码等,为了避免敏感信息泄露,一般的做法是禁用这两个端点,但是粒度过于粗糙,在很多情况下是不合适的,因为它可能会大大增加调试程序和定位问题的复杂度,所以对端点进行一些信息脱敏是一个很好的折衷。SpringBoot使用Sanitizer(中文意思:消毒剂)进行脱敏。比如属性配置有如下配置:mysql.password=123456redis.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中不需要脱敏(在开发环境中,明文裸奔更有利于调试程序)。如果这种情况适用于上述配置方法,则无法处理。准确的说,是很不方便。SpringBoot意识到了这个“困难”。在2.6.0版本中,它增加了更灵活的自定义清理规则的能力。方法很简单:自定义SanitizingFunction类型的Bean即可。//Since:2.6.0@FunctionalInterfacepublicinterfaceSanitizingFunction{SanitizableDataapply(SanitizableDatadata);}比如关于Redis的配置项放在redis.properties文件中,然后读入:@PropertySource("classpath:redis.properties")@Configuration(proxyBeanMethods=false)publicclassAppConfiguration{}redis.properties文件内容:redis.pwd=654321要求:redis.properties文件中所有包含pwd的键值都进行脱敏处理,其他属性源忽略。这时候就无法使用上面的配置方式(或者很难实现),SpringBoot2.6.0的新特性,API方式可以非常灵活方便:@BeanpublicSanitizingFunctionpwdSanitizingFunction(){returndata->{org.springframework.core.env.PropertySourcepropertySource=data.getPropertySource();Stringkey=data.getKey();//只对redis.properties中的部分key进行脱敏if(propertySource.getName().contains("redis.properties")){if(key.equals("redis.pwd")){returndata.withValue(SANITIZED_VALUE);}}returndata;};}再次请求/actuator/env端点,结果如下:SpringMVC默认使用了一种新的匹配策略在SpringFramework5之前,路径匹配的方式一直只有一种:Ant风格的url匹配,也就是大家熟悉的AntPathMatcher。5.0版本后引入了一个新的路径匹配器:PathPattern。它们是什么意思,如何使用,有什么区别,不是本文的重点。作者之前的文章有详细的介绍,推荐阅读。这里有直达电梯:Spring5的新宠:PathPattern、AntPathMatcher:那我去?SpringBoot从2.0.0版本开始就构建在SpringFramework5之上,但是并没有完全改变SpringMVC从2.6.0版本的默认匹配,将AntPathMatcher切换为PathPattern,这也是本次的一大特色版本升级。代码体现在这里://2.5.7publicstaticclassPathmatch{privateMatchingStrategymatchingStrategy=MatchingStrategy.ANT_PATH_MATCHER;}//2.6.0publicstaticclassPathmatch{privateMatchingStrategymatchingStrategy=MatchingStrategy.PATH_PATTERN_PARSER;}如果你只需要担心匹配方法(比如返回Ant兼容)只需添加一行简单的配置:spring.mvc.pathmatch.matching-strategy=ant-path-matcherRedis自动开启连接池现在,只要类路径中存在commons-pool2jar,它就会自动开启Redis的连接池(包括Jedis和Lettuce哦)。在2.6.0之前的版本中,配置Redis时是否开启连接池由用户显示决定。现在是自动的,说明SpringBoot在使用Redis时推荐使用连接池。从源码来看,主要区别就在这里(以现在比较常用的Lettuce为例):LettuceConnectionConfiguration以下代码是2.6.0版本的变化:可以看到策略发生了变化:连接池默认关闭前,需要显示为Enabled。2.6.0之后默认开启,需要显示才能禁用。SpringBoot2.4.x根据SpringBoot当前版本规则停止维护:官方仅免费维护当前主线版本和次版本。新版本发布后,老版本自然会停止维护,迫使开发者不断升级,使用新版本的产品,享受技术红利!说明:这里的暂停维护是指官方免费维护,不包括商业付费维护和依赖升级。一般不需要太在意这部分,暂时保留主要组件版本即可。SpringData2021.1SpringKafka2.8ApacheKafka3.0(Spring真的站在了最前沿)CommonsPool2.11Elasticsearch7.15Hibernate5.6Mockito4.0...删除和弃用按照规定在SpringBoot2.4.0中标记为弃用@Deprecated类将在此版本中删除。还记得2.4.0版中弃用的内容吗?SpringBoot2.4.0最大的升级是ConfigFileApplicationListener的升级。电梯直达:SpringBoot2.4.0正式发布,新的配置文件加载机制(不向后兼容)可以在下个版本(即2.6.0版本)去除,但是Spring团队这次,我还是担心步幅太大拖丹,所以保留了改了说3.0去掉了。弃用的类:JDBC的AbstractDataSourceInitializer系统,使用DataSourceScriptDatabaseInitializer系统替换Hibernate的SpringPhysicalNamingStrategy,使用CamelCaseToUnderscoresNamingStrategy替换测试框架的AbstractApplicationContextRunner类的几个方法,使用新的RunnerConfiguration类替换官网新的SUPPORT选项卡,由于SpringBoot的更新迭代速度非常快,各个版本的发布时间和维护周期一直困扰着广大开发者。为此,随着2.6.0版本的发布,官网还热情的提供了一个SUPPORT标签来展示各个版本的状态:以及当天的状态:地址:https://spring.io/projects/spring-boot#support总结SpringBoot2.6.0还是有很多更新点的,值得肯定,当然也值得升级。虽然Java领域的云原生时代受到了挑战,但毫无疑问,在未来5年甚至10年内,SpringBoot仍将是云原生应用的标准脚手架和基础设施。它的能力可以解放开发人员的精力,把时间花在业务设计和开发上。最后,我想再分享一句话。笔者觉得每次版本升级都符合Spring的决策理念:先服从,后引领。毕竟对于庞大的Spring系统来说,每一个重要的决定都不仅仅是一记耳光,更需要一个宏观的思想作为背后的指引。以循环引用为例,SpringFramework最初默认允许循环依赖:设计上似乎留下了“不和谐”,但后来Spring初出茅庐,话语权不够,所以拥抱大众生存是第一位的优先事项。Spring技术栈的开发现在已经成为实际的开发标准。可以说在Java领域拥有绝对的话语权,于是开始引领:默认不允许循环引用。【编辑推荐】HarmonyOS官方战略合作共建-HarmonyOS技术社区万字长文|LinuxC/C++后台服务器开发学习路线Log4j史诗级漏洞,我们小公司怎么办?SpringBoot官方处理Log4j2注入漏洞指南八个Python实用技巧,简单易用,让你半年都不用受苦iOS15.2准正式版,今日正式发布!加入四大更新