Small Spring IOC篇:作用域和FactoryBean

一、前言

首先我认为先要搞清楚什么是作用域,什么又是FactoryBean?

因为这两个在我们日常开发中很少用到,他们的功能不在于业务开发,而是组件开发,比如MyBatis、Dobbo 等,这些组件为了更好的融入 Spring 生态,通常需要通过实现 FactoryBean 的接口来让使用者对繁琐的创建步骤无感化。

那么我就直接先说这两个东西的概念:

作用域:

我们这里说的作用域都是基本作用域,而基本作用域只有两个,分别是单例模式和原型模式。先前我们都是通过在创建 bean 时自动存入单例缓存中,于是每次拿的bean都是同一个,也就是呆在单例工厂 map 中的那个。而原型模式相反,它是多例的,每次拿取 bean 都是一个新创建的 bean,也就是说这个就不走单例缓存了。所以实现原理也很好想象:先尝试拿缓存,拿不到再判断是不是单例,是单例就自己创建然后放到缓存中,不是单例就只创建。

FactoryBean:

顾名思义,就是工厂 Bean ,这里要和 BeanFactory 区分开,BF 是用来创建 Bean 对象的工厂,是一个工厂,而 FB 是用来生产 bean 对象的 bean,是一个 Bean。但是这个东西在日常开发中真的很少用过,提供这个接口就是专门用来给中间件作为扩展点的,举个例子,比如 MyBatis 中的 IUserMapper 接口,我们作为框架使用者,无需手动创建任何 bean ,只需要在 xml 文件中配置,就能自动连接数据库、然后执行 SQL 等一系列操作(熟悉JDBC应该对流程比较清楚),因此对于我们框架使用者来说, FactoryBean 能帮助我们更便捷的使用,但是我们在业务中很少用到。其基本功能就是:通过代工生产对象,从而简化原本由用户做的复杂流程。

二、作用域

1.新增作用域属性和解析流程

作用域是每个Bean的属性,那么我们当然就要从BeanDefinition开始新增,因此先新增作用域属性:

java 复制代码
public class BeanDefinition {

    String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;

    String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;

    private Class beanClass;

    private PropertyValues propertyValues;

    private String initMethodName;

    private String destroyMethodName;

    private String scope = SCOPE_SINGLETON;

    private boolean singleton = true;

    private boolean prototype = false;

    public BeanDefinition(Class beanClass) {
        this(beanClass, null);
    }

    public BeanDefinition(Class beanClass, PropertyValues propertyValues) {
        this.beanClass = beanClass;
        this.propertyValues = propertyValues != null ? propertyValues : new PropertyValues();
    }

    public void setScope(String scope) {
        this.scope = scope;
        this.singleton = SCOPE_SINGLETON.equals(scope);
        this.prototype = SCOPE_PROTOTYPE.equals(scope);
    }

    //setter/getter...
}

由于是 xml 配置,作用域也是在 xml 配置,因此我们当然要修改 XmlBeanDefinitionReader

,让其去识别 scope 属性并把配置信息填充到 BeanDefinition 中去:

