1. 问题描述
空指针异常,获取属性配置类AliOssProperties中的endpoint属性时,为空。
配置文件中正确配置了相关的属性,并且AliOssProperties上加了@ConfigurationProperties,启动类上通过@EnableConfigurationProperties(AliOssProperties.class)启用了配置属性支持。
Error starting ApplicationContext.
To display the conditions report re-run your application with 'debug' enabled.
Failed to instantiate [com.cyt.utils.AliOssUtil]:
Constructor threw exception;
nested exception is java.lang.NullPointerException:
Cannot invoke "com.cyt.properties.AliOssProperties.getEndpoint()"
because "this.aliOssProperties" is null
2. 问题分析
原因是在 @Autowired
注入 AliOssProperties
时,注入发生在 Spring 容器初始化的过程中,而 endpoint
、accessKeyId
等这些字段是在 aliOssProperties
注入之前初始化的,所以它们在被注入之前仍然是 null
。
@Autowired
private AliOssProperties aliOssProperties;
private String endpoint = aliOssProperties.getEndpoint(); // 这里可能会导致 NPE
private String accessKeyId = aliOssProperties.getAccessKeyId();
private String accessKeySecret = aliOssProperties.getAccessKeySecret();
private String bucketName = aliOssProperties.getBucketName();
这些字段的赋值依赖于 aliOssProperties
,但是由于 aliOssProperties
还未完成注入,所以当 Spring 初始化这些字段时,aliOssProperties
仍然是 null
,因此会导致 NullPointerException
。
3. 知识扩展
在 Spring 中,@Autowired
注解和属性的初始化顺序是由 Java 类的生命周期以及 Spring 容器的管理流程决定的。以下是注入和属性初始化的先后过程,解释为什么在使用 @Autowired
注入时,直接给属性赋值可能导致 NullPointerException
。
3.1 Java 类的加载与初始化顺序
Java 类的加载和初始化是 JVM 管理的一个流程,通常包括以下几个步骤:
- 加载类(Class Loading):JVM 会加载类的字节码到内存中。
- 链接类(Class Linking):JVM 会对类的静态部分进行解析和验证。
- 类初始化(Class Initialization):
- 静态变量初始化:静态变量初始化是类加载的一部分,优先级最高。
- 实例变量初始化:接下来是非静态实例变量的初始化。
- 构造器初始化:最后执行构造方法,完成类的实例化。
3.2 Spring Bean 的初始化顺序
在 Spring 中,Bean 的生命周期由 Spring 容器管理,Bean 初始化时会经历以下几个重要步骤:
-
Bean 实例化(Instantiation):Spring 容器首先根据配置文件或注解扫描生成类的实例,即通过无参构造函数创建对象(等同于
new
操作)。 -
依赖注入(Dependency Injection):此时,Spring 会为类中的
@Autowired
、@Value
注解的字段注入依赖项。比如,将AliOssProperties
对象注入到AliOssUtil
类中。 -
初始化回调(Initialization Callback):如果类实现了
InitializingBean
接口或使用了@PostConstruct
注解,Spring 会在依赖注入完成后调用相关方法进行初始化。 -
Bean 可用:完成依赖注入和初始化后,Bean 才真正可以被使用。
3.3 发生 NullPointerException
的原因
-
Bean 实例化:Spring 首先实例化
AliOssUtil
类,也就是执行new AliOssUtil()
操作。此时,Spring 容器还没有为aliOssProperties
注入值。 -
属性初始化:在实例化过程中,类中的实例变量会被初始化。此时,Java 会尝试为
endpoint
、accessKeyId
等字段赋值。这些字段是基于aliOssProperties
进行初始化的,而aliOssProperties
还没有被注入,因此它的值是null
。调用aliOssProperties.getEndpoint()
就会抛出NullPointerException
。 -
依赖注入:实例化和属性初始化完成后,Spring 才会执行依赖注入操作,将
aliOssProperties
注入到AliOssUtil
类中。这时aliOssProperties
才不再是null
,但是已经错过了在声明时直接赋值的时机。
4. 解决方案
4.1 在方法中使用
不要在属性声明时使用 aliOssProperties
,而是在方法中延迟使用它。
@Component
@Slf4j
public class AliOssUtil {
@Autowired
private AliOssProperties aliOssProperties;
public String upload(byte[] bytes, String objectName) {
// 在使用时再获取 aliOssProperties 的值,确保此时它已经注入完成
String endpoint = aliOssProperties.getEndpoint();
String accessKeyId = aliOssProperties.getAccessKeyId();
String accessKeySecret = aliOssProperties.getAccessKeySecret();
String bucketName = aliOssProperties.getBucketName();
// 上传逻辑...
}
}
4.2 手动配置 AliOssUtil的
Bean对象
4.2.1 实现
通过 Java Config 类 使用 @Bean
注解手动创建 AliOssUtil
实例,而不是通过默认的 Spring 容器自动扫描和注入。它解决了依赖注入和属性初始化顺序的问题,因为显式地将AliOssProperties
的属性传递给 AliOssUtil。
/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
@ConditionalOnMissingBean
表示当 Spring 容器中不存在 AliOssUtil
类型的 Bean 时,才会创建这个 Bean。这样可以避免重复创建 Bean 的问题,防止冲突。这对于大型项目特别有用,因为不同的模块可能会有不同的配置来源,如果某个模块已经定义了 AliOssUtil
,这个配置类就不会重复创建。
4.2.2 分析
由于 Spring 的依赖注入机制,Spring 会按照依赖顺序来解析每个 Bean。当 AliOssUtil
依赖于 AliOssProperties
时,Spring 会确保在创建 AliOssUtil
实例之前,AliOssProperties
已经完成初始化。因此:
- 当 Spring 看到
aliOssUtil
方法的参数是AliOssProperties
,它会先去查找并创建(如果尚未创建)一个AliOssProperties
实例。 - 之后,Spring 会将这个
AliOssProperties
实例传递给aliOssUtil
方法。 - 由于
AliOssProperties
已经正确初始化并注入,AliOssUtil
的构造函数可以安全地使用这些属性进行初始化。
5. 总结
- Spring 的依赖注入是在 Bean 实例化后进行的,因此如果在 Bean 实例化时就使用未注入的依赖项(比如
@Autowired
的属性),可能会导致NullPointerException
。 - 为了解决这个问题,可以延迟对依赖项的使用(即在方法中使用),或者手动管理Bean对象。