Нужен ли Spring кеш третьего уровня для решения циклических зависимостей?

Spring
Нужен ли Spring кеш третьего уровня для решения циклических зависимостей?

Все мы знаем, что Spring решает циклические зависимости через кеш третьего уровня, но действительно ли ему нужно использовать буфер третьего уровня для решения циклических зависимостей? Можно ли использовать только два уровня кеша? Эта статья представляет собой введение в то, как Spring использует кеш третьего уровня для решения циклических зависимостей, и проверяет, может ли двухуровневый кеш решать циклические зависимости.

круговая зависимость

Поскольку мы хотим решать циклические зависимости, мы должны знать, что такое циклические зависимости. Как показано ниже:

image-20200928145650156

Из приведенного выше рисунка мы видим, что:

  • А зависит от Б
  • В зависит от С
  • С зависит от А
public class A {
    private B b;
}

public class B {
    private C c;
}

public class C {
    private A a;
}

Эта зависимость образует замкнутый цикл, создавая ситуацию циклических зависимостей.

Вот общие шаги для неразрешенных циклических зависимостей:

  1. Создайте экземпляр A до того, как A завершит заполнение свойств и выполнение метода инициализации (@PostConstruct).
  2. Объект A обнаруживает, что ему нужно внедрить объект B, но в контейнере нет объекта B (если объект создан и внедрение свойства завершено и метод инициализации выполнен, он будет помещен в контейнер).
  3. Создайте экземпляр B до того, как B завершит заполнение свойств и выполнение метода инициализации (@PostConstruct).
  4. Объект B обнаруживает, что ему нужно внедрить объект C, но в контейнере нет объекта C.
  5. Создайте экземпляр C, который еще не завершил заполнение свойств и выполнение метода инициализации (@PostConstruct).
  6. Объект C обнаруживает, что ему нужно внедрить объект A, но в контейнере нет объекта A.
  7. Повторите шаг 1.

Кэш L3

Суть решения Spring для циклических зависимостей заключается в предварительном предоставлении объектов, а предварительно предоставленные объекты помещаются в кеш второго уровня. В следующей таблице приведено описание кэша L3:

название описывать
singletonObjects В кэше первого уровня хранятся готовые компоненты.
earlySingletonObjects В кэше второго уровня хранятся предварительно открытые компоненты bean-компонентов, которые не завершены, а внедрение свойств и метод init не завершены.
singletonFactories В кэше третьего уровня хранятся фабрики компонентов, в основном производящие компоненты, которые хранятся в кэше второго уровня.

Все bean-компоненты, управляемые Spring, в конечном итоге будут храниться в singletonObjects.Хранящиеся здесь bean-компоненты прошли все жизненные циклы (кроме уничтоженного жизненного цикла), завершены и могут использоваться пользователями.

EarlySingletonObjects хранит bean-компоненты, которые были созданы, но еще не внедрили свойства и не выполнили метод init.

singletonFactory хранит фабрики, которые производят бобы.

Экземпляры бинов уже созданы, так зачем вам фабрика, производящая бины? Собственно это связано с АОП.Если нет необходимости проксировать бины в проекте, то фабрика бинов будет напрямую возвращать тот объект, который был инстанцирован в начале.Если нужно использовать АОП для проксирования, то эта фабрика будет играть важную роль, что также является одним из вопросов, на котором необходимо сосредоточиться в данной статье.

разрешить циклические зависимости

Как Spring решает циклические зависимости с помощью представленного выше трехуровневого кеша? Здесь мы используем только круговую зависимость, образованную A и B в качестве примера:

  1. Создание экземпляра A. На данный момент A не завершил выполнение метода заполнения свойств и инициализации (@PostConstruct), а A является лишь полуфабрикатом.
  2. Создайте фабрику компонентов для A и поместите ее в singletonFactories.
  3. Обнаружение A требует внедрения объекта B, но кэши первого, второго и третьего уровней являются объектом обнаружения B.
  4. Создание экземпляра B. На данный момент B не завершил выполнение метода заполнения свойств и инициализации (@PostConstruct), а B является лишь полуфабрикатом.
  5. Создайте фабрику компонентов для B и поместите ее в singletonFactories.
  6. Обнаружено, что B необходимо внедрить объект A. В это время объект A не найден на первом и втором уровне, но объект A находится в кеше третьего уровня, а объект A получен из кеш третьего уровня, а объект А помещается в кеш второго уровня, и одновременно удаляется объект А в кеше L3. (Обратите внимание, что в настоящее время A все еще является полуфабрикатом, и методы заполнения атрибутов и инициализации еще не завершены)
  7. Внедрить объект A в объект B.
  8. Объект B завершает заполнение атрибутов, выполняет метод инициализации и помещает его в кэш первого уровня, а объект B удаляется в кэше второго уровня. (Объект B на данный момент уже является готовым продуктом)
  9. Объект A получает объект B, который внедряет объект B в объект A. (объект A получает полный объект B)
  10. Объект А завершает заполнение атрибута, выполняет метод инициализации и помещает его в кеш первого уровня, одновременно удаляя объект А из кеша второго уровня.

Разбираем весь процесс из исходного кода:

