Spring框架中@Lazy延迟加载原理和使用详解

2025-05-07 15:50

本文主要是介绍Spring框架中@Lazy延迟加载原理和使用详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Spring框架中@Lazy延迟加载原理和使用详解》:本文主要介绍Spring框架中@Lazy延迟加载原理和使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐...

一、@Lazy延迟加载原理

如果某个类想要使它在Spring启动时不加载我们听的最多的便是为其加上@Lazy注解或者在@ComponentScan扫描注解中设置lazyInit为true即可完成。那么我们先来看看这两者分别的实现原理。

1.延迟加载原理

1.1 @Lazy三种配置方法

我们使用延迟加载一般有三种实现方式,第一种也是最原始的配置方式是在XML文件中直接配置标签属性:

<bean id="XXX" class="XXX.XXX.XXXX" lazy-init="true"/>

第二种方式为在@Component类上加上@Lazy注解:

@Lazy
@Component
public class XXXX {
    ...
}

第三种方式是在@Configuration类中配置@Bean时添加@Lazy注解:

@Configuration
public class XXXX {
    @Lazy
    @Bean
    public XXX getXXX() {
    http://www.chinasem.cn    return new XXX();
    }
}

1.2 @ComponentScan配置延迟加载

使用包扫描的配置方式如下:

@ComponentScan(value = "XXX.XXX", lazyInit = true)
@Configuration
public class XXXX {
    ...
}

1.3 加载原理

当使用上述三种配置后,Spring在扫描加载Bean时会读取@Lazy和@Component注解相应值,并设置Bean定义的lazyInit属性。

读取注解配置时最终会调用ClassPathBeanDefinitionScanner及其子类实现的DOScan方法,在这个方法中完成注解的读取配置。

关键源码如下:

public class ClassPathBeanDefinitionScanner 
        extends ClassPathScanningCandidateComponentProvider {
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
       // 不管是读取注解或者XML配置方式bean,最终读取加载Bean时都会进入到该方法
       // 对相应的包进行处理
       // beanDefinitions是保存返回bean定义的集合
       Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
       // 遍历多个包下的类
       for (String basePackage : basePackages) {
          // 获取满足条件的bean定义集合
          Set<BeanDefinition> candidates = 
                  findCandidateComponents(basePackage);
          // 对每个bean定义进行处理
          for (BeanDefinition candidate : candidates) {
             ScopeMetadata scopeMetadata = this.scopeMetadataResolver
                     .resolveScopeMetadata(candidate);
             candidate.setScope(scopeMetadata.getScopeName());
             String beanName = this.beanNameGenerator
                     .generateBeanName(candidate, this.registry);
             // 这个方法会处理@ComponentScan中的lazyInit值,因为在使用
             // @ComponentScan注解时会首先把该值赋值到beanDefinitionDefaults
             // 默认bean定义值的对象中,在postProcessBeanDefinition方法中
             // 会首先应用一次这些默认值,其中就包括lazyInit、autowireMode等
             if (candidate instanceof AbstractBeanDefinition) {
                postProcessBeanDefinition(
                        (AbstractBeanDefinition) candidate, beanName);
             }
             // 读取@Lazy、@Primary和@DependsOn等注解值
             if (candidate instanceof AnnotatedBeanDefinition) {
                AnnotationConfigUtils
                        .processCommonDefinitionAnnotations(
                                (AnnotatedBeanDefinition) candidate);
             }
             // 如果候选者满足要求则将其注册到Bean定义中心
             if (checkCandidate(beanName, candidate)) {
                BeanDefinitionHolder definitionHolder = 
                        new BeanDefinitionHolder(candidate, beanName);
                definitionHolder = AnnotationConfigUtils
                        .applyScopedProxyMode(scopeMetadata, 
                            definitionHolder, thiChina编程s.registry);
                beanDefinitions.add(definitionHolder);
                // 注册bean定义
                registerBeanDefinition(definitionHolder, this.registry);
             }
          }
       }
       return beanDefinitions;
    }
    protected void postProcessBeanDefinition(
            AbstractBeanDefinition beanDefinition, String beanName) {
       // 此处会应用默认值,如lazyInit、autowireMode、initMethod等
       beanDefinition.applyDefaults(this.beanDefinitionDefaults);
       if (this.autowireCandidatePatterns != null) {
          beanDefinition.setAutowireCandidate(PatternMatchUtils
                  .simpleMatch(this.autowireCandidatePatterns, beanName));
       }
    }
}

