【小家Spring】Spring IoC是如何使用BeanWrapper和Java内省结合起来给Bean属性赋值的

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【小家Spring】Spring IoC是如何使用BeanWrapper和Java内省结合起来给Bean属性赋值的前言书写此篇博文的缘由是出自一道面试题:面试题目大概如标题所述。我个人认为这道面试题问得是非常有水平的,因为它涉及到的知识点既有深度,又有广度,可谓一举两得~~~因此在这里分享给大家。

【小家Spring】Spring IoC是如何使用BeanWrapper和Java内省结合起来给Bean属性赋值的
前言
书写此篇博文的缘由是出自一道面试题:面试题目大概如标题所述。
我个人认为这道面试题问得是非常有水平的,因为它涉及到的知识点既有深度,又有广度,可谓一举两得~~~因此在这里分享给大家。

为了给此文做铺垫,前面已经有两篇文章分别叙述了Java内省和BeanWrapper,而且还分析了底层接口:属性访问器(PropertyAccessor)。若对此部分还不是很了解的话,建议可以先出门左拐或者单击【相关阅读】里的链接~

Spring IoC和Java内省的依赖关系说明
Spring需要依赖注入就需要使用BeanWrapper,上章节说了BeanWrapperImpl的实现大都委托给了CachedIntrospectionResults去完成,而CachedIntrospectionResults它的核心说法就是Java内省机制。

从层层委托的依赖关系可以看出,Spring IoC的依赖注入(给属性赋值)是层层委托的最终给了Java内省机制,这是Spring框架设计精妙处之一。这也符合我上文所诉:BeanWrapper这个接口并不建议应用自己去直接使用~~~
那么本文就着眼于此,结合源码去分析Spring IoC容器它使用BeanWrapper完成属性赋值(依赖注入)之精华~

Spring IoC中使用BeanWrapper源码分析
Spring IoC我相信小伙伴并不陌生了,但IoC的细节不是本文的重点。为了便于分析,我把这个过程画一个时序图描述如下:
在这里插入图片描述
有了这个简略的时序图,接下来就一步一步的分析吧

doCreateBean() 创建Bean
任何创建Bean的过程,都得经历doCreateBean()。这句代码我们已经非常熟悉了,它在AbstractAutowireCapableBeanFactory里:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
    ...
    // 这一步简单的说:通过构造函数实例化Bean后,new BeanWrapperImpl(beanInstance)包装起来
    // 并且:initBeanWrapper(bw);  作用是注册ConversionService和registerCustomEditors() ...
    instanceWrapper = createBeanInstance(beanName, mbd, args);
    ...
    // 给属性赋值:此处会实施BeanWrapper的真正实力~~~~
    // 注意:此处第三个参数传入的是BeanWrapper,而不是源生beanduixiang~~~ 
    populateBean(beanName, mbd, instanceWrapper);
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    ...
}

doCreateBean这个方法完成整个Bean的实例化、初始化。而这里面我们最为关注的自然就是populateBean()这个方法,它的作用是完成给属性赋值,从时序图中也可以看出这是一个入口

populateBean():给Bean的属性赋值~

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    ...
    // 从Bean定义里面把准备好的值都拿出来~~~
    // 它是个MutablePropertyValues,持有N多个属性的值~~~
    PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
    ...
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        ...
        // 此处会从后置处理,从里面把依赖的属性,值都拿到。比如大名鼎鼎的AutowiredAnnotationBeanPostProcessor就是在此处拿出值的~~~
        PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
        ...
        pvs = pvsToUse;
    }
    ...
    // 若存在属性pvs ,那就做赋值操作吧~~~(本处才是今天关心的重点~~~)
    if (pvs != null) {
        applyPropertyValues(beanName, mbd, bw, pvs);
    }
}

深入到方法内部,它完成了k-v值的准备工作,很多重要的BeanPostProcessor也在此处得到执行。对于最终给属性赋值的步骤,是交给了本类的applyPropertyValues()方法去完成~~~

其实到了此处,理论上小伙伴就应该就能猜到接下来的核心下文了~

applyPropertyValues():完成属性赋值
这个方法的处理内容才是本文最应该关注的核心,它在处理数据解析、转换这一块还是存在不小的复杂度的~

