Эта статья была впервые опубликована вblog.cc1234.cc
предисловие
Самый трудоемкий пункт этой статьи — придумать хороший заголовок, который должен быть и ярким, и сдержанным, а это, оказывается, сложнее, чем сократить спрос!
Поскольку зависимости между объектами часто бывают сложными, неправильное использование может вызвать множество непредвиденных проблем.круговая зависимость(также называемыйциклическая ссылка).
Spring предоставляет нам внедрение зависимостей и поддерживает циклическое внедрение зависимостей в некоторых сценариях (внедрение одиночного компонента).
Основная цель этой статьи — проанализировать, как Spring обрабатывает циклические зависимости при создании bean-компонентов.
Я начну с того, что такое циклическая зависимость и насколько она плоха, и, наконец, рассмотрю, как Spring решает эту проблему с помощью своего исходного кода.
Циклические зависимости генерируются не только между Spring bean-компонентами, но и между системными модулями и между системами Это типичный неприятный запах, и мы должны изо всех сил стараться его избегать.
что такое циклическая зависимость
Циклические зависимости относятся кЗависимости между несколькими объектами образуют замкнутый цикл.
На следующем рисунке показана круговая зависимость, образованная двумя объектами A и B.
На следующем рисунке показана круговая зависимость, образованная несколькими объектами.
В действительности круговые зависимости могут быть не столь очевидны на первый взгляд из-за таких факторов, как глубокие зависимости и сложные отношения.
Почему следует избегать циклических зависимостей
Циклические зависимости принесут в систему много неожиданных проблем, давайте кратко обсудим
один,Круговая зависимость создает эффект домино
Другими словами, все тело двигается, если дергать за один волосок.Представьте, что камешек падает в спокойное озеро, и рябь мгновенно распространится на все вокруг.
Циклические зависимости образуют циклическую зависимость, неустойчивое изменение в какой-то точке кольца приведет к неустойчивым изменениям во всем кольце.
Фактический опыт есть
- Сложно писать тесты для кода, потому что тесты нестабильны из-за волатильности
- Трудно рефакторить, из-за взаимных зависимостей, если вы измените один, это, естественно, повлияет на другие зависимые объекты.
- Сложно поддерживать, вы не можете представить себе последствия ваших изменений
- ......
два,Циклические зависимости могут привести к переполнению памяти
Обратитесь к коду ниже
public class AService {
private BService bService = new BService();
}
public class BService {
private AService aService = new AService();
}
когда ты проходишьnew AService()
Вы получите ошибку переполнения стека при создании объекта.
если ты понимаешьJavaПоследовательность инициализации должна знать, почему возникает эта проблема.
потому что звонюnew AService()
Сначала будет выполнена инициализация атрибута bService, а инициализация bService выполнит инициализацию AService, сформировав таким образом циклический вызов, что в итоге приведет к переполнению памяти стека вызовов.
Пример циклической зависимости Spring
Ниже мы используем простой пример, чтобы показать внедрение циклической зависимости в Spring, я показываю пример циклической зависимости внедрения конструктора и внедрения поля соответственно.
-
Внедрение конструктора
@Service public class AService { private final BService bService; @Autowired public AService(BService bService) { this.BService = bService } }
@Service public class BService { private final AService aService; @Autowired public BService(AService aService) { this.aService = aService; } }
-
Полевая инъекция
@Service public class AService { @Autowired private BService bService; }
@Service public class BService { @Autowired private AService aService; }
Setter
Инъекция похожа на инъекцию Фейлда.
Если вы запустите контейнер Spring,Внедрение конструктораМетод вызовет исключение BeanCreationException, сообщая вам о наличии циклической зависимости.
Тем не менее, способ введения поля начнется нормально, и введение будет успешным.
Это показывает, что, хотя Spring может обрабатывать циклические зависимости, вы должны делать это таким образом, чтобы он мог обрабатывать предварительные условия.
Например, бин-прототип не может обрабатывать внедрение циклических зависимостей, на это нужно обратить внимание.
Метод обнаружения циклических зависимостей
Когда мы специально анализируем, как внедрение поля в Spring решает циклические зависимости, давайте посмотрим, как обнаруживать циклические зависимости.
В сценарии циклической зависимости мы можем определить следующие ограничения
- Зависимости - это структура графа
- Зависимость направлена
- Циклическая зависимость утверждает, что зависимость создает цикл
Прояснив это, мы можем знать, что суть обнаружения циклических зависимостей заключается вОпределяет, возникает ли цикл в графе, это очень простая проблема алгоритма.
использоватьHashSet
Запишите элементы, которые появляются в направлении этой зависимости по очереди, и когда есть повторяющиеся элементы, это означает, что环
, и этот повторяющийся элемент является начальной точкой кольца.
На рисунке ниже красные узлы представляют точки, которые появляются в цикле.
Взяв в качестве примера первый график, направление зависимости будет A->B->C->A , и легко обнаружить, что A является кольцевой точкой.
Как Spring обрабатывает циклические зависимости
Весна может справитьсяСинглтон БинКруговая зависимость (Полевой метод инжекции), в этом разделе мы увидим, как это делается
Во-первых, мы упрощаем жизненный цикл создания Spring Bean в два этапа: создание экземпляра -> внедрение зависимостей, как показано на следующем рисунке.
создавать экземплярэквивалентно прохождениюnew
создает конкретный объект, авнедрение зависимостиЭто эквивалентно присвоению значения свойству объекта.
Затем мы расширяем этот процесс до процесса создания двух взаимозависимых bean-компонентов, как показано на следующем рисунке.
A должен создать экземпляр B при выполнении внедрения зависимостей, а B будет создавать экземпляр A при выполнении внедрения зависимостей, формируя типичный цикл зависимостей.
Узел, который генерирует кольцо, — это B. На этапе выполнения внедрения зависимостей, если мы его «отрежем», кольца не будет, как показано на следующем рисунке.
Таким образом, циклической зависимости нет, но возникает другая проблема: B не подвергся внедрению зависимостей, то есть B неполный Что мне делать?
На данный момент A создан и поддерживается в контейнере Spring, A содержит ссылку на B, а Spring поддерживает ссылку на B без внедрения зависимостей.
Когда веснаАктивно создаватьКогда B, вы можете напрямую получить ссылку на B (процесс создания экземпляра опущен), а при выполнении внедрения зависимостей вы также можете напрямую получить ссылку на A из контейнера, чтобы B был создан.
B, удерживаемый A без внедрения зависимостей, является тем же самым ссылочным объектом в процессе отдельного создания B позже.
Show me the code
Общие положения без кода бездушны
Я нарисовал упрощенную блок-схему, чтобы показать процесс создания Bean (опуская такие события, как Spring BeanPostProcessor, Aware и т. д.), надеюсь, вы ее пройдете, а потом мы посмотрим на исходный код.
Вход прямо изgetBean(String)
метод начинается сpopulateBean
Конец, обработки для анализа циклических зависимостей достаточно
getBean(String)
даAbstractBeanFactoryметод, который внутренне вызываетdoGetBean
метод, следующий исходный код:
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
@Override
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly){
...
// #1
Object sharedInstance = getSingleton(beanName);
...
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (mbd.isSingleton()) {
// #2
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
// #3
return createBean(beanName, mbd, args);
}
});
}
...
return (T)bean;
}
}
я упростилdoGetBean
Тело метода соответствует блок-схеме, поэтому мы можем легко найти следующий вызывающий процесс
doGetBean -> getSingleton(String) -> getSingleton(String, ObjectFactory)
getSingleton
даDefaultSingletonBeanRegistryперегруженный метод
DefaultSingletonBeanRegistryподдерживал триMapКомпоненты, используемые для кэширования различных состояний, которые мы проанализируем позже.getSingleton
будет использоваться, когда
/** 维护着所有创建完成的Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** 维护着创建中Bean的ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** 维护着所有半成品的Bean */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
getSingleton(String)
вызывается перегруженный методgetSingleton(String, boolean)
, и этот метод на самом деле является реализацией bean-компонента запроса, сначала посмотрите на картинку, а затем посмотрите на код:
На рисунке мы видим следующую иерархию запросов
singletonObjects => earlySingletonObjects => singletonFactories
Объедините исходный код
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
@Override
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从singletonObjects获取已创建的Bean
Object singletonObject = this.singletonObjects.get(beanName);
// 如果没有已创建的Bean, 但是该Bean正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 从earlySingletonObjects获取已经实例化的Bean
singletonObject = this.earlySingletonObjects.get(beanName);
// 如果没有实例化的Bean, 但是参数allowEarlyReference为true
if (singletonObject == null && allowEarlyReference) {
// 从singletonFactories获取ObjectFactory
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 使用ObjectFactory获取Bean实例
singletonObject = singletonFactory.getObject();
// 保存实例, 并清理ObjectFactory
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
пройти черезgetSingleton(String)
Если bean-компонент не найден, он продолжит вызывать внизgetSingleton(String, ObjectFactory)
, это тоже перегруженный метод, исходный код такой
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
...
// 获取缓存的Bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
...
// 标记Bean在创建中
beforeSingletonCreation(beanName);
boolean newSingleton = false;
...
// 创建新的Bean, 实际就是调用createBean方法
singletonObject = singletonFactory.getObject();
newSingleton = true;
...
if (newSingleton) {
// 缓存bean
addSingleton(beanName, singletonObject);
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
Процесс очень понятен, больше никаких диаграмм рисовать не нужно, короче, если бин не может быть найден по beanName, используйте входящий ObjectFactory для создания бина.
Из самого первого фрагмента кода мы можем знать, что на самом деле вызывается метод getObject этой ObjectFactory.createBean
метод
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
// #3
return createBean(beanName, mbd, args);
}
});
createBean
даAbstractAutowireCapableBeanFactoryРеализовано, внутренне вызваноdoCreateBean
метод
doCreateBean
Он берет на себя ответственность за создание экземпляра компонента, внедрение зависимостей и т. д.
См. рисунок ниже
createBeanInstance
Отвечает за создание экземпляра объекта Bean.
addSingletonFactory
Ссылка на одноэлементный объект будет сохранена через ObjectFactory, а затем ObjectFactory будет кэширована вMapin (этот метод выполняется до внедрения зависимостей).
populateBean
В основном для выполнения инъекции зависимостей.
Ниже приведен исходный код, который в основном совпадает с приведенной выше блок-схемой.Я также отметил детали.
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
implements AutowireCapableBeanFactory {
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
...
return doCreateBean(beanName, mbdToUse, args);
}
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
...
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
// 实例化Bean
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
// 允许单例Bean的提前暴露
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 新建并缓存ObjectFactory
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
// 如果忽略BeanPostProcessor逻辑, 该方法实际就是直接返回bean对象
// 而这里的bean对象就是前面实例化的对象
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
...
// 依赖注入
populateBean(beanName, mbd, instanceWrapper);
...
}
}
Если вы внимательно прочитаете приведенный выше фрагмент кода, я полагаю, что вы нашли ключевой момент обработки циклических зависимостей в Spring.
В качестве примера мы возьмем циклическую инъекцию зависимостей A, B и нарисуем полную блок-схему инъекций.
Обратите внимание на приведенное вышежелтый узел, давайте пройдем процесс еще раз
- При создании A будетэкземпляр Апройти через
addSingleFactory
(Желтый узел) Кэширование метода, затем выполняется внедрение зависимостей B. - Внедрение пройдет через процесс создания, и, наконец, B выполнит внедрение зависимостей A.
- Поскольку на первом шаге ссылка A уже кэширована, при повторном создании A вы можете передать
getSingleton
Метод получает предварительную ссылку этого A (получает объектную фабрику, кэшированную в начале, и получает ссылку на объект через нее), так что внедрение зависимостей B завершено. - После создания B инъекция зависимости от имени A также завершается, затем A также успешно создается (на самом деле в Spring тоже есть такие шаги, как начальный, но на этот раз он не имеет отношения к теме нашего обсуждения)
Это завершает весь процесс внедрения зависимостей.
Суммировать
Пришло время снова подвести итоги.Хотя полный текст немного длинноват, обработка Spring циклических зависимостей singleton bean-компонентов не сложна, и с небольшим расширением мы также можем научиться на этой идее обработки справляться с аналогичными проблемами.
Неизбежные статьи по-прежнему оставляют много ям, таких как
- Я не объяснил подробно, почему инъекция конструктора не может обрабатывать циклические зависимости.
- Я не вдавался в подробности того, как Spring обнаруживает циклические зависимости.
- Я также не объяснил, почему bean-компоненты-прототипы не могут обрабатывать циклические зависимости.
- .....
Конечно, их можно найти в процессе создания bean-компонентов в Spring (метод getBean(String)), а детали оставляем читателю, чтобы узнать их в исходном коде.