先来看一段代码:
@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
? 如同有点奇怪是吧?
然而,起初一想,这的确只是 spring(非 cglib)的一个feature
, 而不是 bug。
因为既然办法是 final 的,代表办法事务未然不失效了,在这种状况下,办法外部获取不到类的外部变量属于事务不失效引发的次生问题。它自身是因为不标准的写法导致的,因而我认为不能算是 bug。
其实写到这里,这个不成熟的 ussue 有了回复,大略看了一下,可能是我渣渣英语,没有表述分明,回复其实就是把我问题的形容反复了一下,大略是就这么设计的意思。
总结
java 的 private 关键字自身是很有意义的,同时也是避免 bug 的利器。
如果面试官再问到你 spring 事务生效的起因,除了 12 个场景以外,你或者还能够联合本文引申进去其它的内容,疏导话题。