// 本方法传入了beanName和bean定义信息,以及它对应的BeanWrapper和value值们~
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
    if (pvs.isEmpty()) {
        return;
    }
    ...
    MutablePropertyValues mpvs = null;
    List<PropertyValue> original;

    // 说明一下:为何这里还是要判断一下,虽然Spring对PropertyValues的内建实现只有MutablePropertyValues
    // 但是这个是调用者自己也可以实现逻辑的~~~so判断一下最佳~~~~
    if (pvs instanceof MutablePropertyValues) {
        mpvs = (MutablePropertyValues) pvs;
        // 此处有个短路处理:
        // 若该mpvs中的所有属性值都已经转换为对应的类型,则把mpvs设置到BeanWrapper中,返回
        if (mpvs.isConverted()) {
            // Shortcut: use the pre-converted values as-is.
            try {
                bw.setPropertyValues(mpvs);
                return;
            } catch (BeansException ex) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Error setting property values", ex);
            }
        }
        // 否则,拿到里面的属性值们~~~
        original = mpvs.getPropertyValueList();
    } else {
        original = Arrays.asList(pvs.getPropertyValues());
    }

    // 显然,若调用者没有自定义转换器,那就使用BeanWrapper本身~~~(因为BeanWrapper实现了TypeConverter 接口~~)
    TypeConverter converter = getCustomTypeConverter();
    if (converter == null) {
        converter = bw;
    }
    
    // 获取BeanDefinitionValueResolver,该Bean用于将bean定义对象中包含的值解析为应用于目标bean实例的实际值。
    BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter);

    // Create a deep copy, resolving any references for values.
    // 此处翻译成深度拷贝不合适,倒不如翻译成深度解析更为合理~~~~
    List<PropertyValue> deepCopy = new ArrayList<>(original.size());
    boolean resolveNecessary = false;

    // 遍历没有被解析的original属性值们~~~~
    for (PropertyValue pv : original) {
        if (pv.isConverted()) {
            deepCopy.add(pv);
        } else { // 那种还没被解析过的PropertyValue此处会一步步解析~~~~
            String propertyName = pv.getName(); // 属性名称
            Object originalValue = pv.getValue(); // 未经类型转换的值(注意:是未经转换的,可能还只是个字符串或者表达式而已~~~~)

            // 最为复杂的解析逻辑~~~
            Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
            Object convertedValue = resolvedValue;

            // 属性可写 并且 不是嵌套(如foo.bar,java中用getFoo().getBar()表示)或者索引(如person.addresses[0])属性
            boolean convertible = bw.isWritableProperty(propertyName) && !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);
            if (convertible) {
                // 用类型转换器进行转换
                convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter);
            }
            if (resolvedValue == originalValue) {
                if (convertible) {
                    pv.setConvertedValue(convertedValue);
                }
                deepCopy.add(pv);
            } else if (convertible && originalValue instanceof TypedStringValue &&
                    !((TypedStringValue) originalValue).isDynamic() &&
                    !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) {
                pv.setConvertedValue(convertedValue);
                deepCopy.add(pv);
            }
            else {
                resolveNecessary = true;
                deepCopy.add(new PropertyValue(pv, convertedValue));
            }
        }
    }

    // 标记mpvs已经转换
    if (mpvs != null && !resolveNecessary) {
        mpvs.setConverted();
    }

    // Set our (possibly massaged) deep copy.
    // 使用转换后的值进行填充~~~~~~~~~~
    try {
        bw.setPropertyValues(new MutablePropertyValues(deepCopy));
    } catch (BeansException ex) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Error setting property values", ex);
    }

}

// 属性值的转换
@Nullable
private Object convertForProperty(@Nullable Object value, String propertyName, BeanWrapper bw, TypeConverter converter) {
    // 需要特别注意的是:convertForProperty方法是BeanWrapperImpl的实例方法,并非接口方法
    // 这个方法内部就用到了CachedIntrospectionResults,从何就和Java内省搭上了关系~~~
    if (converter instanceof BeanWrapperImpl) {
        return ((BeanWrapperImpl) converter).convertForProperty(value, propertyName);
    } else { // 自定义转换器
        PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
        MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
        return converter.convertIfNecessary(value, pd.getPropertyType(), methodParam);
    }
}

说明:BeanDefinitionValueResolver是Spring一个内建的非public类,它在上述步骤中承担了非常多的任务,具体可参考此处:BeanDefinitionValueResolver和PropertyValues

从命名中就能看出,它处理BeanDefinition的各式各样的情况,它主要是在xml配置时代起到了非常大的作用,形如这样:

<property name="referBeanName" ref="otherBeanName" />


因为我们知道在xml时代配置Bean非常的灵活:引用Bean、Map、List甚至支持SpEL等等,这一切权得益于BeanDefinitionValueResolver这个类来处理各种case~

