关于Spring的bean注入
本篇内容涉及Spring Bean注入类型和方式,以及类加载的一些介绍,还有一些注入问题的解决办法
1.了解Bean
在Spring框架中,Bean是一个被Spring IoC容器管理的对象。Spring IoC(控制反转)容器负责Bean的实例化、配置和组装。当你将类标记为Spring管理的Bean时,Spring可以为你创建类的实例,并管理其生命周期。此外,Spring还允许你通过依赖注入(DI)的方式将Bean之间的依赖关系自动注入到Bean中,从而实现了低耦合的设计。
简单来说,bean是spring容器管理对象的一个名词概括。项目中涉及到的一些装配管理,依赖注入,切面与增强,生命周期等都与Bean注入有关。
对于bean,我们要知道Spring框架下大部分对象的创建管理都是交给Spring容器(IOC思想),并进行配置管理,比如:
@Bean,@Component,@Controller,@Service 和@Repository,添加注解后交给Spring容器进行管理,可以通过注解进行依赖注入,在容器启动的时候是会扫描标注这些注解的类创建 Bean 并放入容器中。
现在常用注入依赖方式:
- 构造器注入:利用构造方法的参数注入依赖
- Setter注入:调用Setter的方法注入依赖
- 字段注入:在字段上使用 @Autowired、@Resource或 @Inject 注解
2.注入方式
首先是三种注入方式的简单介绍:
考虑到与spring的匹配度,这里使用Spring注解 @Autowired而非java标准注解 @Inject
3.1 注入实例
字段注入
@Controller
public class UserController {
@Autowired
private UserService userService;
}
构造器注入
@Controller
public class UserController {
private final UserService userService;
public UserController(UserService userService){
this.userService = userService;
}
}
Setter注入
@Controller
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService){
this.userService = userService;
}
}
3.2 三种方式对比
构造器注入方式可靠性最强,可维护性也最好,配合lombok的注解@RequiredArgsConstructor,不需要手动写构造方法了。
IDEA不推荐使用@Autowired进行Field注入的原因
在使用IDEA时可能会发现,Field injection is not recommended (字段注入是不被推荐的)。
原因:
1.使用@Resource则不会提示警告,这是因为@Resource是JSR-250提供的,与@Autowired比对IOC容器的耦合更低(@Autowired是spring提供的),尽管概率很低,但还是要考虑更换容器的可能性,以及更换后代码运行是否正常。
2.字段注入没法注入不可变对象,而构造器注入可以
3.字段注入会导致单元测试(mock)也要使用IOC容器
4.构造器注入通过final可以预防运行时bean被修改出现空值,导致空指针,安全性高一些,相对的,灵活性要差一些,比如不如另外两种注入方式灵活:
- Setter和字段注入适用于依赖经常变动的情况
- 构造器注入,依赖不变
简单总结如下
从可靠性,可维护性,是否循环依赖检测,性能方面对比:
可靠性主要取决于构造过程中是否可变
可维护性,即可读性,是否利于维护
对于Bean之间是否存在循环依赖关系的检测能力(srpingboot2.6以后默认配置下只要存在循环依赖都无法启动)
性能主要和启动顺序要求有关
表格对比
注入方式 | 可靠性 | 可维护性 | 循环依赖检测 | 性能影响 |
---|---|---|---|---|
Field Injection | 不可靠 | 差 | 不检测 | 较快 |
Constructor Injection | 可靠 | 好 | 自动检测 | 相对较慢 |
Setter Injection | 不可靠 | 差 | 不检测 | 较快 |
总结来说:推荐使用构造器注入
tips:可以配合lombok注解@RequiredArgsConstructor使用:
@RequiredArgsConstructor
@Service
public class SysOssServiceImpl implements ISysOssService, OssService {
private final SysOssMapper baseMapper;
}
可省略构造方法书写
3.特殊业务场景
3.1 注入式Bean为空
工具类或其余非@Controller,@Service 和@Repository注解修饰的普通类需要调用其余接口,这时直接使用@Autowired或者其他注入注解会无效,debug可以发现对应的bean为空。
我们在项目中,一般在controller层中注入service接口,在service层中注入其它的service接口或者mapper接口都是可以的,但是如果我们要在我们自己封装的Utils工具类中或者非controller普通类中使用@Resource或@Autowried注解注入Service或者Mapper接口就会出现注入为null的问题
测试背景:有个日志注解我们用多线程异步去执行,现在给多线程异步报错统一处理,新增一个handler处理类,然后在日志保存方法里故意抛出异常,测试handler处理类中mapper是否会空指针。
测试代码1:
按照普通业务代码那样去写:
@Slf4j
@Component
public class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Resource
private WarningResultMapper warningResultMapper;
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
if (Objects.isNull(warningResultMapper)) {
System.out.println("bean为null");
}else {
System.out.println("bean不为null");
}
log.info("测试是否有");
log.info("Exception message - " + throwable.getMessage());
log.info("Method name - " + method.getName());
for (Object param : objects) {
log.info("Parameter value - " + param);
}
}
}
输出 bean为null
@Resource换成@Autowired也依然是null,我的MyAsyncExceptionHandler里的方法也不是工具类那种静态的,mapper为什么会null呢?有人说是多线程防注入。
测试代码2:
在多线程实例中使用 @Autowired
注解得不到对象,对象为 null,为什么呢?
这是因为多线程是防注入的,所以只是在多线程实现类中简单的使用 @Autowired
方法注入自己的 service,会在程序运行到此类调用 service 方法的时候提示注入的 service 为 null。
于是我改为使用 @PostConstruct, 被 @PostConstruct 修饰的方法会在服务加载 Servlet 的时候运行,并且只会被执行一次。PostConstruct 在构造函数之后执行,init () 方法之前执行。PreDestroy()方法在 destroy () 方法执行执行之后执行, 结果能注入成功
使用静态变量 加 @PostConstruct
@Slf4j
@Component
public class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Resource
private WarningResultMapper warningResultMapper;
private static MyAsyncExceptionHandler myAsyncExceptionHandler;
@PostConstruct
public void init() {
myAsyncExceptionHandler = this;
myAsyncExceptionHandler.warningResultMapper = this.warningResultMapper;
}
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
if (Objects.isNull(myAsyncExceptionHandler.warningResultMapper)) {
System.out.println("bean为null");
}else {
System.out.println("bean不为null");
}
log.info("测试是否有");
log.info("Exception message - " + throwable.getMessage());
log.info("Method name - " + method.getName());
for (Object param : objects) {
log.info("Parameter value - " + param);
}
}
}
打印:
bean不为null
注入成功,这种方式也适用于static修饰类调用bean的注入报错。
因为静态变量(成员)它是属于类的,而非属于实例对象的属性;同样的静态方法也是属于类的,普通方法(实例方法)才属于对象。而Spring容器管理的都是实例对象,包括它的@Autowired
依赖注入的均是容器内的对象实例,
通常类加载早于Spting容器启动,当类加载器加载静态变量时,Spring的上下文环境还没有被加载,所以无法为静态变量绑定值,所以对于static成员是不能直接使用@Autowired
注入的,可以用@PostConstruct初始化等方法,主要思想是延迟赋值,在使用前完成赋值即可。
如果依然有疑惑,请先了解Spring上下文加载与静态变量的关系,见Tips
Tips
Spring上下文加载
Spring框架的上下文环境(context)负责管理应用中的bean(即对象),包括它们的生命周期和依赖关系。Spring上下文在应用程序启动时加载,并在此过程中解析配置文件(如XML文件或注解),创建并管理bean。
静态变量
静态变量是类级别的变量,无论类有多少实例,都会共享静态变量;静态变量在类被加载到JVM时进行初始化,一般来说就是第一次 通过new 创建类实例的时候或者访问静态方法的时候。
静态变量与Spring上下文加载的关系
由于静态变量在类加载时初始化,而Spring上下文是在应用程序启动时加载的,通常JVM类加载要早于Spring容器启动,这意味着在静态变量初始化时,Spring的上下文可能还没有被完全加载和初始化。因此,在静态变量初始化时,你无法直接访问Spring上下文中的bean或利用Spring的依赖注入功能来为静态变量赋值。
@PostConstruct注解
@PostConstruct
注解用于标记在依赖注入完成后需要执行的方法。这个方法会在构造函数之后、类完全初始化之前被自动调用。因此,它提供了一个在Spring上下文加载完成后执行代码的机会,这时你可以安全地访问Spring管理的bean,包括为静态变量赋值。
类内方法调用顺序
- 静态方法:用static声明,jvm加载类的时候执行,只执行一次。
- 构造函数:对象一建立就调用相应的构造函数。
(有没有想起大学试卷里执行顺序的考题了)
测试代码3:
还有另一种方法
使用静态变量,并配合set注入
具体见setMyAsyncExceptionHandler();
这种注入方式也很好理解,就是通过调用成员变量的set方法来注入想要使用的依赖对象,但是为什么mapper对象还要用static修饰呢?不能去掉,然后直接用this赋值吗,我推测这还是和多线程环境有关,用static修饰应该是为了保证:所有该类的实例共享同一个warningResultMapper
变量。静态变量在类被加载到JVM时就被初始化,并且在类的整个生命周期内都存在
@Slf4j
@Component
public class MyAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
// @Resource
private static WarningResultMapper warningResultMapper;
private static MyAsyncExceptionHandler myAsyncExceptionHandler;
/*
@PostConstruct
public void init() {
myAsyncExceptionHandler = this;
myAsyncExceptionHandler.warningResultMapper = this.warningResultMapper;
}
*/
@Autowired
public void setMyAsyncExceptionHandler(WarningResultMapper warningResultMapper) {
MyAsyncExceptionHandler.warningResultMapper = warningResultMapper;
}
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
//if (Objects.isNull(myAsyncExceptionHandler.warningResultMapper)) {
if (Objects.isNull(warningResultMapper)) {
System.out.println("bean为null");
} else {
System.out.println("bean不为null");
}
log.info("测试是否有");
log.info("Exception message - " + throwable.getMessage());
log.info("Method name - " + method.getName());
for (Object param : objects) {
log.info("Parameter value - " + param);
}
}
}
还有一种方法,代码注入,这里偷懒引用别人的demo
@Component //关键1
public class ArticlesReceiver {
private static WechatArticlesTempService wechatArticlesTempService = SpringContextHolder.getBean(WechatArticlesTempService.class); //关键2
public WechatArticlesTemp getResposeArticlesBoby(String mediaId) {
WechatArticlesTemp articlesTemp = wechatArticlesTempService.getById(mediaId); //关键3
return articlesTemp ;
}
}
3.2 循环依赖报错
项目启动,循环依赖报错
The dependencies of some of the beans in the application context form a cycle:
XXXX
Requested bean is currently in creation: Is there an unresolvable circular reference?
我们可以在报错信息里找到异常类型:BeanCurrentlyInCreationException,说明这种报错属于循环依赖问题。
3.2.1 什么是循环依赖
创建新的A时,发现要注入原型字段B,创建新的B发现要注入原型字段A,或者A类自己循环注入自己,都会报错。
对于这种情况,Spring提供了处理机制,但处理的方式和彻底性取决于依赖注入的类型
字段注入,Setter注入
spring利用三级缓存解决字段注入循环依赖问题,如果sprongboot版本2.6以上,需要先开启配置 允许循环依赖:
spring:
main:
allow-circular-references: true
如果不开启配置启动时就会检测报错,不管你是不是非构造器注入,只要存在循环依赖都会启动报错
构造器注入
对于通过构造函数注入的bean,Spring无法自动解决循环依赖问题。这是因为在构造过程中,bean还未完全初始化,因此无法被注入到其他bean中。如果使用会发现,Spring会抛出BeanCurrentlyInCreationException异常。
我这次报错就是因为在构造器注入的前提下试图自己注入自己:由于spring采用默认单例模式导致报错循环依赖
3.2.2 解决办法
方案一:利用@Lazy注解
@Lazy 的延迟加载打破循环依赖:假设现在有AService和BService,通过其它途径生成 BService 的lazy 的代理对象,不会去走创建BService 的代理对象,然后注入AService 这套流程。
这样创建AService 的单例对象并放入到单例池中,BService 的bean 在实例化后,注入AService bean 属性就可以从单例池中加载到AService 的真正的bean ,而不会出现bean 对象不一致的问题。
如下代码中的ProjectDocumentService bean注入
// @RequiredArgsConstructor
@Service
public class ProjectDocumentServiceImpl implements ProjectDocumentService {
@Lazy
@Autowired
private ConDocumentService conDocumentService;
}
在这个例子中,ProjectDocumentServiceImpl
依赖于 ConDocumentService
,但通过使用 @Lazy
,Spring 不会在创建 ProjectDocumentServiceImpl
的实例时立即解析 ConDocumentService
。相反,它会在第一次访问 ConDocumentService
字段时解析它。
方案二:重构代码消除循环依赖
检查并重新设计组件之间的关系,使它们不再相互依赖,以解耦为目的,我觉得可以从以下几个方面考虑:
- 将部分功能抽离为工具类或静态方法
- 改变交互模式,比如借助设计模式等进行解耦,比如策略模式,我们对策略算法进行抽离封装,不同的逻辑处理有不同的策略类,客户代码不再需要直接依赖于具体的策略实现,而是依赖于一个共同的接口(即策略接口),从而实现解耦。
方案三:上下文接口实现
实现ApplicationContextAware
或BeanPostProcessor
接口,你可以在bean完全初始化后(即解决了循环依赖后)获取它们并执行自定义的逻辑
代码:
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class BeanUtils implements ApplicationContextAware {
protected static ApplicationContext applicationContext ;
@Override
public void setApplicationContext(ApplicationContext arg0) throws BeansException {
if (applicationContext == null) {
applicationContext = arg0;
}
}
public static Object getBean(String name) {
//name表示其他要注入的注解name名
return applicationContext.getBean(name);
}
/**
* 拿到ApplicationContext对象实例后就可以手动获取Bean的注入实例对象
*/
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
获取bean:
private RedisUtils redisUtils = BeanUtils.getBean(RedisUtils.class);
4. 关于spring的bean单例模式
在Spring中,bean可以被定义为两种模式:prototype(原型)和singleton(单例)
singleton(单例):只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实例。
prototype(原型,多例):对这个bean的每次请求都会创建一个新的bean实例,类似于new。
Spring bean 默认作用域是单例。
5.Java类加载机制
5.1 在Spring启动后,整个过程涉及到Spring容器加载Bean,也涉及到JVM加载类。二者有什么联系
在Spring框架中,Bean的加载和JVM加载类之间存在密切的联系,但它们是两个不同层次的概念。
JVM加载类
JVM加载类是指将类的字节码从.class文件加载到内存中,并创建对应的Class对象的过程。这个过程包括类的加载、链接(验证、准备、解析)和初始化阶段。类加载由Java虚拟机的类加载器(ClassLoader)来完成。
Spring框架本身并不涉及类的加载阶段,这是由Java虚拟机在运行时动态完成的。Spring依赖于JVM加载类的机制来实现依赖注入和管理Bean实例。
Spring加载Bean
Spring框架在启动时会创建一个IOC(控制反转)容器,也称为应用上下文。这个容器负责管理Bean的生命周期、依赖注入等任务。
当Spring容器启动时,它会根据配置文件(如XML配置、注解或者Java配置类)中的定义,实例化Bean对象并将它们装配到容器中。这个过程包括Bean的创建、初始化和注入依赖。
Spring加载Bean的过程需要依赖JVM加载类的功能。当Spring容器实例化一个Bean时,它首先要求JVM加载该Bean类的字节码。然后,Spring根据配置文件或注解,实例化Bean对象并完成依赖注入。
因此,Spring框架的Bean加载过程依赖于JVM加载类的机制。Spring本身并不负责类的加载,而是在类加载完成后,利用已加载的类来创建和管理Bean实例。这种分工使得Spring框架能够有效地利用JVM的类加载机制,实现灵活的依赖注入和控制反转功能。
5.2 Spring在启动过程中,是否会实例化所有Bean?
Spring在启动过程中确实会实例化所有在配置中定义的Bean,但这并不意味着它会立即调用每个Bean的构造函数或者执行它们的初始化方法。实际上,Spring会按需创建Bean的实例,并在需要时进行依赖注入和初始化。
具体来说,当Spring容器启动时,它会扫描配置文件(如XML配置、Java配置类或者注解)中的Bean定义。然后,它会根据这些定义实例化Bean,并将它们放入容器的Bean工厂中管理。这个过程称为Bean的注册。但是,Spring并不会立即实例化和初始化每个Bean的实例,而是等到某个Bean被需要时才进行实例化和初始化操作。
Spring的延迟初始化策略允许应用程序更高效地使用内存资源,并且在容器启动时不必立即创建所有Bean实例。因此,虽然Spring会在启动时创建所有Bean的定义,但它并不一定会在启动时就创建所有Bean的实际实例。
5.3 Bean的注册,Bean按需实例化,延迟初始化
所以这意味着最开始Spring启动时,所有的Bean实例都会被创建并注册进入Map,但是实例化和初始化是按需的?准确地说,当Spring容器启动时,它会创建并注册所有在配置文件或者注解中定义的Bean的定义(Bean Definition),而不是所有的Bean实例。这些Bean的定义包括Bean的类信息、依赖关系等,并被存储在容器的Bean工厂中,通常是一个Map结构,用于管理这些Bean的元数据。
具体流程:
Bean的注册:
Spring会在启动时扫描配置,解析所有的Bean定义(如@Component、@Service、@Repository等注解或者XML配置中的元素),并将这些定义转换成内部数据结构(BeanDefinition)。这些BeanDefinition描述了Bean的类、依赖、作用域等信息。
按需实例化:
当应用程序需要访问某个Bean时,Spring才会根据对应的BeanDefinition来实际创建该Bean的实例。这时候,Spring会根据Bean的作用域(如单例、原型等)决定是否需要创建新的实例,以及是否需要执行Bean的初始化方法(如@PostConstruct注解标记的方法)。
延迟初始化:
Spring的延迟初始化机制确保只有在需要时才会创建Bean实例,从而节省资源并提高应用程序的启动性能。即使在容器启动后,很多Bean可能并不会立即被实例化和初始化,除非有其他Bean或者代码依赖它们。
总结来说,Spring在启动时会注册所有Bean的定义到Bean工厂中,但实际的Bean实例化和初始化是按需进行的,根据应用程序的需要动态创建和管理。
6. 参考资料
雨凝大佬的文章,简洁凝练:https://www.rainng.com/field-injection-is-not-recommend/
多线程注入null:https://cloud.tencent.com/developer/article/1595799
三种注入方式对比: https://www.cnblogs.com/mili0601/p/15582421.html
类加载机制: https://pdai.tech/md/java/jvm/java-jvm-classload.html
spring Bean加载:https://blog.csdn.net/qq_38096989/article/details/140293882