java 复制代码
protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException {
        Document doc = XmlUtil.readXML(inputStream);
        Element root = doc.getDocumentElement();
        NodeList childNodes = root.getChildNodes();

        for (int i = 0; i < childNodes.getLength(); i++) {
            // 判断元素
            if (!(childNodes.item(i) instanceof Element)) continue;
            // 判断对象
            if (!"bean".equals(childNodes.item(i).getNodeName())) continue;

            // 解析标签
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String className = bean.getAttribute("class");
            String initMethod = bean.getAttribute("init-method");
            String destroyMethodName = bean.getAttribute("destroy-method");
            String beanScope = bean.getAttribute("scope");

            // 获取 Class,方便获取类中的名称
            Class<?> clazz = Class.forName(className);
            // 优先级 id > name
            String beanName = StrUtil.isNotEmpty(id) ? id : name;
            if (StrUtil.isEmpty(beanName)) {
                beanName = StrUtil.lowerFirst(clazz.getSimpleName());
            }

            // 定义Bean
            BeanDefinition beanDefinition = new BeanDefinition(clazz);
            beanDefinition.setInitMethodName(initMethod);
            beanDefinition.setDestroyMethodName(destroyMethodName);

            if (StrUtil.isNotEmpty(beanScope)) {
                beanDefinition.setScope(beanScope);
            }

            // 读取属性并填充
            for (int j = 0; j < bean.getChildNodes().getLength(); j++) {
                if (!(bean.getChildNodes().item(j) instanceof Element)) continue;
                if (!"property".equals(bean.getChildNodes().item(j).getNodeName())) continue;
                // 解析标签:property
                Element property = (Element) bean.getChildNodes().item(j);
                String attrName = property.getAttribute("name");
                String attrValue = property.getAttribute("value");
                String attrRef = property.getAttribute("ref");
                // 获取属性值:引入对象、值对象
                Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
                // 创建属性信息
                PropertyValue propertyValue = new PropertyValue(attrName, value);
                beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
            }
            if (getRegistry().containsBeanDefinition(beanName)) {
                throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }

2.BF中进行筛选过程

至此,BeanDefinition中就已经填充了 scope 属性了,接下来就是进入 ApplicationContext 的 refresh() 流程了,走到最后一步 BeanDefinition 就进入到 BeanFactory 维度了,在 createBean() 方法中流转。

我们不会再让原型 bean 存入单例 Map 中去了,如果存进去,后面调用就还是会走缓存了,因此需要判断一下。当然这个判断的流程我们直接写在了 BeanDefinition 中,这个就有点类似于 DDD 的充血模型了。

我们在注册销毁方法时也不能去把原型模式的 bean 注册进去,原因非常简单,因为**原型模式下的 bean 根本不会存到 singleobjectsMap 中。**换言之,这个原型 bean 创建出来后(回调、初始化结束后)就和 Spring 没有半毛钱关系了,最后只能由JVM通过GC从堆中回收。

java 复制代码
    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
            // 给 Bean 填充属性
            applyPropertyValues(beanName, bean, beanDefinition);
            // 执行 Bean 的初始化方法和 BeanPostProcessor 的前置和后置处理方法
            bean = initializeBean(beanName, bean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Instantiation of bean failed", e);
        }

        // 注册实现了 DisposableBean 接口的 Bean 对象
        registerDisposableBeanIfNecessary(beanName, bean, beanDefinition);

        // 判断 SCOPE_SINGLETON、SCOPE_PROTOTYPE
        if (beanDefinition.isSingleton()) {
            addSingleton(beanName, bean);
        }
        return bean;
    }
java 复制代码
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, BeanDefinition beanDefinition) {
        // 非 Singleton 类型的 Bean 不执行销毁方法
        if (!beanDefinition.isSingleton()) return;

        if (bean instanceof DisposableBean || StrUtil.isNotEmpty(beanDefinition.getDestroyMethodName())) {
            registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, beanDefinition));
        }
    }

至此,作用域这个新属性我们就新增成功了。

3.测试

XML 复制代码
<beans>
    <bean id="userService" class="cn.bugstack.springframework.test.bean.UserService" scope="prototype">
        <property name="uId" value="10001"/>
        <property name="company" value="腾讯"/>
        <property name="location" value="深圳"/>
        <property name="userDao" ref="proxyUserDao"/>
    </bean>

</beans>
java 复制代码
    @Test
    public void test_prototype() {
        // 1.初始化 BeanFactory
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
        applicationContext.registerShutdownHook();

        // 2. 获取Bean对象调用方法
        UserService userService01 = applicationContext.getBean("userService", UserService.class);
        UserService userService02 = applicationContext.getBean("userService", UserService.class);

        // 3. 配置 scope="prototype/singleton"
        System.out.println(userService01);
        System.out.println(userService02);

        // 4. 打印十六进制哈希
        System.out.println(userService01 + " 十六进制哈希:" + Integer.toHexString(userService01.hashCode()));
        // 官方这里还有一行 ClassLayout.parseInstance(userService01).toPrintable(),
        // 它来自 openjdk.jol(jol-cli 依赖),只用来看对象头大小,与作用域逻辑无关;
        // 本机离线环境没有该 jar,为保持模块可编译已移除。
    }