其实在现在注解大行其道的今天,配置Bean我们大都使用@Bean来配置,它是一种工厂方法的实现,因此这个处理类的作用就被弱化了很多。但是,但是,但是,它仍旧是我们实施定制化BeanDefinition的一个有力武器~

applyPropertyValues()这一步完成之后,就彻底完成了对Bean实例属性的赋值。从中可以看到最终的赋值操作,核心依赖的就是这么一句话:

bw.setPropertyValues(new MutablePropertyValues(deepCopy))
并且从转换的逻辑我们也需要知道的是:IoC并不是100%得使用BeanWrapper的,若我们是自定义了一个转换器,其实是可以不经过Java内省机制,而是直接通过反射来实现的,当然并不建议这么去做~

总结
BeanWrapper体系相比于 Spring 中其他体系是比较简单的,它作为BeanDefinition向 Bean转换过程中的中间产物,承载了 bean 实例的包装、类型转换、属性的设置以及访问等重要作用(请不要落了访问这个重要能力)。

关于此面试题怎么去回答,如果是我主考我会这么评价回答:

能答到populateBean()这里算是对这块知识入门了
能答到applyPropertyValues()这里,那基本对此回答就比较满意了
当然若能答到:通过自定义实现一个转换器+反射实现作为实现,而绕过Java内省机制。那势必就可以加分了~

  1. 若达到自定义、个性化定义BeanDefinition这块(虽然与本问题没有必然关联),也是可以加分的(毕竟是面试而非考试~)
    原文地址https://www.cnblogs.com/fangshixiang/p/11212095.html
相关文章
|
26天前
|
人工智能 Java API
Java也能快速搭建AI应用?一文带你玩转Spring AI可落地性
Java语言凭借其成熟的生态与解决方案,特别是通过 Spring AI 框架,正迅速成为 AI 应用开发的新选择。本文将探讨如何利用 Spring AI Alibaba 构建在线聊天 AI 应用,并实现对其性能的全面可观测性。
|
1月前
|
XML Java 测试技术
Spring IOC—基于注解配置和管理Bean 万字详解(通俗易懂)
Spring 第三节 IOC——基于注解配置和管理Bean 万字详解!
157 26
|
3天前
|
Java 容器 Spring
什么是Spring IOC 和DI ?
IOC : 控制翻转 , 它把传统上由程序代码直接操控的对象的调用权交给容 器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转 移,从程序代码本身转移到了外部容器。 DI : 依赖注入,在我们创建对象的过程中,把对象依赖的属性注入到我们的类中。
|
21天前
|
人工智能 Java API
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
|
2天前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——指定项目配置文件
在实际项目中,开发环境和生产环境的配置往往不同。为简化配置切换,可通过创建 `application-dev.yml` 和 `application-pro.yml` 分别管理开发与生产环境配置,如设置不同端口(8001/8002)。在 `application.yml` 中使用 `spring.profiles.active` 指定加载的配置文件,实现环境快速切换。本节还介绍了通过配置类读取参数的方法,适用于微服务场景,提升代码可维护性。课程源码可从 [Gitee](https://gitee.com/eson15/springboot_study) 下载。
15 0
|
2天前
|
Java 微服务 Spring
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
在微服务架构中,随着业务复杂度增加,项目可能需要调用多个微服务。为避免使用`@Value`注解逐一引入配置的繁琐,可通过定义配置类(如`MicroServiceUrl`)并结合`@ConfigurationProperties`注解实现批量管理。此方法需在配置文件中设置微服务地址(如订单、用户、购物车服务),并通过`@Component`将配置类纳入Spring容器。最后,在Controller中通过`@Resource`注入配置类即可便捷使用,提升代码可维护性。
13 0
|
2天前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
11 0
|
3月前
|
XML Java 数据格式
【SpringFramework】Spring IoC-基于XML的实现
本文主要讲解SpringFramework中IoC和DI相关概念,及基于XML的实现方式。
125 69
|
2月前
|
存储 NoSQL Java
使用Java和Spring Data构建数据访问层
本文介绍了如何使用 Java 和 Spring Data 构建数据访问层的完整过程。通过创建实体类、存储库接口、服务类和控制器类,实现了对数据库的基本操作。这种方法不仅简化了数据访问层的开发,还提高了代码的可维护性和可读性。通过合理使用 Spring Data 提供的功能,可以大幅提升开发效率。
85 21
|
1月前
|
网络协议 Java Shell
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
java spring 项目若依框架启动失败,启动不了服务提示端口8080占用escription: Web server failed to start. Port 8080 was already in use. Action: Identify and stop the process that’s listening on port 8080 or configure this application to listen on another port-优雅草卓伊凡解决方案
80 7

热门文章

最新文章