写点什么

一个由 public 关键字引发的 bug

作者:小小怪下士
  • 2023-03-16
    湖南
  • 本文字数:3725 字

    阅读完需:约 12 分钟

先来看一段代码:


@Service@Slf4jpublic 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 截图可以观察到,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";
@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方法没有被代理到。


内部调用

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


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


再来一题

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


@Service@Slf4jpublic 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 个场景以外,你或许还可以结合本文引申出来其它的内容,引导话题。

用户头像

还未添加个人签名 2022-09-04 加入

热衷于分享java技术,一起交流学习,探讨技术。 需要Java相关资料的可以+v:xiaoyanya_1

评论 (1 条评论)

发布
用户头像
记得加个原创
2023-03-16 22:56 · 广东
回复
没有更多了
一个由public关键字引发的bug_Java_小小怪下士_InfoQ写作社区