【方向盘】Spring Boot 2.6.0正式发布,循环引用终于被禁

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 【方向盘】Spring Boot 2.6.0正式发布,循环引用终于被禁

✍正文


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


image.png

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默认还是允许的哦


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


什么是循环引用?


image.png


如图,循环引用一般指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应用,控制台输出:


image.png


结果:正常启动。这便是我们口头上常说的: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应用,控制台输出:


image.png



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


如何解决循环引用?


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


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


  1. 确保循环引用不再存在:整改/优化业务逻辑
  2. 允许循环引用:无需改代码


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

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


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


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


  1. 思考不足。提起需求就开工看起来效率很高,实则往往相反
  2. 眼光不远。这是短期利益和长期收益的PK,短期利益更具诱惑性,然而长期收益才具备更高价值
  3. 追求不够。明明知道这么做不太好,但就是这么做了。克服困难好比打怪升级,过关斩将方能提高自己的上限



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


方案二:允许循环引用


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


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


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


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


image.png


所以只需在配置文件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,得到的结果是这样子的:


image.png


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


image.png


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


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


效果如下:


image.png


完美脱敏!!!这么做可以搞定绝大部分场景,但是某些特殊情况下,通过这种配置不是很好做,比如:同一个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端点,结果如下:


image.png


✌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


image.png


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


image.png


image.png


image.png


可以看到策略是有变化的:之前默认关闭连接池需要显示开启,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版本弃用了哪些?

image.png


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


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


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


image.png


弃用类:


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


✌官网新增SUPPORT标签页


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


image.png


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


image.png


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


✍总结


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


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


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


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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
160 0
|
7月前
SpringBoot+Mybatis-Plus+PageHelper分页+多条件查询
SpringBoot+Mybatis-Plus+PageHelper分页+多条件查询
181 0
|
1月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
41 2
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
70 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
75 2
|
6月前
|
运维 Java 关系型数据库
Spring运维之boot项目bean属性的绑定读取与校验
Spring运维之boot项目bean属性的绑定读取与校验
57 2
|
6月前
|
存储 运维 Java
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
Spring运维之boot项目开发关键之日志操作以及用文件记录日志
68 2
|
6月前
|
Java Maven
springboot项目打jar包后,如何部署到服务器
springboot项目打jar包后,如何部署到服务器
447 1
|
6月前
|
XML 运维 Java
Spring运维之boot项目打包jar和插件运行并且设置启动时临时属性和自定义配置文件
Spring运维之boot项目打包jar和插件运行并且设置启动时临时属性和自定义配置文件
57 1
|
6月前
springboot2.4.5使用pagehelper分页插件
springboot2.4.5使用pagehelper分页插件
169 0