java 复制代码
cn.bugstack.springframework.test.bean.UserService$$EnhancerByCGLIB$$4cabb984@180bc464
cn.bugstack.springframework.test.bean.UserService$$EnhancerByCGLIB$$4cabb984@1324409e
cn.bugstack.springframework.test.bean.UserService$$EnhancerByCGLIB$$4cabb984@180bc464 十六进制哈希:180bc464

进程已结束,退出代码为 0

三、FactoryBean

首先要搞清楚,FB 的产物没有自己的名字,它的名字就是自己的 FB 的名字,因此存的时候的 key 就是 FB 的名字。这个先记住,不然后面会搞晕。

1.FB的应用

这里我又要自顶向下了,因为这个概念实在是在日常业务开发中比较少用到。

所以我们先看测试部分,着重看这个接口的用途是啥:

java 复制代码
public interface IUserDao {

    String queryUserName(String uId);

}

下面这个类就是一个 FB ,我们在 xml 配置中专门配置了,因此这个工厂会作为一个完全正常的 bean 注册到 Spring 的单例 map 中。

java 复制代码
public class ProxyBeanFactory implements FactoryBean<IUserDao> {

    @Override
    public IUserDao getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {

            // 添加排除方法
            if ("toString".equals(method.getName())) return this.toString();
            
            Map<String, String> hashMap = new HashMap<>();
            hashMap.put("10001", "小傅哥");
            hashMap.put("10002", "八杯水");
            hashMap.put("10003", "阿毛");
            
            return "你被代理了 " + method.getName() + ":" + hashMap.get(args[0].toString());
        };
        return (IUserDao) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IUserDao.class}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return IUserDao.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}
java 复制代码
public class UserService {

    private String uId;
    private String company;
    private String location;
    private IUserDao userDao;

    public String queryUserInfo() {
        return userDao.queryUserName(uId) + "," + company + "," + location;
    }
    //getter/setter...
}

在 xml 配置文件中,userDao 属性注入时我们引用的是 proxyUserDao 这个 FactoryBean 。当框架使用者尝试获取 userService 对象时,其实这个 userService 中的 userDao 属性对象就会由 FB 代工,调用方法时也就调用的是代理对象的方法。

XML 复制代码
<beans>
    <bean id="userService" class="cn.bugstack.springframework.test.bean.UserService" scope="prototype">
        <property name="uId" value="10001"/>
        <property name="company" value="腾讯"/>
        <property name="location" value="深圳"/>
        <property name="userDao" ref="proxyUserDao"/>
    </bean>

    <bean id="proxyUserDao" class="cn.bugstack.springframework.test.bean.ProxyBeanFactory"/>

</beans>
java 复制代码
    @Test
    public void test_factory_bean() {
        // 1.初始化 BeanFactory
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
        applicationContext.registerShutdownHook();
        // 2. 调用代理方法
        UserService userService = applicationContext.getBean("userService", UserService.class);
        System.out.println("测试结果:" + userService.queryUserInfo());
    }

2.FB代工功能实现

这个地方不能分小点,必须连贯说,因为其中的过程耦合得很紧,一个一个单独拉出来说反而很混乱,因此我将以走流程的形式讲述这个部分的内容。

依旧自顶向下,假设现在我们进行到 refresh() 的最后一步:

java 复制代码
// 6. 提前实例化单例Bean对象
beanFactory.preInstantiateSingletons();

我们接下来要把所有 bean 通过 getBean() 的形式放到单例 map 中缓存上。(因为 getBean() 的流程是先检查是否已有缓存,有就直接返回,没有就创建缓存然后返回)

正常的 bean 走这套流程肯定是没有问题的,但是如果是 FB 走就会有问题,因为FB的产物并不是放在单例 map 中的,而是自己开辟了一个 "产物 map" ------ factoryBeanObjectCache 用于专门存放所有 FB 的产物 ,而这个 "产物 map"是放在一个单独的 FactoryBeanRegistrySupport 注册对象中统一管理的。如果 FB 走同样的流程就会导致最后框架使用者调用的是 FB 本体,而非其代理的产物 bean 。

因此我们要先通过 instanceof 来筛选出 FB ,然后再跑到 factoryBeanObjectCache 中去尝试找到这个 FB 的产物,如果没有找到那就说明不在产物缓存里面,那很简单,我们去调用 FB 的 getObject() 去重新创建产物 bean 。

