Spring @Import игнорирует @ConditionalOnBean, определенный в том же классе

Я пытаюсь настроить пользовательскую автоконфигурацию Spring.

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

До сих пор простой @ConditionalOnBean в импортируемой конфигурации должен делать это, как в документе четко указано:

Если класс @Configuration помечен @Conditional, все методы @Bean, аннотации @Import и аннотации @ComponentScan, связанные с этим классом, будут подчиняться условиям.

Я не хочу устанавливать @ConditionalOnBean в обычной конфигурации Spring (которой нет в модуле автоконфигурации)

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

Вот соответствующий код.

  • Пользовательский класс автоконфигурации
package myproject.lib.autoconfigure

@Configuration
@AutoConfigureAfter(RabbitAutoConfiguration.class)
@Import(ImportSpecificConfig.class)
public class ObjectStorageFacadeAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(HttpClient.class)
    public HttpClient httpClient() {
        return HttpClient.newHttpClient();
    }
    
    //a few other beans...
}
  • Конфигурация импортера:
package myproject.lib.autoconfigure

@ConditionalOnBean(ConnectionFactory.class)
@Configuration
@Import(SpecificConfig.class)
public class ImportSpecificConfig {
   @Bean
   public SomeBean aBean(){
     return new SomeBean();
   }
}
  • Старая штатная конфигурация (которая тоже импортирует кучу всего)
package other.lib

@Configuration
@Import(CommonConfiguration.class)
@EnableConfigurationProperties(ObjectStorageRabbitQueueProperties.class)
@ComponentScan
public class SpecificConfig {

}

Ожидаемое поведение для меня — не обрабатывать @Import(SpecificConfig.class) в ImportSpecificConfig.class, когда отсутствует ConnexionFactory бин. Также он не должен создавать в контексте bean-компонент SomeBean.

Затем я написал тест:

class ObjectStorageFacadeAutoConfigurationTest {
    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(ObjectStorageFacadeAutoConfiguration.class));
    @Test
    void autoConfiguredCacheManagerIsInstrumented() {
        this.contextRunner
                .run((context) -> {
                    assertThat(context).hasSingleBean(HttpClient.class);
                });
    }
}

Этот тест не проходит при запуске контекста. Утверждаю, что мне не хватает некоторых bean-компонентов в контексте, необходимом для создания экземпляра bean-компонента, определенного в... SpecificConfig package.

Итак, мое понимание (которое я мог подтвердить с помощью отладчика) заключается в том, что аннотация @Import в классе ImportSpecificConfig используется, даже если в контексте нет никакого bean-компонента, соответствующего условию @ConditionalOnBean. Это запускает аннотации @ComponentScan и @Import из старой обычной конфигурации, которая не проходит тест, потому что требуемые компоненты отсутствуют в контексте (что и предполагалось).

Кстати, SomeBean на самом деле не создается, как ожидалось.

Странно то, что если я использую @ConditionalOnBean в старом обычном классе конфигурации SpecificConfig, происходит ожидаемое поведение: @Import(CommonConfiguration.class) не активируется, и тогда у меня нет проблем с инициализацией контекста.

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

тл;др; @Import в аннотированном @Configuration классе оценивается, даже если класс @Configuration имеет @ConditionalOnBean, который оценивается как false. И bean-компонент, определенный в этом классе @Configuration, не создается, что подтверждает тот факт, что @ConditionalOnBean оценивается как false. Кажется, что @ComponentScan - настоящая проблема. Я считаю, что это не должно оцениваться, но это так.

Вот простой воспроизводимый пример https://github.com/fdeguibert/sample

Что я не понимаю? Есть ли в этом какая-то хитрость @ConditionalOnBean, которую я не вижу, или это плохое поведение?

Я не уверен, что @ConditionalOnMissingBean предполагается использовать в классе конфигурации.

burm87 10.12.2020 15:39

хм, в аннотации @ConditionalOnBean есть @Target(value = {TYPE,METHOD}), так что все должно быть в порядке. Кроме того, есть @ConditionalOnBean для нескольких классов в пакете org.springframework.boot.actuate.autoconfigure. Так что звучит нормально. (CacheMetricsAutoConfiguration например)

minioim 10.12.2020 15:48

Условия имеют две фазы: PARSE_CONFIGURATION и REGISTER_BEAN. Условия на основе бобов активны только в последнем случае. Когда ImportSpecificConfig анализируется, @ConditionalOnBean(ConnectionFactory.class) не препятствует обработке @Import(SpecificConfig.class), однако отслеживание цепочки импорта должно предотвращать регистрацию bean-компонентов, определенных в SpecificConfig. К сожалению, в вашем вопросе недостаточно подробностей, чтобы я мог определить, что происходит не так. Можете ли вы предоставить минимальный воспроизводимый пример?

Andy Wilkinson 10.12.2020 15:59

Я даю вам это как можно скорее

minioim 10.12.2020 16:15

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

minioim 10.12.2020 16:47

@AndyWilkinson Наконец-то я воспроизвел проблему. Вот минимальный воспроизводимый пример. github.com/fdeguibert/sample Добавление @ComponentScan на OldConfig вызвало проблему. Но, насколько я понимаю, этого не должно быть, поскольку его также следует фильтровать с помощью @ConditionalOnBean.... Спасибо.

minioim 10.12.2020 17:24

Настоящая беда с тестом "без TriggerBean". Он не должен активировать @ComponentScan, и тогда я не должен получать bean-компонент в контексте. Другой тест является "нормальной" ошибкой.

minioim 10.12.2020 17:30
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
7
1 417
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

После дополнительных копаний и благодаря @AndyWilkinson я понял свою ошибку.

@ConditionalOnBean фактически является фазовым условием REGISTER_BEAN. Таким образом, @Import игнорирует его, и, очевидно, @ComponentScan также игнорирует его, поскольку эта аннотация необходима на этапе PARSE_CONFIGURATION.

Итак, если я правильно понимаю, @ConditionalOnBean, примененный на уровне типа @Configuration, будет фактически использоваться Bean-компонентами, определенными в этом классе @Configuration, а не самим классом.

Хороший способ проверить это — заменить @ConditionalOnBean на @Conditional(FalseCondition.class)

И пытаюсь определить FalseCondition двумя способами:

  • первый:
public class FalseCondition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }
}

-> дать тот же результат, что и @ConditionalOnBean : оценивается @Import

  • второй :
public class FalseCondition implements ConfigurationCondition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.PARSE_CONFIGURATION;
    }
}

-> @Import не оценивается. поскольку это условие применяется на этапе настройки синтаксического анализа.

Другие вопросы по теме