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

задняя часть Spring

1. Начните с вопроса коллеги

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

Давайте сначала посмотрим на фрагмент кода, который в то время пошёл не так:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

В этих двух фрагментах кода определены два класса службы: TestService1 и TestService2.Экземпляр TestService2 вводится в TestService1, а экземпляр TestService1 вводится в TestService2, что составляет циклическую зависимость.

Однако это не обычная циклическая зависимость, потому что был добавлен метод test1 TestService1.@Asyncаннотация.

Угадайте, что происходит, когда программа запускается и работает?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

сообщил об ошибке. . . Причина в циклической зависимости.

«Это ненаучно. Разве Spring не претендует на решение проблемы циклических зависимостей, почему он все еще появляется?»

Если немного изменить приведенный выше код:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}

Поместите метод test1 TestService1 на@AsyncПосле удаления аннотации и TestService1, и TestService2 должны внедрять экземпляры друг друга, что также представляет собой циклическую зависимость.

Но перезапустил проект и обнаружил, что он работает нормально. Почему это?

С этими двумя вопросами давайте начнем путешествие по изучению пружинных циклических зависимостей.

2. Что такое циклическая зависимость?

Циклическая зависимость. Грубо говоря, существует прямая или косвенная зависимость между одним или несколькими экземплярами объекта, и эта зависимость представляет собой циклический вызов.

Случай 1: опора на собственные прямые зависимости

Второй случай: прямая зависимость между двумя объектами

Третий случай: косвенные зависимости между несколькими объектами

Прямая циклическая зависимость в первых двух случаях относительно интуитивна и легко определяется, но третья непрямая циклическая зависимость иногда трудно идентифицировать из-за уровня глубоких вызовов бизнес-кода.

3. N сценариев циклических зависимостей

Круговые зависимости весной в основном включают следующие сценарии:

Внедрение одноэлементного сеттера

Этот метод впрыска должен быть наиболее часто используемым весной Код выглядит следующим образом:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

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

Внутри Spring есть три уровня кеша:

  • Кэш первого уровня singletonObjects, используемый для сохранения созданных, внедренных и инициализированных экземпляров bean-компонентов.
  • Кэш второго уровня EarlySingletonObjects, используемый для сохранения созданных экземпляров bean-компонентов.
  • Кэш singletonFactories L3 используется для сохранения фабрик по созданию bean-компонентов, чтобы у более поздних расширений была возможность создавать прокси-объекты.

Вот картинка, показывающая, как Spring решает циклические зависимости:Рисунок 1

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

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}
@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;

    public void test3() {
    }
}

TestService1 зависит от TestService2 и TestService3, TestService2 зависит от TestService1, а TestService3 также зависит от TestService1.

В соответствии с описанным выше процессом TestService1 может быть внедрен в TestService2, а экземпляр TestService1 получен из кэша третьего уровня.

Предполагая, что кеша второго уровня нет, процесс внедрения TestService1 в TestService3 выглядит следующим образом:фигура 2

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

Разве это не проблема?

Чтобы решить эту проблему, кеш второго уровня, представленный Spring. На рис. 1 выше экземпляр объекта TestService1 был добавлен в кеш второго уровня, и когда TestService1 внедряется в TestService3, необходимо только получить объект из кеша второго уровня.изображение 3

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

Ответ: Нет, потому что если вы хотите улучшить объект экземпляра, добавленный в кэш L3, напрямую использовать объект экземпляра невозможно.

Что весна делает для этого сценария?

Ответ находится в этом коде в методе doCreateBean класса AbstractAutowireCapableBeanFactory:

Он определяет анонимный внутренний класс и получает прокси-объект с помощью метода getEarlyBeanReference.На самом деле нижний уровень заключается в создании прокси-объекта с помощью getEarlyBeanReference класса AbstractAutoProxyCreator.

Внедрение сеттера с несколькими экземплярами

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

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

Многие говорят, что в этом случае контейнер Spring будет сообщать об ошибке при запуске, но это не так, я очень ответственно говорю вам, что программа может запускаться нормально.

Зачем?

по фактуAbstractApplicationContextКатегорияrefreshметод сообщает нам ответ, он вызоветfinishBeanFactoryInitializationметод, целью которого являетсяspringИнициализировать некоторые заранее при запуске контейнераbean. Метод и внутренние вызовыpreInstantiateSingletonsметод.

