宁武皇仁光九年锦文轩刻本《异闻录》载: 扶桑画师浅溪,居泰安,喜绘鲤。院前一方荷塘,锦鲤游曳,溪常与嬉戏。 其时正武德之乱,潘镇割据,战事频仍,魑魅魍魉,肆逆于道。兵戈逼泰安,街邻皆逃亡,独溪不舍锦鲤,未去。 是夜,院室倏火。有人入火护溪,言其本鲤中妖,欲取溪命,却生情愫,遂不忍为之。翌日天明,火势渐歇,人已不见。 溪始觉如梦,奔塘边,但见池水干涸,莲叶皆枯,塘中鲤亦不知所踪。 自始至终,未辨眉目,只记襟上层迭莲华,其色魅惑,似血着泪。 后有青岩居士闻之,叹曰:魑祟动情,必作灰飞。犹蛾之投火耳,非愚,乃命数也。 ————《锦鲤抄》

关于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 字段时解析它。

方案二:重构代码消除循环依赖

​ 检查并重新设计组件之间的关系,使它们不再相互依赖,以解耦为目的,我觉得可以从以下几个方面考虑:

  • 将部分功能抽离为工具类或静态方法
  • 改变交互模式,比如借助设计模式等进行解耦,比如策略模式,我们对策略算法进行抽离封装,不同的逻辑处理有不同的策略类,客户代码不再需要直接依赖于具体的策略实现,而是依赖于一个共同的接口(即策略接口),从而实现解耦。

方案三:上下文接口实现

实现ApplicationContextAwareBeanPostProcessor接口,你可以在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

posted @ 2024-09-10 15:16  哒布溜  阅读(77)  评论(0编辑  收藏  举报