先看一段代码:@Service@Slf4jpublicclassAopTestService{publicStringname="Really";@Retryablepublicvoidtest(){//模拟业务运行log.debug("name:{}",this.name);//模拟外部操作,失败重试}}很简单的代码,然后在另一个类中调用它publicvoidtest(){testService.test();日志.info("名称:{}",testService.name);}问题也很简单,上面的代码打印出来的是什么?如果你看不出来,我们来看看(笑),看看(笑)我是如何触发一个简单的BUG的。bug路上的上述代码肯定是不规范的。通常情况下,应该在类中定义为private私有变量,然后提供getter/setter方法供外部访问。在直接定义变量为public,直接从外部类访问的情况下,我一般情况下是写不出来的。然而,有一天,赶时间的时候,一个类写了好几千行代码,我肯定是想把公共代码抽出来,按照业务拆分代码。原类中有一个private成员变量,在类的内部方法中访问。由于部分代码被拆分到其他类中,因此需要从外部访问该变量。偷懒了一会儿,把变量的访问级别从private改成了public。如果省略业务代码,很可能会变成上面开头的示例代码。我已经习惯了,我以为我可以这样访问。但是我被打脸了。一般情况下,代码虽然不规范,但确实是可以访问的。为什么我不能访问这里?因为我给方法加了@Retryable注解。什么是可重试?由于网络、外部接口等一些不可预知的问题,我们的程序或接口会出现故障。这时候就需要重试机制,比如延迟1S重试,增加间隔重试。如果我们自己实现这些功能,看起来很繁琐,也不优雅,所以spring官方实现了retryable模块。这里可以跳过它的原理,只知道它使用的是动态代理+AOP。这个注解需要为AopTestService生成一个代理类。动态代理不能代理属性。所以在另一个类中,使用AopTestService的代理类不能直接访问目标类的成员变量。严格来说这不算BUG,因为在调试阶段立马就发现了,但我真的是一眼看不出来。一眼就能看出问题的老大,请喝茶。现在我们知道动态代理类只能代理方法不能代理属性。但言辞苍白,我们还需要直接的证据。最表面的原因,如直接Debug屏幕截图中所观察到的,是aopTestService从cglib生成代理类。在此代理类中,该值为空。然后反编译动态代理生成的代码,可以看到只有方法定义,没有父类变量定义。为什么spring代理属性中的动态代理不行?前面说了spring动态代理只能代理方法,不能代理属性。cglib很好,为什么spring不行?再深入一点。我们可以停在源码上看看cglib怎么没有proxy属性。在spring-aop模块中找到类ObjenesisCglibAopProxy。从名字就可以看出,spring的动态代理使用的是Objenesis+cglib。在srping开机启动时可以观察到该类中的createProxyClassAndInstance方法断点:可以看到使用Objenesis实例化了AopTestService代理对象。如果Objenesis实例失败,它将通过默认构造函数实例化。因为没有调用构造函数,spring在生成动态代理类的时候没有保留父类的属性。那么什么是Objenesis?从上面的代码和注释也可以推断出它是一个可以绕过构造方法的实例对象的工具。为什么需要绕过构造函数实例对象?这又分为春季和非春季。non-srping下确实存在这样的场景,比如构造函数需要参数,构造函数有副作用,构造函数会抛出异常。因此,在类库中经常会有一个类必须有默认构造函数的限制。Objenesis通过绕过对象实例构造函数克服了这一限制。至于spring为什么使用Objenesis来绕过构造函数,那是另外一个问题了。为什么java有private关键字?这看似是个无厘头的问题,但确实有不少初学者有这样的疑问。想了想,至少我刚接触java的时候没有想过这个问题。创建一个javabean,私有所有变量,然后自动生成getter/setter就大功告成了。再比如知乎这个问题,好像是钓鱼,也有人觉得是个好问题,但不知道是不是反渠道。我觉得这位大佬说的很好。这位大佬说到核心点了:private标记内部代码,不应该对外使用,配合get/set让代码可控。在一个系统中,当多人协作、从业者、代码质量参差不齐时,代码的可控性是多么重要。从一个实例到另一个实例的推导不仅仅是@Retryable会导致上述失败场景,其他涉及动态代理和AOP的东西也会导致失败。比如最常见的事务,@Transcational。常见面试经验,导致spring事务失败的场景有哪些?这12个场景,除了自身原因如不支持事务、不被spring管理等,其他如方法访问权限、final方法、内部调用等都与动态管理和AOP有关。finalspringboot2.0之后的访问权限和动态代理使用cglib。从CodeGenerationLibrary这个名字来看,cglib就是一个代码生成的东西。就是重写这个类,private方法和final方法不能重写。所以交易会失败。私人字符串值=“你好世界”;@Transactionalpublicvoidproxy(ApplicationContextapplicationContext){log.info(this.value);}publicfianlvoidnoProxy(ApplicationContextapplicationContext){Objectobj=applicationContext.getBean(this.getClass());代理(应用程序上下文);上面的示例代码中,通过设置System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"directory");将生成的动态代理类输出到目录中。在启动主要方法中。反编译后可以看到final修改的方法不在里面,证明final方法没有被代理。内部调用方法内部调用。如果一个非事务方法调用同一个类中的另一个事务方法,默认使用this对象,调用的是非动态代理类的目标对象,所以会失效。注意以上两点,这是测试点。另一个问题是根据上面的示例代码简单地更改它。两种交易方式,其中一种是最终的。@Service@Slf4jpublicclassAopTestService{privateStringvalue="helloworld";@Transcationalpublicvoidproxy(ApplicationContextapplicationContext){Objectobj=applicationContext.getBean(this.getClass());booleanbool1=AopUtils.isAopProxy(obj);booleanbool2=AopUtils.isAopProxy(this);log.info("bool1:{},bool2:{},value:{}",bool1,bool2,value);}@TranscationalpublicfinalvoidnoProxy(ApplicationContextapplicationContext){Objectobj=applicationContext.getBean(this.getClass());booleanbool1=AopUtils.isAopProxy(obj);booleanbool2=AopUtils.isAopProxy(this);log.info("bool1:{},bool2:{},value:{}",bool1,bool2,value);}publicStringgetValue(){返回值;}publicvoidsetValue(Stringvalue){this.value=value;}}请问上面两个方法分别输入什么?为什么?我们来摸一摸。首先,这两个方法都用@Transcational注释,所以类AopTestService和这两个方法都应该被代理。那么noProxy这个方法就不能改写了,因为它被final修饰了,所以noProxy最终不会被代理。当方法可以被代理时,代理对象使用目标对象调用目标方法,因此“代理”方法可以访问该值。当不代理noProxy方法时,同时代理了类AopTestService,所以代理类只能用来调用目标方法。代理类不能代理属性。所以这里不能访问值。1.当代理类发现被调用的方法可以被代理时,使用目标对象进行调用。从下图可以看出,最终调用的是target目标对象,而是代理对象。点进去可以更明显的看出使用了代理对象内部的目标对象。2.当代理类发现被调用的方法无法被代理时,最好理解为使用代理对象来调用。假设我在controller层调用service类的方法,AopTestService对象就是代理对象,因为没有代理noProxy,所以最常见的方式就是使用代理对象直接调用。所以代理方法output:bool1:true,bool2:false,value:helloworldnoProxy方法output:bool1:true,bool2:true,value:null代理方法打印出来的第一个布尔值为true,第二个布尔值为假的,也可以反过来证明上面的说法。即Objectobj=applicationContext.getBean(this.getClass())直接获取到springiocwindow中的对象是代理对象(true),但是当前调用执行的是目标对象,而不是代理对象(错误的)。但是,又出现一个问题,为什么在自己的类中访问内部变量值会得到null呢?似乎有点奇怪,对吧?不过后来想想,这确实只是spring(不是cglib)的特性,并不是bug。因为既然方法是final的,就意味着方法的事务不再有效。在这种情况下,方法内部无法获取到类的内部变量是无效事务导致的次要问题。本身就是写的不规范造成的,所以我觉得不能算是bug。居然写到这里,这个不成熟的ussue有回复,大概看了,可能是我英文马虎,没表达清楚,回复其实是重复我的问题描述,大概是这样设计的.总结java本身的private关键字是很有意义的,也是防止bug的利器。如果面试官问你spring事务失败的原因,除了12种场景之外,你或许还可以根据本文提炼出其他内容来引导话题。
