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

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据