当Spring的Bean的成员变量为null时,想一想动态代理

1,810 阅读4分钟

写在前面

这是我第一次写文章,还是有一些想要说的话,只对内容感兴趣的小伙伴们请略过。
本人是在大连的日资企业做对日项目(懂的都懂,debuff拉满),近来感觉到技术成长陷入了瓶颈。需要一些外在因素来促进自己的进步。
因为没有实际使用场景+人懒话多,不太喜欢自己做一些项目,所以选择写一些文章(输出带动输入)。
主要内容是在平时解决疑难杂症的过程中,发现的值得思考的问题。

起因

在一个平(shi)平(he)无(mo)奇(yu)的工作日,组里的一个小老弟遇到了一个奇怪的问题叫我过去看看。 问题是Spring的Bean中成员变量,有个方法中调用会是null,而其他方法中没有问题。
这里使用的是SpringBoot 2.X,简单写一下伪代码

public interface Service {
    OutDtoA methodA(InDtoA in);
    OutDtoB methodB(InDtoB in);
    OutDtoC methodC(InDtoC in);
}

@Service
public class ServiceImpl implements Service {
    private Mapper mapper;
    ServiceImpl(Mapper mapper){
        this.mapper = mapper;
    }
    @Override
    OutDtoA methodA(InDtoA in) {
        return mapper.mapA(in);
    }
    @Override
    OutDtoB final methodB(InDtoB in){
        return mapper.mapB(in); // 执行到这里会发现mapper为null。methodA、methodC方法都没有问题。
    }
    @Override
    OutDtoC methodC(InDtoC in){
        return mapper.mapC(in);
    }
}

到这里聪明的小伙伴们已经发现问题了,由于小兄弟的失误,methodB前多了个final修饰。
但实际代码量和注释量很大,找了半天才发现这个final。当时也没多想,抱着试一试的态度删除了final运行了一下,发现问题解决了,到这里我不由得思考了一下,为什么会出现这种奇怪的现象呢。

分析问题

问题已经明确,是由于函数的final导致的,顺着final来思考一下。
如果函数加上final修饰会产生什么影响呢,基本就是子类无法继承这个函数。
而这个业务级别的Service没有子类,排除了业务代码的问题。突然灵光一闪,拍案而起,是CGLIB啊。

到这里需要一些java动态代理的知识,如果有不太了解的小伙伴,可以看一下站内大佬们的文章,写的都很好。
我会在下面简单介绍一下,不需要的朋友们可以略过

动态代理简介

动态代理是一种可以在不修改原代码的前提下,添加功能的设计模式。当前被广泛使用,Spring的AOP就是基于此完成的。
就Spring来讲,动态代理的实现方式有两种,一种为JDK动态代理,一种为CGLIB

JDK动态代理:
通过代理类来包裹被代理类,通过实现相同接口,替代被代理类
CGLIB:
通过创建被代理类的子类,并且拦截被代理类的方法调用,转移至代理类的相应方法,通过子类调用父类的方法,替代被代理类

梳理

顺着CGLIB的思路,简单梳理一下产生成员变量为null的现象。

OK流程:
①调用目标方法
②CGLIB代理类拦截,并调用代理类中对应方法
③正常处理

NG流程:
①调用目标方法
②由于目标方法被final修饰,无法被拦截,导致直接调用了目标方法
③Spring只为我们创建了代理类的实例,没有创建被代理类的实例,所以就没有调用被代理类的构造函数,成员变量为null

问题到此基本就结束了,但是在我的印象里,Spring中如果被代理类有接口的话,会默认使用JDK动态代理,而现象却不是,为此还需要进一步调查。

调查

翻看了大佬们的文章,发现Spring确实如此,而SpringBoot却不太一样。
先说结论:
①Spring根据被代理类是否有接口来判断,有接口就使用JDK动态代理,没有就使用CGLIB
②SpringBoot 2.0以前会默认使用JDK动态代理,而2.0以后会默认使用CGLIB(可以通过配置指定代理模式)
由于英语能力有限,在SpringBoot官方文档中没找到,就不贴官方说明了。 有兴趣的可以参考大佬们文章,会贴在最后。

到这里为止,所有的问题都能理的通。但是呢,正常JDK8之后的JDK动态代理处理效率会高于CGLIB,而SpringBoot团队又是为了什么退而求其次呢。
在翻看文章时发现了 SpringBoot的issue ,是关于动态代理的讨论。

简单来讲,官方为了更低的使用门槛 (JDK动态代理必须要使用接口) ,而使用了CGLIB。只要类与方法前没有final修饰,就适配了大部分的使用场景。
但是实际使用时需要注意是否引用了大量使用final的包,如果有的话就需要考虑更换代理方式了。

引用

记录一下对自己有帮助的一些文章,上述过程中有不明白地方的小伙伴们,可以看一下大佬们的文章(写的真好!)
如果有版权问题,请留言或者联系我,必删!
动态代理
Spring的动态代理