经过ClassPathBeanDefinitionScanner或子类实现的扫描读取后,延迟加载的配置便被配置到了Bean定义中,等初始化时再使用该属性,这里需要注意的是@ComponentScan延迟加载属性是可以被@Lazy覆盖的,因为@Lazy是在@ComponentScan后面处理的。

2.延迟加载实现原理

前面我们已经知道了在何处读取注解配置的属性,现在我们稍微看下其具体判断实现的地方。

2.1 AbstractApplicationContext

Spring框架在刷新时会初始化非延迟加载的单例bean,而一般我们使用的bean都是单例的。其关键源码如下:

public abstract class AbstractApplicationContext 
        extends DefaultResourceLoader
        implements ConfigurableApplicationContext {
    @Override
    public void refresh() throws BeansException, IllegalStateException {
        ...
        // 刷新流程中执行初始化非延迟单例的方法
        finishBeanFactoryInitialization(beanFactoryvvXqsr);
        ...
    }
    protected void finishBeanFactoryInitialization(
            ConfigurableListableBeanFactory beanFactory) {
        ...
        // 实际执行初始化非延迟加载单例
        beanFactory.preInstantiateSingletons();
    }
}

2.2 DefaultListableBeanFactory

最终会调用Spring工厂来实例化,直接看到其实现方法源码:

public class DefaultListableBeanFactory 
        extends AbstractAutowireCapableBeanFactory
        implements ConfigurableListableBeanFactory, BeanDefinitionRegistry,
        Serializable {
    // 当BeanDefinition被创建注册到工厂中时bean定义的名字将会被保存到这个集合中
    // 且里面的顺序为注册进来的顺序
    private volatile List<String> beanDefinitionNames = 
            new ArrayList<>(256);
    @Override
    public void preInstantiateSingletons() throws BeansException {
        // 获取所有已注册到Spring工厂中的bean定义
        List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
        // 遍历bean定义,初始化非抽象、单例且非延迟加载的bean对象
        for (String beanName : beanNames) {
           RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
           // !bd.isLazyInit()便是判断非延迟加载的,因此前面获取到的延迟加载
           // 属性会在这里进行判断
           if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
              // 这里面会判断是否是FactoryBean类型,最终都会调用到getBean中
              ...
           }
        }
        // 后续略过
        ...
    }
}

二、使用细节

读取源码其中一个目的是为了更好的实际的应用以及准确的把握对应功能的生效范围,因此在使用延迟加载时需要额外注意一些点。

Spring框架延迟加载属性在调用getBean之后将会失效,因为getBean方法是初始化bean的入口,这不难理解,那么平时我们使用@Autowired等自动注入注解时能和@Lazy注解一起使用吗?

接下来我们从两个实例来说明一下,这两个实例都是使用平时的使用用法,在Component上添加@Lazy注解,且让其实现InitializingBean接口,当Bean被加载时我们便能得知,看其是否会生效,示android例如下:

1.@Lazy失效实例

1.1 Controller非延迟加载类

声明一个Controller控制器:

@Controller
public class TestController implements InitializingBean{
    @Autowired
    private TestService testService;
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("testController Initializing");
    }
}

1.2 Service延迟加载类

再声明一个Service服务类:

@Lazy
@Service
public class TestService implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("testService Initializing");
    }
}

1.3 结果输出

启动程序后控制台输出:

testService Initializing

testController Initializing

启动完Spring程序后输出了TestService里面打印的字符串。这就奇怪了,明明使用了@Lazy注解,但是却并没有其作用,在Spring启动项目时还是加载了这个类?这就涉及到@Autowired等自动注入注解的使用了,如果有兴趣了解其实现的可以去看文章Spring框架原理之实例化bean和@Autowired实现原理。

由于Controller类不是延迟加载的,且里面使用@Autowired自动注入注解注入了Service,因此在程序初始化时Controller将会被初始化,同时在处理@Autowired注解的字段时,会调用getBean方法从Spring工厂中获取字段的bean对象,因此通过@Autowired路线加在了Service,这就导致了@Lazy注解失效了,因此虽然没通过refresh方法流程初始化,但是却通过@Autowired的处理类初始化了。

2.@Lazy起效实例

想要@Lazy注解起作用,只需要改一步,即把Controller也改成@Lazy,让其在启动时不被加载,不触发@Autowired注解依赖链的调用即可。

2.1 修改的Controller实例

修改后如下:

