先来看一段代码:

@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,由于在调试阶段就立马发现了,但我的确没能一眼看出来。

能够一眼看出问题所在的大佬,请喝茶。

一个由public关键字引发的bug


现在咱们知道,动态署理类只能署理办法而不能署理特点。可是言语是苍白的,咱们还是要有直接的依据。 最表象的原因,直接Debug截图能够观察到,aopTestServicecglib生成了署理类。在这个署理类里value值为null

一个由public关键字引发的bug

再经过反编译动态署理生成的代码,能够看到只需办法的界说,没有父类变量的界说。

一个由public关键字引发的bug


为什么spring中的动态署理不能署理特点?


前面说到,spring动态署理只能署理办法,不能署理特点。

cglib都能够,为什么spring不能够呢?

再深入一点。咱们能够在源码中断点,看看cglib终究如何没有署理特点。

在spring-aop模块中查找类ObjenesisCglibAopProxy,从姓名傍边就能够看出来,spring的动态署理全用了Objenesis+cglib
在这个类中的createProxyClassAndInstance办法断点,在srping boot发动的时分,能够观察到:

一个由public关键字引发的bug

能够看到这儿运用了Objenesis实例化AopTestService署理方针。假如Objenesis实例失利,再经过默许结构办法进行实例。
由于没有调用结构办法,所以spring生成动态署理类的时分没能保留父类的特点。


所以`Objenesis`是什么?

从以上的代码和注释傍边也能够推测得出,它是一个能够绕过结构办法实例方针的一个工具。
为什么需求绕过结构办法实例方针?

这又分为spring非spring
非srping下的确有这样的场景,比方

结构器需求参数 结构器有side effects 结构器会抛异常

因而,在类库中经常会有类有必要具有一个默许结构器的约束。Objenesis经过绕开方针实例结构器来克服这个约束。


至于为什么spring要运用Objenesis绕过结构办法,那便是另一个问题了。


java为什么要有private关键字?


这似乎是一个无厘头的问题,可是的确有很多初学者有这个疑问。 我想了想,至少在我刚触摸java的时分没想过这个问题。创建一个`java bean`,`private`所有变量,然后自动生成`getter/setter`干就完了。

又比方这个知乎问题,看起来看是在垂钓,也有人以为是好问题,不晓得是不是反窜。

一个由public关键字引发的bug

我觉得这位大佬说得很好

一个由public关键字引发的bug

这位大佬说到最核心的点:

private标记内部代码,外部不该运用,并合作get/set使代码可控。

在一个系统里,多人协作,从业人员,代码品质良莠不齐的情况下,代码可控是多么的重要。


举一反三


不仅仅是@Retryable才会导致上面失效的场景,其它只需涉及到动态署理和AOP的都会导致失效。

比方最常见的业务,@Transcational

常见的面试经,导致spring业务失效的场景有哪些?

一个由public关键字引发的bug

这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办法没有被署理到。

一个由public关键字引发的bug


内部调用


  1. 办法内部调用。假如同类中,一个非业务办法调用另一个业务办法,默许运用的是this方针,非动态署理类的方针方针调用,所以会失效。

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

一个由public关键字引发的bug


再来一题


在上面的示例代码的基础上简略改一下。两个业务办法,其间一个是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方针方针,而是署理方针。

一个由public关键字引发的bug

点击进去能够更明显的看到,运用的是署理方针内部的方针方针

一个由public关键字引发的bug

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