关于java:一个由public关键字引发的bug

1次阅读

共计 4600 个字符,预计需要花费 12 分钟才能阅读完成。

先来看一段代码:

@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 截图能够察看到,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
@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 个场景以外,你或者还能够联合本文引申进去其它的内容,疏导话题。

正文完
 0