@Lazy
@Controller
public class TestController implements InitializingBean{
    @Autowired
    private TestServicejavascript testService;
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("testController Initializing");
    }
}

如果是这种配置@Lazy将会起作用,在项目启动时将不会加载这两个需要延迟加载的bean。

总结

从上面的例子我们可以总结及延伸出两个注意点:

  1. 非延迟加载的类中不能自动注入延迟加载的类,会导致延迟加载失效;
  2. 如果想要实现某个类延迟加载使用自动注入功能时需要调用链前都不存在非延迟加载类,否则延迟加载失效。

作用效果总结图如下:

Spring框架中@Lazy延迟加载原理和使用详解

其实@Scope指定原型和单例时有些情况也会导致原型bean“失效”,这又是另外一个故事了,后面有机会再分析一波。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程China编程(www.chinasem.cn)。

这篇关于Spring框架中@Lazy延迟加载原理和使用详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154509

相关文章

Java中的getBytes()方法使用详解

《Java中的getBytes()方法使用详解》:本文主要介绍Java中getBytes()方法使用的相关资料,getBytes()方法有多个重载形式,可以根据需要指定字符集来进行转换,文中通过代... 目录前言一、常见重载形式二、示例代码三、getBytes(Charset charset)和getByt

Java使用Stream流的Lambda语法进行List转Map的操作方式

《Java使用Stream流的Lambda语法进行List转Map的操作方式》:本文主要介绍Java使用Stream流的Lambda语法进行List转Map的操作方式,具有很好的参考价值,希望对大... 目录背景Stream流的Lambda语法应用实例1、定义要操作的UserDto2、ListChina编程转成M

使用easy connect之后,maven无法使用,原来需要配置-Djava.net.preferIPv4Stack=true问题

《使用easyconnect之后,maven无法使用,原来需要配置-Djava.net.preferIPv4Stack=true问题》:本文主要介绍使用easyconnect之后,maven无法... 目录使用easGWowCy connect之后,maven无法使用,原来需要配置-DJava.net.pr

idea报错java: 非法字符: ‘\ufeff‘的解决步骤以及说明

《idea报错java:非法字符:‘ufeff‘的解决步骤以及说明》:本文主要介绍idea报错java:非法字符:ufeff的解决步骤以及说明,文章详细解释了为什么在Java中会出现uf... 目录BOM是什么?1. BOM的作用2. 为什么会出现 \ufeff 错误?3. 如何解决 \ufeff 问题?最

python+OpenCV反投影图像的实现示例详解

《python+OpenCV反投影图像的实现示例详解》:本文主要介绍python+OpenCV反投影图像的实现示例详解,本文通过实例代码图文并茂的形式给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一、前言二、什么是反投影图像三、反投影图像的概念四、反向投影的工作原理一、利用反向投影backproj

使用Java编写一个字符脱敏工具类

《使用Java编写一个字符脱敏工具类》这篇文章主要为大家详细介绍了如何使用Java编写一个字符脱敏工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1、字符脱敏工具类2、测试工具类3、测试结果1、字符脱敏工具类import lombok.extern.slf4j.Slf4j

Java实现按字节长度截取字符串

《Java实现按字节长度截取字符串》在Java中,由于字符串可能包含多字节字符,直接按字节长度截取可能会导致乱码或截取不准确的问题,下面我们就来看看几种按字节长度截取字符串的方法吧... 目录方法一:使用String的getBytes方法方法二:指定字符编码处理方法三:更精确的字符编码处理使用示例注意事项方

pandas DataFrame keys的使用小结

《pandasDataFramekeys的使用小结》pandas.DataFrame.keys()方法返回DataFrame的列名,类似于字典的键,本文主要介绍了pandasDataFrameke... 目录Pandas2.2 DataFrameIndexing, iterationpandas.DataF

使用Python和PaddleOCR实现图文识别的代码和步骤

《使用Python和PaddleOCR实现图文识别的代码和步骤》在当今数字化时代,图文识别技术的应用越来越广泛,如文档数字化、信息提取等,PaddleOCR是百度开源的一款强大的OCR工具包,它集成了... 目录一、引言二、环境准备2.1 安装 python2.2 安装 PaddlePaddle2.3 安装

嵌入式Linux之使用设备树驱动GPIO的实现方式

《嵌入式Linux之使用设备树驱动GPIO的实现方式》:本文主要介绍嵌入式Linux之使用设备树驱动GPIO的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录一、设备树配置1.1 添加 pinctrl 节点1.2 添加 LED 设备节点二、编写驱动程序2.1