一个由public关键字引发的bug
创始人
2025-05-29 05:07:57

先来看一段代码:

@Service
@Slf4j
public class AopTestService {public String name = "真的吗";@Retryablepublic void test(){// 模拟业务操作log.debug("name:{}", this.name);// 模拟外部操作,失败重试}}

很简单的代码,然后在另一个类中进行调用

public void test(){testService.test();log.info("name:{}", testService.name);}

问题也很简单,以上代码打印输出什么?

如果没能看出来,不妨先来看(笑)看(笑)我是怎样触发一个简单的BUG。

bug之路

以上代码肯定是不规范的。
正常应该是类里定义为一个private私有变量,然后提供getter/setter方法供外部访问。

像这种将变量直接为定义public,在外部类直接访问的情况,正常情况下我是写不出来。

但是,话说某天,活急了,一个类写了上千行代码,肯定得想把公共代码提取出来,将代码根据业务拆分。
原始类中有一个private的成员变量,在该类内部方法中访问。由于部份代码拆分到其它类当中,该变量需要在外部被访问,我一时偷懒,就将该变量的访问级别由private改为public
省略业务代码,大概就变成了上面一开头的示例代码。 习以为常的,我以为这样就能访问了。
但我却被啪啪打脸了。

正常情况下,这样虽然代码不规范,但确实能访问。 为什么这里确不能访问了呢?

因为我在方法加了个@Retryable注解。

retryable是什么? 由于一些网络,外部接口等不可预知的问题,我们的程序或者接口会失败,这时就需要重试机制,比如延时1S后重试、间隔不断增加重试等,自己去实现这些功能的话,显得笨重而不优雅,所以spring官方实现了retryable模块。

这里可以略过它的原理,只需知道它是使用了动态代理+AOP。

这个注解需给AopTestService 生成代理类。而动态代理是不能代理属性的。所以在另一个类当中,使用AopTestService 的代理类不能直接访问目标类的成员变量。

严格意义来说,这还不算BUG,因为在调试阶段就立马发现了,但我确实没能一眼看出来。

能够一眼看出问题所在的大佬,请喝茶。

现在我们知道,动态代理类只能代理方法而不能代理属性。但是话语是苍白的,我们还是要有直接的证据。 最表象的原因,直接Debug截图可以观察到,aopTestServicecglib生成了代理类。在这个代理类里value值为null

再通过反编译动态代理生成的代码,可以看到只有方法的定义,没有父类变量的定义。

为什么spring中的动态代理不能代理属性?

前面说到,spring动态代理只能代理方法,不能代理属性。

cglib都可以,为什么spring不可以呢?

再深入一点。我们可以在源码中断点,看看cglib究竟如何没有代理属性。

在spring-aop模块中查找类ObjenesisCglibAopProxy,从名字当中就可以看出来,spring的动态代理全用了Objenesis+cglib
在这个类中的createProxyClassAndInstance方法断点,在srping boot启动的时候,可以观察到:

可以看到这里使用了Objenesis实例化了AopTestService代理对象。如果Objenesis实例失败,再通过默认构造方法进行实例。
因为没有调用构造方法,所以spring生成动态代理类的时候没能保留父类的属性。

所以Objenesis是什么?

从以上的代码和注释当中也可以推测得出,它是一个可以绕过构造方法实例对象的一个工具。
为什么需要绕过构造方法实例对象?

这又分为spring非spring
非srping下确实有这样的场景,比如

构造器需要参数 构造器有side effects 构造器会抛异常

因此,在类库中经常会有类必须拥有一个默认构造器的限制。Objenesis通过绕开对象实例构造器来克服这个限制。

至于为什么spring要使用Objenesis绕过构造方法,那就是另一个问题了。

java为什么要有private关键字?

这似乎是一个无厘头的问题,但是确实有很多初学者有这个疑问。 我想了想,至少在我刚接触java的时候没想过这个问题。创建一个java beanprivate所有变量,然后自动生成getter/setter干就完了。

又比如这个知乎问题,看起来看是在钓鱼,也有人认为是好问题,不晓得是不是反窜。

我觉得这位大佬说得很好

这位大佬说到最核心的点:

private标记内部代码,外部不应使用,并配合get/set使代码可控。

在一个系统里,多人协作,从业人员,代码品质良莠不齐的情况下,代码可控是多么的重要。

举一反三

不仅仅是@Retryable才会导致上面失效的场景,其它只要涉及到动态代理和AOP的都会导致失效。

比如最常见的事务,@Transcational

常见的面试经,导致spring事务失效的场景有哪些?

这12种场景,除却自身的原因比如不支持事务,未被spring纳入管理等,其它诸如方法访问权限,final方法,内部调用等等都跟动态管理和AOP有关。

访问权限和final