Хорошо видно, где находится красная метка: неабстрактные, одноэлементные и нелениво загружаемые классы могут быть инициализированы bean-компонентом заранее. И многие случаиSCOPE_PROTOTYPEТипы классов, а не синглтоны, не будут предварительно инициализированными bean-компонентами, поэтому программа может запускаться нормально.

Как заставить его инициализировать бины заранее?

Просто определите еще один одноэлементный класс и добавьте в него TestService1.

@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;
}

Перезапустите программу и выполните результат:

Requested bean is currently in creation: Is there an unresolvable circular reference?

Конечно же, существует круговая зависимость.

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

Внедрение конструктора

Этот метод инъекцииspring4.xДля официально рекомендуемого метода в приведенной выше версии взгляните на следующий код:

@Service
publicclass TestService1 {

    public TestService1(TestService2 testService2) {
    }
}
@Service
publicclass TestService2 {

    public TestService2(TestService1 testService1) {
    }
}

результат операции:

Requested bean is currently in creation: Is there an unresolvable circular reference?

Существует циклическая зависимость, почему?

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

Примеры внедрения установщика одного прокси-объекта

Этот метод инъекции на самом деле используется чаще, например, обычное использование:@AsyncАннотированные сценарии будут пропущены черезAOPПрокси-объекты генерируются автоматически.

То же самое относится и к проблеме моего коллеги.

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

Заранее известно, что при запуске программы будет сообщено об ошибке, и существует циклическая зависимость:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

Почему существует циклическая зависимость?

Ответ на картинке ниже:

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

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

Если вы измените имя TestService1 на TestService6 в это время, все остальное останется без изменений.

@Service
publicclass TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

Перезапустите программу снова, и это чудесным образом хорошо.

какие? Почему это?

Это начинается с загрузки загрузки фасоли весной. По умолчанию пружинные поиски рекурсивно в соответствии с полным путем файла, отсортированы по имени файла + и нагрузки. Таким образом, TestService1 загружается перед TestService2, и после изменения имени файла TestService2 загружается перед TestService6.

Почему TestService2 загружает на 1 больше, чем TestService6?

Ответ на картинке ниже:

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

Зависит от циклических зависимостей

Существует также специальный сценарий, например, нам нужно создать экземпляр Bean B до создания экземпляра Bean A. В это время мы можем использовать аннотацию @DependsOn.

@DependsOn(value = "testService2")
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

После запуска программы результат выполнения:

Circular depends-on relationship between 'testService2' and 'testService1'

В этом примере, если оба TESTSERVICE1 и TESTSERVICE2 не аннотируются с @dependson, проблем нет, но если это аннотация добавлена, будет проблемой циркулярной зависимости.

Почему это?

ответAbstractBeanFactoryКатегорияdoGetBeanВ этом коде метода:

Он проверит, имеет ли экземпляр dependOn циклическую зависимость, и выдаст исключение, если существует циклическая зависимость.

4. Как решить круговую зависимость?

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

Круговые зависимости, порожденные генерацией прокси-объектов

Существует множество решений этой проблемы циклической зависимости, в основном в том числе:

  1. использовать@LazyАннотации, отложенная загрузка
  2. использовать@DependsOnАннотация, указывающая последовательность загрузки
  3. Измените имя файла и измените порядок загрузки циклически зависимых классов.

Циклические зависимости с использованием @DependsOn

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

Несколько циклических зависимостей

Такую проблему циклической зависимости можно решить, превратив компонент в синглтон.

Циклическая зависимость конструктора

Этот тип проблемы циклической зависимости может быть решен с помощью@LazyАннотация решена.

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

Последнее слово (пожалуйста, обратите внимание, не проституируйте меня по пустякам)

Если эта статья оказалась для вас полезной или познавательной, отсканируйте QR-код и обратите внимание, ваша поддержка — самая большая мотивация для меня продолжать писать.

Попросите в один клик три ссылки: лайк, вперед и смотреть. Следите за официальной учетной записью: Су Сан говорил о технологиях и ответил в официальной учетной записи: интервью, артефакты кода, руководства по разработке и управление временем имеют большие преимущества для фанатов.Кроме того, ответьте: добавив группу, вы можете общаться и учиться со многими старших производителей BAT.