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

文章讲述了在SpringBoot项目中,由于一个成员变量在某个final方法中为null,引发的问题。经过分析发现是final关键字阻止了CGLIB动态代理对方法的拦截,从而导致成员变量未初始化。SpringBoot2.x默认使用CGLIB动态代理,而非JDK动态代理,原因是CGLIB无需接口约束,降低了使用门槛,但可能在某些场景下效率较低。文章提醒开发者注意final的使用以及动态代理的选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1:起因

在一个平(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){
        // 执行到这里会发现mapper为null。methodA、methodC方法都没有问题。
        return mapper.mapB(in); 
    }
    
    @Override
    OutDtoC methodC(InDtoC in){
        return mapper.mapC(in);
    }
}

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

2:问题分析

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

3:动态代理简介

动态代理是一种可以在不修改原代码的前提下,添加功能的设计模式。当前被广泛使用,Spring的AOP就是基于此完成的。
就Spring来讲,动态代理的实现方式有两种,一种为JDK动态代理,一种为CGLIB。
JDK动态代理:
通过代理类来包裹被代理类,通过实现相同接口,替代被代理类。
CGLIB:
通过创建被代理类的子类,并且拦截被代理类的方法调用,转移至代理类的相应方法,通过子类调用父类的方法,替代被代理类。

4:梳理

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

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

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

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

5:调查

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

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

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

6:引用

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

动态代理
Spring的动态代理

文章转载

### Spring 中 Static 方法中 @Autowired 注入为 Null 的原因分析 在 Spring 框架中,`@Autowired` 是一种基于依赖注入的设计模式,用于自动装配 Bean 实例。然而,由于 Java 静态变量和静态方法的特点,Spring 容器无法直接将实例化的对象注入到 `static` 变量或方法中[^1]。 #### 原因解析 Java 的 `static` 关键字表示该字段或方法属于类本身而非某个特定的对象实例。而 Spring 的依赖注入机制是针对具体的对象实例进行操作的。因此,当尝试通过 `@Autowired` 将一个非静态的 Spring Bean 注入到静态变量,容器会因为生命周期不匹配而导致注入失败,最终表现为 `null`[^4]。 --- ### 解决方案 以下是几种常见的解决方案: #### 方案一:使用 `ApplicationContextAware` 可以通过实现 `ApplicationContextAware` 接口手动获取所需的 Bean 实例,并将其赋值给静态变量。 ```java import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class MyUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext context) { applicationContext = context; } public static YourService getYourService() { return (YourService) applicationContext.getBean("yourService"); } } ``` 这种方式允许你在任何地方调用 `MyUtil.getYourService()` 来获得所需的服务实例[^2]。 --- #### 方案二:使用 `@PostConstruct` 初始化静态变量 可以在非静态的方法上标注 `@PostConstruct`,利用此方法初始化静态变量。 ```java import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MyUtil { @Autowired private transient YourService yourService; // 使用 transient 避免序列化问题 private static YourService staticYourService; @PostConstruct public void init() { staticYourService = yourService; } public static void doSomething() { staticYourService.someMethod(); } } ``` 这种方法的核心在于通过实例方法完成静态变量的初始化工作。 --- #### 方案三:提供 Setter 方法并配合 `@Autowired` 如果不想修改现有逻辑,可以为静态变量定义一个 setter 方法,并在其上应用 `@Autowired` 注解。 ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MyUtil { private static YourService staticYourService; @Autowired public static void setStaticYourService(YourService yourService) { staticYourService = yourService; } public static void doSomething() { staticYourService.someMethod(); } } ``` 这种做法简单明了,适合于小型项目中的快速修复需求。 --- #### 方案四:避免使用 Static 方法 从根本上讲,尽量减少对静态方法的依赖是一种更好的设计实践。考虑重构代码结构,使业务逻辑更多地依赖于实例方法而不是静态方法。这样不仅可以充分利用 Spring 提供的功能特性,还能增强程序的可测试性和灵活性[^3]。 --- ### 总结 上述四种方法各有优劣,具体选择取决于实际应用场景以及团队的技术偏好。推荐优先采用 **方案一** 或者重新评估是否真的需要使用静态上下文环境下的依赖注入。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值