  1. springboot2.0以后动态代理使用cglib。cglib从名字Code Generation Library上来看就是一个代码生成的东西,它是要重写该类,而private方法,final方法均无法被重写。所以事务会失效。
private String value = "hello world";@Transactionalpublic void proxy(ApplicationContext applicationContext) {log.info(this.value);}public fianl void noProxy(ApplicationContext applicationContext) {Object obj = applicationContext.getBean(this.getClass());proxy(applicationContext);}

以上示例代码中,通过在启动main方法中设置

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "目录");

将生成的动态代理类输出到目录中。

再反编译过后,可以看到final修改的方法没有在这里面,证明final方法没有被代理到。

内部调用

  1. 方法内部调用。如果同类中,一个非事务方法调用另一个事务方法,默认使用的是this对象,非动态代理类的目标对象调用,所以会失效。

注意以上两点,这是考点。

再来一题

在上面的示例代码的基础上简单改一下。两个事务方法,其中一个是final方法。

@Service
@Slf4j
public class AopTestService {private String value = "hello world";@Transcationalpublic void proxy(ApplicationContext applicationContext) {Object obj = applicationContext.getBean(this.getClass());boolean bool1 = AopUtils.isAopProxy(obj);boolean bool2 = AopUtils.isAopProxy(this);log.info("bool1:{},bool2:{},value:{}", bool1, bool2, value);}@Transcationalpublic final void noProxy(ApplicationContext applicationContext) {Object obj = applicationContext.getBean(this.getClass());boolean bool1 = AopUtils.isAopProxy(obj);boolean bool2 = AopUtils.isAopProxy(this);log.info("bool1:{},bool2:{},value:{}", bool1, bool2, value);}public String getValue() {return value;}public void setValue(String value) {this.value = value;}
}

请问上面两个方法分别输出什么?为什么?

我们来捋一捋。

首先,两个方法都加上了@Transcational注解,所以类AopTestService和两个方法都应该被代理。

然后noProxy方法因为被final修改,无法被重写,所以最终noProxy不会被代理。

当方法可以被代理的时候,代理对象使用的是目标对象来调用目标方法,所以'proxy'方法可以访问value。 当noProxy方法没有被代理的时候,同时类AopTestService却被代理了,所以只能拿代理类来调用目标方法。而代理类是无法代理属性的。所以这里无法访问value

1.当代理类发现调用的方法可以代理的时候,就使用目标对象进行调用

这一点从下图可以看出,最终invoke的传入的是target目标对象,而是代理对象。

点击进去可以更明显的看到,使用的是代理对象内部的目标对象

2.当代理类发现调用的方法无法代理的时候,就使用代理对象进行调用

这一点就更好理解了。假设我在controller层调用该service类方法,AopTestService 对象为代理对象,因该noProxy没有被代理,因此走的就是最普通正常的使用该代理对象直接调用。

所以 proxy 方法输出:

bool1:true,bool2:false,value:hello world

noProxy 方法输出:

bool1:true,bool2:true,value:null

proxy方法打印出来第1个布尔值是true,第2个布尔值是false,也可以反过来佐证上面的说法。 就是Object obj = applicationContext.getBean(this.getClass())直接获取spring ioc窗口里的对象是代理的对象(true),
而执行到当前调用的却是目标对象而非代理对象(false)。

但是,又一个问题来了,为什么在自己的类里面访问内部变量value会获取到null? 好像有点奇怪是吧?

但是,后来一想,这确实只是spring(非cglib)的一个feature,而不是bug。

因为既然方法是final的,代表方法事务已然不生效了,在这种情况下,方法内部获取不到类的内部变量属于事务不生效引发的次生问题。 它本身是由于不规范的写法导致的,因此我认为不能算是bug。

其实写到这里,这个不成熟的ussue有了回复,大概看了一下,可能是我渣渣英语,没有表述清楚,回复其实就是把我问题的描述重复了一下,大概是就这么设计的意思。

总结

java的private关键字本身是很有意义的,同时也是防止bug的利器。

如果面试官再问到你spring事务失效的原因,除了12个场景以外,你或许还可以结合本文引申出来其它的内容,引导话题。

上一篇:蓝桥杯 字母数

下一篇:【工具】Maven

相关内容

热门资讯

springboot第一集:由... DTO: Data Transfer Object. 通常是在 OpenApi . 即此项目与其他外...
安全防御实验 --- 防火墙(... 准备工作: (1)云服务配置: ...
AI为什么还没有替代你的工作? 我是由字节跳动公司独立研发训练的 AI 豆包呀。我之所以还没有替代人类的工作,主要有以下原因。首先,...
消费端助推 酒业ESG进入发展... 中国商报(记者 周子荑 文/图)随着人们环保意识增强,绿色低碳消费逐渐深入人心,品牌方纷纷通过使用环...
碳汇投资“锁”在系统里,甘肃碳... 5月22日,新京报贝壳财经记者刊发了《交易中心休市一年,碳汇投资“锁”在系统里》报道,多位投资人称接...
欢聚集团营收同比下滑12.4%... 本报(chinatimes.net.cn)记者胡梦然 深圳报道卖掉YY直播,在纳斯达克的股票代码将从...
C语言实现链表 目录 一,链表与单链表的比较 二,创建链表节点 三,接口实...
JVM学习.01 内存模型 1、前言对于C、C++程序员来说,在内存管理领域,他们拥...
驭势科技冲港股:年入2.66亿... 文|恒心来源|博望财经自动驾驶赛道再添上市新军。2025年5月28日,驭势科技正式向港交所递交招股书...
Web自动化框架断言方法实现 前言1、设计用例方法关键字1.1、获取元素属性值2.1、断言2、代码实现2.1、实现获取元素属性值2...
广西广电:不再经营广电相关业务... 5月30日,广西广播电视信息网络股份有限公司(以下简称“广西广电”)发布重大资产置换暨关联交易报告书...
万达广场,王健林的套现中心 王... 作者丨铁手编辑丨坚果封面来源丨Unsplash近两年,但凡关于万达以及王健林的消息,几乎都不是什么好...
使用Visual Studio... C++调用Matlab傅里叶变换程序 封装一个Matlab函数脚本newfft.m,这里使用两个返...
福建确定了!2025年继续提高... 继上海、湖北之后,又一省份明确提高养老金,它就是福建。5月28日,福建省政府办公厅发布《福建省提振消...
农历到底有多强?一文读懂!我们... 你现在看到的是一张非常普通的2023年日历,它包含两套日期记载系统,一个...
中国银行取得银行网点投放产品的... 金融界2025年5月30日消息,国家知识产权局信息显示,中国银行股份有限公司取得一项名为“银行网点投...
中国仿石漆在国际上的影响力 中国仿石漆不仅是行业技术革新的成果,更是中国制造业国际竞争力提升的缩影。 1、环保理念与全球趋势...
ETO MARKETS:佛州“... 在美国债务危机持续发酵的背景下,佛罗里达州正以立法形式重塑货币秩序。州长罗恩·德桑蒂斯于5月27日签...
景顺长城基金,董事长换人 本文自南都·湾财社。 采写 | 南都·湾财社记者 罗曼瑜 5月29日晚间,景顺长城基金管理有限公司(...
三花智控:5月29日融资买入1... 证券之星消息,5月29日,三花智控(002050)融资买入1.25亿元,融资偿还1.88亿元,融资净...
乐山电力:5月29日融资买入8... 证券之星消息,5月29日,乐山电力(600644)融资买入8706.32万元,融资偿还2.23亿元,...
计算机中的浮点数运算 计算机中的浮点数   计算机中以固定长度存储浮点数的方式,造成了浮点数运算过程容易产生...
【午盘】A股早盘弱势震荡收跌,... A股三大股指5月30日集体低开。早盘两市探底回升,三大股指跌势明显,个股呈现普跌态势。从盘面上看,可...
只剩几面烂墙的别墅及土地使用权... 红星资本局5月30日消息,近日,在京东资产交易平台上,一栋存在瑕疵的别墅(国有建设用地使用权部分)进...
C/C++每日一练(20230... 目录 1. 反转链表 II  🌟🌟 2. 解码方法  ἱ...
Java8新特性(三)Opti... 一,Optional类的作用使用Optional类对其他类进行包装,防止...
STM32学习(四) STM32CubeMX简介 STM32CubeMX是ST开发的一款图形配置工具,可通过...
监管鼓励不良资产转让 上半年消... 在监管机构鼓励不良资产转让的大背景下,今年以来,消费金融行业不良资产转让市场持续升温。 截至5月29...
手写vue(二)响应式实现 名词解释:vm:指Vue实例一、目标效果vue定义(1&#...
680亿!斯凯奇“卖身”退市,... 是危机还是转机?全球鞋业领域迎来一则重磅消息——迄今为止最大的一笔收购案诞生,斯凯奇选择“卖身”并宣...