一个由public关键字引发的bug
先来看一段代码:
@Service
@Slf4j
public class AopTestService {
public String name = "真的吗";
@Retryable
public 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截图可以观察到,aopTestService
由cglib
生成了代理类。在这个代理类里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 bean`,`private`所有变量,然后自动生成`getter/setter`干就完了。
又比如这个知乎问题,看起来看是在钓鱼,也有人认为是好问题,不晓得是不是反窜。
我觉得这位大佬说得很好
这位大佬说到最核心的点:
private标记内部代码,外部不应使用,并配合get/set使代码可控。
在一个系统里,多人协作,从业人员,代码品质良莠不齐的情况下,代码可控是多么的重要。
举一反三
不仅仅是@Retryable
才会导致上面失效的场景,其它只要涉及到动态代理和AOP的都会导致失效。
比如最常见的事务,@Transcational
。
常见的面试经,导致spring事务失效的场景有哪些?
这12种场景,除却自身的原因比如不支持事务,未被spring纳入管理等,其它诸如方法访问权限,final方法,内部调用等等都跟动态管理和AOP有关。
访问权限和final
- springboot2.0以后动态代理使用cglib。cglib从名字
Code Generation Library
上来看就是一个代码生成的东西,它是要重写该类,而private方法,final方法均无法被重写。所以事务会失效。
private String value = "hello world";
@Transactional
public 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方法
没有被代理到。
内部调用
- 方法内部调用。如果同类中,一个非事务方法调用另一个事务方法,默认使用的是this对象,非动态代理类的目标对象调用,所以会失效。
注意以上两点,这是考点。
再来一题
在上面的示例代码的基础上简单改一下。两个事务方法,其中一个是final方法。
@Service
@Slf4j
public class AopTestService {
private String value = "hello world";
@Transcational
public 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);
}
@Transcational
public 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
?
有点奇怪是吧?
因此,我给官方提了个issue:
https://github.com/spring-projects/spring-framework/issues/30102
但是,后来一想,这确实只是spring(非cglib)的一个feature
,而不是bug。
因为既然方法是final的,代表方法事务已然不生效了,在这种情况下,方法内部获取不到类的内部变量属于事务不生效引发的次生问题。
它本身是由于不规范的写法导致的,因此我认为不能算是bug。
其实写到这里,这个不成熟的ussue有了回复,大概看了一下,可能是我渣渣英语,没有表述清楚,回复其实就是把我问题的描述重复了一下,大概是就这么设计的意思。
总结
java的private关键字本身是很有意义的,同时也是防止bug的利器。
如果面试官再问到你spring事务失效的原因,除了12个场景以外,你或许还可以结合本文引申出来其它的内容,引导话题。
一个由public关键字引发的bug的更多相关文章
- Spring 循环引用(一)一个循环依赖引发的 BUG
Spring 循环引用(一)一个循环依赖引发的 BUG Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环 ...
- 关于vector变量的size,是一个无符号数引发的bug。LeetCode 3 sum
class Solution { public: vector<vector<int>> threeSum(vector<int>& a) { vector ...
- C# 一个简单的秒表引发的窗体卡死问题
一个秒表程序也是我的一个心病,因为一直想写这样的一个东西,但是总往GUI那边想,所以就比较怵,可能是上学的时候学MFC搞出的后遗症吧,不过当我今天想好用Win Form(话说还是第一次写win for ...
- 安卓微信overflow-x overflow-y引发的bug
今天xgo文章图片页上线用微信扫页面发现一个bug,页面可以双击放大缩小. 找了半天原因,发现是图片描述设置了overflow-y引发的bug. 建议在微信场景里满屏显示不能滚动的页面里慎用overf ...
- QByteArray引发的bug
QByteArray引发的bug 在接收UDP数据的函数里,有如下代码片段 if(0x10 == data.size() && 0xCA == (unsigned char)data. ...
- Java中,一个存在十几年的bug...
今天,分享一个JDK中令人惊讶的BUG,这个BUG的神奇之处在于,复现它的用例太简单了,人肉眼就能回答的问题,JDK中却存在了十几年.经过测试,我们发现从JDK8到14都存在这个问题. 大家可以在自己 ...
- 让VS2010/VS2012添加新类时自动添加public关键字
在VS添加类别的时候,每次都需要添加public关键字,表示好麻烦. 但是可以避免这个麻烦的. 通过修改VS2010的ItemTemplate,可以避免这个麻烦. 修改方法如下: 1. 打开文件夹Mi ...
- 一个由IsPrime算法引发的细节问题
//******************************* // // 2014年9月18日星期四,于宿舍撰写 // 作者:夏华林 // //******************* ...
- 怎样才能提交一个让开发人员拍手叫好的bug单
怎样才能提交一个让开发人员拍手叫好的bug单 软件测试人员写得最多的文档就是测试用例和BUG,现在测试用例和BUG都没有标准的模板,每个公司使用的缺陷管理工具都有可能不一样,如果你换了一家公司就有可能 ...
- unity3d之public变量引发错误
public变量引发错误 在vs ide中怎么更改也无效 后来发现public里面的值一直不改变,手动改之.
随机推荐
- CF1557总结
CF1557总结 Codeforces Round #737 (Div. 2) 先看了 A .意思是要把序列分成两个子序列,使得两序列各自平均值的和最小,输出最小值,要求 \(O(n)\) .想半天然 ...
- 实验:笔记本电脑做桥接有线网络,笔记本通过wifi连手机热点,,硬件通过笔记本的有线网口上网
1.问题 我们需要做实验,将我们设计的W5500实验板上internet, 搭建环境比较麻烦. 1)学校上网需要先HTTP认证: 2)家里经常路由器固定位置,没有足够长的网线: 3)有时候需要临时搭 ...
- gui的服务器和vnc安装测试
为了OpenStack做连接准备,我们要准备企业中不常用到的gui桌面,和vnc连接去调试 然后开始我们的教程 yum grouplist 列出包组选择要安装的服务 systemctl stop za ...
- Mysql学习:2、mysql基本命令
1.下载安装好的mysql分为两种数据库: a.系统自带: b.用户自己创建: 2.基本命令: create database 数据库名称; // 创建数据库:记住后面加 分号 drop databa ...
- C++ push_back()函数应用
最近在学习Opencv,用C++写程序,做了一个虚拟画笔的项目,即通过摄像头采集视频图像信息,识别视频中的画笔,并画笔在空中的划痕显示在视频图像上.在进行到划痕显示的,由于视频是实时采集的,检测到的画 ...
- css3边框属性学习
1.boder-radius <!DOCTYPE html> <html> <head> <meta charset="utf-8" /& ...
- Ubuntu16.04配置网卡
设置步骤: 1.路由器插电后,电脑使用网线,连接无线路由器任一LAN口,注意两者单独连接,先不要连接宽带网线.打开电脑浏览器,在地址栏输入192.168.100.1. 在路由器的管理界面,输入路由器的 ...
- 一次讲清promise
此文章主要讲解核心思想和基本用法,想要了解更多细节全面的使用方式,请阅读官方API 这篇文章假定你具备最基本的异步编程知识,例如知道什么是回调,知道什么是链式调用,同时具备最基本的单词量,例如page ...
- 微信支付宝app支付回调参数
微信app支付回调通知参数: <xml><appid><![CDATA[wx9703cd*******]]></appid><attach> ...
- 4组-Alpha冲刺-6/6
一.基本情况 队名:摸鲨鱼小队 组长博客:https://www.cnblogs.com/smallgrape/p/15574385.html 小组人数:8人 二.冲刺概况汇报 组长:许雅萍 过去两天 ...