SpringBoot(4) bean的条件注解与懒加载

1,595 阅读6分钟

条件注解

在IoC篇曾简单介绍过条件注解并列举了四个常用的注解,它们内部存在@Conditional注解,作用域基本是类和方法,接下来将会详细介绍条件注解的实现。

ConditionEvaluator->shouldSkip():
    // 判断是否有Conditional注解,没有直接返回
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }
    ......
    // conditions是@Conditional的注解值
    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 这里执行注解值的 condition.matches 方法来进行匹配,返回布尔值
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            // 不符合的话,后续逻辑不添加bdm
            return true;
        }
    }

    return false;
SpringBootCondition->matches():
    ConditionOutcome outcome = getMatchOutcome(context, metadata);

这里的conditions是类或方法条件注解的@Conditional注解值,如果有多个的话需要都满足才行,@ConditionalOnClass的是OnClassCondition@ConditionalOnBean的是OnBeanCondition@Profile的是ProfileCondition,判断是否符合执行的是注解值的matches方法。

OnClassCondition

OnClassCondition->getMatchOutcome():
    // 获取注解ConditionalOnClass的注解值,返回全限定名数组
    List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
    if (onClasses != null) {
        List<String> missing = filter(onClasses, ClassNameFilter.MISSING,
                classLoader);
        // 只要有缺失就立即返回
        if (!missing.isEmpty()) {
            return ConditionOutcome
                    .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                        .didNotFind("required class", "required classes")
                        .items(Style.QUOTE, missing));
    // 省略部分是注解ConditionalOnMissingClass的处理,逻辑类似
    ......
FilteringSpringBootCondition->filter():
    // missing情况下,如果没有加载到该类,添加进matches集合中
    if (classNameFilter.matches(candidate, classLoader)) {
        matches.add(candidate);
    }
    return matches;
    
FilteringSpringBootCondition->ClassNameFilter:
    protected enum ClassNameFilter {
        PRESENT {
            @Override
            public boolean matches(String className, ClassLoader classLoader) {
                return isPresent(className, classLoader);
            }
    
        },
    
        MISSING {
            @Override
            public boolean matches(String className, ClassLoader classLoader) {
                return !isPresent(className, classLoader);
            }
    
        };
        
        isPresent():
            try {
               forName(className, classLoader);
               return true;
            } catch (Throwable ex) {
                return false;
            }
    }

@ConditionalOnClass的处理就是加载给定类,没有则跳过,@ConditionalOnMissingClass与其相反。

OnBeanCondition

假设ConditionalOnBean的值如下 image.png

OnBeanCondition->getMatchOutcome():
    ConditionMessage matchMessage = ConditionMessage.empty();
    if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
        // 将注解信息封装成BeanSearchSpec对象
        // 如果没有指定types和names属性,看注解是否与@Bean配合使用,如果是的话,添加返回类的全限定名
        BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
                ConditionalOnBean.class);
        MatchResult matchResult = getMatchingBeans(context, spec);
        // 只要有缺失就立即返回
        if (!matchResult.isAllMatched()) {
            String reason = createOnBeanNoMatchReason(matchResult);
            return ConditionOutcome.noMatch(ConditionMessage
                    .forCondition(ConditionalOnBean.class, spec).because(reason));
        }
        // 省略部分是注解ConditionalOnMissingBean的处理,逻辑类似
        ......
OnBeanCondition->getMatchingBeans():
    // 根据type判断目标bean是否存在
    for (String type : beans.getTypes()) {
        Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
                typeExtractor, context.getClassLoader(), considerHierarchy);
        ......
        // 存在放一个集合,不存在放一个集合
    }
    // 省略部分是对于annotations和names的处理,逻辑类似
    ......
    return matchResult;
OnBeanCondition->getBeanNamesForType():
    try {
        return getBeanNamesForType(beanFactory, considerHierarchy,
                ClassUtils.forName(type, classLoader), typeExtractor);
    } catch (ClassNotFoundException | NoClassDefFoundError ex) {
        // 这里也考虑到了加载类不存在的情况
        return Collections.emptySet();
    }

例子中@ConditionalOnBean封装成的BeanSearchSpec对象如下 image.png

最终执行的是BeanTypeRegistrygetNamesForType()方法

BeanTypeRegistry->getNamesForType():
    // 遍历IoC容器的bdn和manualSingletonNames,当前类成员变量beanTypes没有name的话,则添加
    updateTypesIfNecessary();
    // 从beanTypes中遍历,获取对应的Class对象,如果是给定的子类的话,则添加其名称
    return this.beanTypes.entrySet().stream().filter((entry) -> {
        Class<?> beanType = extractType(entry.getValue(), typeExtractor);
        return beanType != null && type.isAssignableFrom(beanType);
    }).map(Map.Entry::getKey).collect(Collectors.toCollection(LinkedHashSet::new));

对于type,没有用IoC容器的getBeanNamesForType方法,而是很巧妙地使用了遍历+查实例化对象的方式。

