由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

Spring 静态注入实际案例 Demo

有时候我们想将某个实例 Bean 封装成一个工具类,方便复用,也可以直接放到接口中定义为 default 方法。初衷就是这个。结果封装的工具类引发了 NPE 异常。造成线上重大事故😭😭😭

>>>>>>话不多说,直接看代码案例😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭:

为什么这样写有时候 RemoteEBRpcInvoker.getEbFormIdUtil 是一个 NULL???

@Component
public class RemoteEBRpcInvoker {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private static PublishKitRuntimeUtil publishKitRuntimeUtil;
    private static GetEbFormIdUtil getEbFormIdUtil;

    @Autowired
    public RemoteEBRpcInvoker(GetEbFormIdUtil getEbFormIdUtil,
                              PublishKitRuntimeUtil publishKitRuntimeUtil) {
        RemoteEBRpcInvoker.getEbFormIdUtil = getEbFormIdUtil;
        RemoteEBRpcInvoker.publishKitRuntimeUtil = publishKitRuntimeUtil;
    }
    /**
     * 通过指定 appId 获取 RPC 服务实例
     *
     * @param tClass 要构建的 RPC 服务类
     * @param appId  应用 ID
     * @param <T>    服务类的类型
     * @return 构建后的 RPC 服务实例
     */
    public static <T> T newInstance(Class<T> tClass, Long appId) {
        Long appId = getEbFormIdUtil.getAppId(TagConstant.CS_APPID_TAG, tenantKey);
        return publishKitRuntimeUtil.buildRpcService(tClass, DEFAULT_RPC_GROUP, String.valueOf(appId));
    }
 }

就上面这段代码,在我们去调用 newInstance() 方法的时候,方法里面的 getEbFormIdUtil 竟然是个 NULL 对象。明显是我们这种写法 Spring 在静态注入的时候,直接没有引用成功。

所以在这种段代码中,“看似没问题”,实则 RemoteEBRpcInvoker.getEbFormIdUtil 可能为 null ,原因存在两种情况:

原因1: 静态变量初始化顺序问题

我正在使用静态变量 RemoteEBRpcInvoker.getEbFormIdUtilRemoteEBRpcInvoker.publishKitRuntimeUtil,而静态变量的生命周期和实例变量不同。@Autowired 注入的依赖通常在对象实例化时被注入,但静态变量属于类级别,不依赖于对象实例化。这可能导致在某些情况下(例如,依赖注入还未完成时)静态变量没有被正确赋值,导致它们为 null

原因2: Spring 生命周期与静态字段

Spring 依赖注入是基于对象实例的,静态变量不依赖于对象实例。@Autowired 只能保证在构造函数中注入实例变量,而不能确保静态变量的安全初始化。因为静态变量与类的生命周期绑定,而不是与实例的生命周期绑定,Spring 容器可能在静态字段初始化之前或之后管理类的生命周期,从而导致未能正确注入。

接下来是如何解决这个潜在问题方法介绍:

解决方案:

方法1:移除静态字段(违背了我的初衷)

避免将需要通过依赖注入获得的对象定义为静态字段,因为它们不依赖于实例。

你可以修改代码如下:

@Component
public class RemoteEBRpcInvoker {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private PublishKitRuntimeUtil publishKitRuntimeUtil;
    private GetEbFormIdUtil getEbFormIdUtil;

    @Autowired
    public RemoteEBRpcInvoker(GetEbFormIdUtil getEbFormIdUtil,
                              PublishKitRuntimeUtil publishKitRuntimeUtil) {
        this.getEbFormIdUtil = getEbFormIdUtil;
        this.publishKitRuntimeUtil = publishKitRuntimeUtil;
    }
}

这个方法虽然行,但是违背了我的初衷,我需要封装一个工具类,那么就需要将方法弄成 static 状态方法。

方法2:使用 @PostConstruct

如果需要使用静态字段,你可以利用 @PostConstruct 注解,在依赖注入完成后手动初始化静态变量。

@Component
public class RemoteEBRpcInvoker {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private static PublishKitRuntimeUtil publishKitRuntimeUtil;
    private static GetEbFormIdUtil getEbFormIdUtil;

    @Autowired
    public RemoteEBRpcInvoker(GetEbFormIdUtil getEbFormIdUtil,
                              PublishKitRuntimeUtil publishKitRuntimeUtil) {
        this.getEbFormIdUtil = getEbFormIdUtil;
        this.publishKitRuntimeUtil = publishKitRuntimeUtil;
    }

    @PostConstruct
    public void init() {
        RemoteEBRpcInvoker.getEbFormIdUtil = this.getEbFormIdUtil;
        RemoteEBRpcInvoker.publishKitRuntimeUtil = this.publishKitRuntimeUtil;
    }
}

这种方法可以确保在依赖注入完成后,静态变量被正确赋值。

除了我之前提到的解决方案之外,还有一些其他方法可以避免静态字段注入问题,并确保你能够在 Spring 环境中正确地使用静态字段。以下是其他几种可能的方法:

方法3: 使用 @Autowired 的静态方法