Способ создания bean находится вAbstractAutowireCapableBeanFactory::doCreateBean()

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
	
    if (instanceWrapper == null) {
        // ① 实例化对象
        instanceWrapper = this.createBeanInstance(beanName, mbd, args);
    }

    final Object bean = instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null;
    Class<?> beanType = instanceWrapper != null ? instanceWrapper.getWrappedClass() : null;
   
    // ② 判断是否允许提前暴露对象,如果允许,则直接添加一个 ObjectFactory 到三级缓存
	boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        // 添加三级缓存的方法详情在下方
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // ③ 填充属性
    this.populateBean(beanName, mbd, instanceWrapper);
    // ④ 执行初始化方法,并创建代理
    exposedObject = initializeBean(beanName, exposedObject, mbd);
   
    return exposedObject;
}

Способ добавления кеша L3 следующий:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) { // 判断一级缓存中不存在此对象
            this.singletonFactories.put(beanName, singletonFactory); // 添加至三级缓存
            this.earlySingletonObjects.remove(beanName); // 确保二级缓存没有此对象
            this.registeredSingletons.add(beanName);
        }
    }
}

@FunctionalInterface
public interface ObjectFactory<T> {
	T getObject() throws BeansException;
}

Благодаря этому коду мы можем знать, что после того, как Spring создаст экземпляр объекта, он создаст для него фабрику компонентов и добавит эту фабрику в кеш третьего уровня.

Следовательно, то, что Spring предоставляет заранее, — это не экземпляр bean-компонента, а ObjectFactory, который обертывает bean-компонент. Зачем ты это делаешь?

На самом деле это включает в себя AOP, если созданный компонент проксируется, то внедренный компонент должен быть прокси-компонентом, а не исходным компонентом. Но Spring не знает, будут ли у бина циклические зависимости в начале.Обычно (без циклических зависимостей) Spring создаст для него прокси после заполнения свойств и выполнения метода инициализации. Однако, если существует циклическая зависимость, Spring должен заранее создать для нее прокси-объект, в противном случае внедряется исходный объект, а не прокси-объект. Итак, где же должен быть создан прокси-объект заранее?

Подход Spring заключается в предварительном создании прокси-объектов в ObjectFactory. он будет выполнятьgetObject()метод получения бобов. Фактически, метод, который он фактически выполняет, выглядит следующим образом:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                // 如果需要代理,这里会返回代理对象;否则返回原始对象
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

Поскольку прокси создается заранее, чтобы избежать повторного создания прокси-объектов позже, он будет вearlyProxyReferencesОбъекты, которые были проксированы, записываются в .

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 记录已被代理的对象
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
}

Благодаря приведенному выше анализу мы можем узнать, что цель потребности Spring в трехуровневом кеше состоит в том, чтобы отложить создание прокси-объектов без циклических зависимостей, чтобы создание bean-компонентов соответствовало принципам проектирования Spring.

Как получить зависимости

Мы уже знаем роль зависимостей третьего уровня Spring, но как Spring получает зависимости при внедрении свойств?

он прошелgetSingleton()метод получения требуемого компонента.

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 一级缓存
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 二级缓存
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 三级缓存
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // Bean 工厂中获取 Bean
                    singletonObject = singletonFactory.getObject();
                    // 放入到二级缓存中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

Когда Spring заполняет свойства bean-компонента, он сначала ищет имя объекта, который необходимо внедрить, а затем выполняетgetSingleton()Метод получает объект для внедрения, и процесс получения объекта заключается в том, чтобы сначала получить его из кеша первого уровня.Если кеша первого уровня нет, он будет получен из кеша второго уровня.Если есть нет кеша второго уровня, он будет получен из кеша третьего уровня.В кеше уровня нет кеша, то он будет выполненdoCreateBean()метод создания этого bean-компонента.

Кэш L2

Теперь мы знаем, что назначение кэша третьего уровня — задержка создания прокси-объектов, потому что если нет цикла зависимостей, то нет необходимости создавать для него прокси заранее, и его можно отложить до тех пор, пока инициализация завершена.

Поскольку цель состоит только в задержке, можем ли мы не задерживать создание, а создать для него прокси-объект после завершения создания экземпляра, чтобы нам не нужен был кеш третьего уровня. следовательно,我们可以将addSingletonFactory()метод преобразования.

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) { // 判断一级缓存中不存在此对象
            object o = singletonFactory.getObject(); // 直接从工厂中获取 Bean
            this.earlySingletonObjects.put(beanName, o); // 添加至二级缓存中
            this.registeredSingletons.add(beanName);
        }
    }
}

В этом случае каждый раз при создании экземпляра компонента прокси-объект создается напрямую и добавляется в кеш второго уровня.Результаты анализов полностью нормальные,Время инициализации Spring не должно иметь большого значения, потому что, если самому bean-компоненту не нужен прокси-сервер, исходный bean-компонент возвращается напрямую, и нет необходимости проходить сложный процесс создания прокси-компонента.

в заключении

Тесты доказали, что кэш второго уровня также может разрешать циклические зависимости. Почему Spring не выбирает кеш второго уровня, а добавляет дополнительный уровень кеша?

Если Spring выбирает кеш второго уровня для решения циклической зависимости, это означает, что все bean-компоненты должны создавать для них прокси сразу после завершения создания экземпляра, а принцип проектирования Spring заключается в создании прокси-серверов для bean-компонентов после завершения инициализации. Поэтому Spring выбрал кеш третьего уровня. Однако из-за появления циклических зависимостей Spring приходится создавать прокси заранее, потому что, если прокси-объект не будет создан заранее, будет внедрен исходный объект, что вызовет ошибки.