使用@ConditionalOnBean存在bean加载顺序的问题,只有项目中最初扫描出来带有@Component的bean才会在全部放入容器之后判断(即不能是@Import,@Bean等注解引入的bean)。因此建议条件注解只判断@Component层面是否有相应的bean

ProfileCondition

ProfileConditionmatches方法中观察到,先是获取了@Profile注解的属性,然后查看环境属性是否包含了该注解,有则返回true,没有则返回false。

// 最终会走入该方法中
AbstractEnvironment->isProfileActive():
    // 获取activeProfiles
    Set<String> currentActiveProfiles = doGetActiveProfiles();
    // profile是注解值对象
    // currentActiveProfiles包含就返回true,否则返回false
    return (currentActiveProfiles.contains(profile) ||
            (currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));

总结

找到条件注解的@Conditional属性值,查看其matches()方法,就可以探索条件注解的实现原理了。

懒加载

前言:IoC之实例化与依赖注入

懒加载,指的是@Lazy注解,如果对bean使用,不会立即实例化,但是如果bean有引用或是依赖的话,还是会实例化的,如果对字段使用,那么不会立即注入依赖。

对bean引用的代码位于DefaultListableBeanFactory->preInstantiateSingletons()

假设对字段引用使用的是@Lazy @Autowired注解。

DefaultListableBeanFactory->resolveDependency() 
    Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
            descriptor, requestingBeanName);
ContextAnnotationAutowireCandidateResolver->getLazyResolutionProxyIfNecessary():
    return (isLazy(descriptor) // --1
            ? buildLazyResolutionProxy(descriptor, beanName) 
            : null);
ContextAnnotationAutowireCandidateResolver->isLazy(): // --1
    // 遍历字段的注解,看是否有@Lazy注解
    for (Annotation ann : descriptor.getAnnotations()) {
        Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
        if (lazy != null && lazy.value()) {
            return true;
        }
    }
    
ContextAnnotationAutowireCandidateResolver->buildLazyResolutionProxy():
    @Override
    public Object getTarget() {
        Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
        ......
    }
    
    ProxyFactory pf = new ProxyFactory();
    pf.setTargetSource(ts);
    Class<?> dependencyType = descriptor.getDependencyType();
    if (dependencyType.isInterface()) {
        pf.addInterface(dependencyType);
    }
    return pf.getProxy(beanFactory.getBeanClassLoader());
ProxyFactory->getProxy():
    return createAopProxy().getProxy(classLoader);
ProxyCreatorSupport->createAopProxy():
    return getAopProxyFactory().createAopProxy(this);
DefaultAopProxyFactory->createAopProxy():
    // 如果是接口的话,就返回Jdk代理,否则返回Cglib代理
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        return new ObjenesisCglibAopProxy(config);
    } else {
        return new JdkDynamicAopProxy(config);
    }

@Lazy作用于字段时,返回的是动态代理,在调用字段方法时,代理类调用getTarget()方法从IoC容器中注入并获取对象,并调用对象的方法完成最终调用。

附录:io读取常见类和方法

前言:nio之ByteBuffer常见方法

文件输入流 FileInputStream

  • read() 读取一个字节,读到末尾返回-1,下同
  • read(byte b[]) 读取最多数组b长度的字节,将值填充入数组中,读完返回-1,下同
  • read(byte b[], int off, int len) 读取len长度的字节,从数组的off下标开始,填充入数组中,注意off+len不能超过数组的长度,读完返回-1,下同
  • getChannel() 返回文件通道,与NIO有关

缓冲输入流 BufferedInputStream(fileInputStream)

读取最多内置字节数组长度的字节(默认8192),将值填充入数组中,用读内置字节数组代替真实的IO

image.png

如上图所示,in是文件流,buf是内置字节数组,count是本次读取的字节长度,pos是下标,读了len个字节,则pos位移len

当pos到达count时,会再一次IO,如果count返回0,说明读取完毕,此时pos=count=0

字节数组输入流 ByteArrayInputStream(byte[])

image.png

实例化时,将一个字节数组放入内部,读取该字节数组,没有in对象,其它同缓冲输入流

如果pos和count相等,说明读取完毕

文件字符流 FileReader

以字符的形式读取文件,用读内部维护ByteBuffer代替真实的IO

image.png

如上图所示,在UTF-8编码中,一个汉字占三个字节,一个字母占一个字节,读取一次默认是两个字符。当position到达limit时,会再一次IO,如果position返回0,说明读取完毕,此时postion=limit=0

缓冲字符流 BufferedReader(fileReader)

先fileReader io填充ByteBuffer,然后从ByteBuffer中读取最多ByteBuffer长度的字节(默认8192),将字节转换成字符填充入数组中,用读内置字符数组代替真实的IO

image.png

当nextChar到达nChars时,会再一次读ByteBuffer,如果ByteBuffer读取完毕则读取完毕,此时nextChar=nChar

  • readLine() 读取一行