当然,这里又要判断产物 bean 是单例还是原型,如果是单例就存入缓存,如果是原型就不管了,每次调用这个方法我们都重新创建一个新产物 bean 。注意哈,这是两个维度,FB 的作用域 scope 属性是在 xml 中配置的,这个决定的仅仅是 FB 是否为单例,而产物的作用域则是在实现 FB 的 isSingleton() 方法中去定义的。

如果 FB 是原型,而接口方法里面重写的是单例,那最终所有的产物 bean 依旧是单例的,因为产物会存在 "产物 map" 中,每次拿的都是那一个。

java 复制代码
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {

    protected <T> T doGetBean(final String name, final Object[] args) {
        Object sharedInstance = getSingleton(name);
        if (sharedInstance != null) {
            // 如果是 FactoryBean,则需要调用 FactoryBean#getObject
            return (T) getObjectForBeanInstance(sharedInstance, name);
        }

        BeanDefinition beanDefinition = getBeanDefinition(name);
        Object bean = createBean(name, beanDefinition, args);
        return (T) getObjectForBeanInstance(bean, name);
    }  
   
    private Object getObjectForBeanInstance(Object beanInstance, String beanName) {
        if (!(beanInstance instanceof FactoryBean)) {
            return beanInstance;
        }

        Object object = getCachedObjectForFactoryBean(beanName);

        if (object == null) {
            FactoryBean<?> factoryBean = (FactoryBean<?>) beanInstance;
            object = getObjectFromFactoryBean(factoryBean, beanName);
        }

        return object;
    }
        
    // ...
}
java 复制代码
public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanRegistry {

    /**
     * Cache of singleton objects created by FactoryBeans: FactoryBean name --> object
     */
    private final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<String, Object>();

    protected Object getCachedObjectForFactoryBean(String beanName) {
        Object object = this.factoryBeanObjectCache.get(beanName);
        return (object != NULL_OBJECT ? object : null);
    }

    protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName) {
        if (factory.isSingleton()) {
            Object object = this.factoryBeanObjectCache.get(beanName);
            if (object == null) {
                object = doGetObjectFromFactoryBean(factory, beanName);
                this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
            }
            return (object != NULL_OBJECT ? object : null);
        } else {
            return doGetObjectFromFactoryBean(factory, beanName);
        }
    }

    private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName){
        try {
            return factory.getObject();
        } catch (Exception e) {
            throw new BeansException("FactoryBean threw exception on object[" + beanName + "] creation", e);
        }
    }

}

这里要挑一行出来说说:

java 复制代码
this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));

这个 NULL_OBJECT 就是个空对象,但是不是 null,这是因为 currentHashMap 不允许存放 null值,因此我们只能包一个空 Object 的壳存进去。

最后给出接口,这个接口就是用于扩展代工的了,我们会把复杂的代工流程全部塞到 getObject()方法中,从而减轻框架使用者的使用压力。

java 复制代码
public interface FactoryBean<T> {

    T getObject() throws Exception;

    Class<?> getObjectType();

    boolean isSingleton();

}
相关推荐
Shaoshing1 小时前
LeetCode100 42.接雨水
java·算法·leetcode
枫子有风1 小时前
卡码网盘项目深挖
java·redis·jwt·es·pgvector
郭小李1 小时前
springmvc 拦截器
java·开发语言·前端
大风起兮云飞扬丶1 小时前
Java——日期类
java·开发语言
Wang's Blog1 小时前
Java 项目实战:交友项目 - 联系人管理需求分析
java·开发语言·交友
没有气的汽水1 小时前
一次内存缓慢泄漏的排查:每次都 new 一个 OpenAI 客户端,问题出在事件循环上
后端
Nnnnnnn1 小时前
Kafka 踩坑实录:在 WSL2 环境下,容器死活连不上集群怎么办?
后端
lazy H2 小时前
【计算机网络】第 13 篇:NAT 和端口映射是什么?为什么本地服务外网访问不了?
java·网络·网络协议·学习·计算机网络·408
就像楞次定律2 小时前
【Linux 学习记之二】C++ 构建体系与底层链接机制的深度透视(1)
linux·后端