你可以通过编写一个静态的 setter 方法来为静态变量注入依赖。Spring 允许通过 @Autowired 注解静态方法来完成依赖注入。这种方式不会依赖于实例,因此更加符合静态变量的使用场景。

@Component
public class RemoteEBRpcInvoker {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private static PublishKitRuntimeUtil publishKitRuntimeUtil;
    private static GetEbFormIdUtil getEbFormIdUtil;

    @Autowired
    public static void setPublishKitRuntimeUtil(PublishKitRuntimeUtil publishKitRuntimeUtil) {
        RemoteEBRpcInvoker.publishKitRuntimeUtil = publishKitRuntimeUtil;
    }

    @Autowired
    public static void setGetEbFormIdUtil(GetEbFormIdUtil getEbFormIdUtil) {
        RemoteEBRpcInvoker.getEbFormIdUtil = getEbFormIdUtil;
    }
}

这种方法通过静态的 setter 方法来完成静态变量的注入,确保它们在类加载时能被正确初始化。

方法4: 使用 ApplicationContext 获取 Bean

另一种方法是使用 Spring 的 ApplicationContext 来手动获取所需的 Bean。这种方法可以在静态上下文中安全地使用依赖注入。

你可以通过以下方式将 ApplicationContext 注入,并在需要时手动获取依赖。

@Component
public class RemoteEBRpcInvoker implements ApplicationContextAware {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RemoteEBRpcInvoker.applicationContext = applicationContext;
    }

    public static GetEbFormIdUtil getGetEbFormIdUtil() {
        return applicationContext.getBean(GetEbFormIdUtil.class);
    }

    public static PublishKitRuntimeUtil getPublishKitRuntimeUtil() {
        return applicationContext.getBean(PublishKitRuntimeUtil.class);
    }
}

在这种方法中,ApplicationContext 被注入到静态字段 applicationContext 中,并通过静态方法从上下文中获取所需的 Bean。这避免了直接使用 @Autowired 注解静态字段的问题。

方法5: 使用单例模式和手动注入

如果你确实需要使用静态方法或静态字段,也可以考虑使用单例模式来管理这些依赖。在这种情况下,你可以手动注入依赖,并在静态上下文中访问它们。

@Component
public class RemoteEBRpcInvoker {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private static RemoteEBRpcInvoker instance;

    private PublishKitRuntimeUtil publishKitRuntimeUtil;
    private GetEbFormIdUtil getEbFormIdUtil;

    @Autowired
    public RemoteEBRpcInvoker(GetEbFormIdUtil getEbFormIdUtil,
                              PublishKitRuntimeUtil publishKitRuntimeUtil) {
        this.getEbFormIdUtil = getEbFormIdUtil;
        this.publishKitRuntimeUtil = publishKitRuntimeUtil;
        instance = this;
    }

    public static RemoteEBRpcInvoker getInstance() {
        return instance;
    }

    public GetEbFormIdUtil getGetEbFormIdUtil() {
        return this.getEbFormIdUtil;
    }

    public PublishKitRuntimeUtil getPublishKitRuntimeUtil() {
        return this.publishKitRuntimeUtil;
    }
}

这种方式通过一个静态的 instance 来存储当前对象实例,从而可以在静态上下文中访问非静态字段和方法。虽然这并不是典型的 Spring 注入方式,但它可以让你在需要使用静态字段时仍然能访问 Spring 管理的依赖。

方法6: 使用 @Lazy 懒加载

如果某些依赖可能在初始化时无法立即注入,或者你希望推迟静态字段的初始化,可以使用 @Lazy 注解,使依赖注入变为懒加载模式,这样可以确保当依赖确实需要时才会初始化。

@Component
public class RemoteEBRpcInvoker {
    private static final String DEFAULT_RPC_GROUP = "ebuilderform";
    private static PublishKitRuntimeUtil publishKitRuntimeUtil;
    private static GetEbFormIdUtil getEbFormIdUtil;

    @Autowired
    @Lazy
    public RemoteEBRpcInvoker(GetEbFormIdUtil getEbFormIdUtil,
                              PublishKitRuntimeUtil publishKitRuntimeUtil) {
        RemoteEBRpcInvoker.getEbFormIdUtil = getEbFormIdUtil;
        RemoteEBRpcInvoker.publishKitRuntimeUtil = publishKitRuntimeUtil;
    }
}

懒加载会推迟对依赖的初始化,直到第一次实际使用时才进行初始化。这种方式可以帮助解决静态字段在不适当的时间被初始化的问题。


总结

如果遇到以上问题,可以通过以下几种方式解决静态字段注入问题:

  1. 避免静态字段,使用实例字段(推荐)
  2. 使用 @PostConstruct 注解初始化
  3. 使用 静态 setter 方法 来注入静态依赖
  4. 使用 ApplicationContext 手动获取依赖
  5. 通过 单例模式 访问非静态依赖
  6. 使用 @Lazy 注解 懒加载依赖

以上每种方法都有其适用场景,具体选择可以根据项目的需求和设计模式来决定。ε=(´ο`*)))唉,希望此文可以帮助大家踩坑时避坑吧。。。

推荐阅读文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值