Я пытаюсь настроить пользовательскую автоконфигурацию 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
, которую я не вижу, или это плохое поведение?
хм, в аннотации @ConditionalOnBean есть @Target(value = {TYPE,METHOD})
, так что все должно быть в порядке. Кроме того, есть @ConditionalOnBean для нескольких классов в пакете org.springframework.boot.actuate.autoconfigure. Так что звучит нормально. (CacheMetricsAutoConfiguration например)
Условия имеют две фазы: PARSE_CONFIGURATION
и REGISTER_BEAN
. Условия на основе бобов активны только в последнем случае. Когда ImportSpecificConfig
анализируется, @ConditionalOnBean(ConnectionFactory.class)
не препятствует обработке @Import(SpecificConfig.class)
, однако отслеживание цепочки импорта должно предотвращать регистрацию bean-компонентов, определенных в SpecificConfig
. К сожалению, в вашем вопросе недостаточно подробностей, чтобы я мог определить, что происходит не так. Можете ли вы предоставить минимальный воспроизводимый пример?
Я даю вам это как можно скорее
хорошо, @AndyWilkinson, к сожалению, я не могу привести воспроизводимый пример, поскольку... тот, который я только что написал, на самом деле работает так, как ожидалось.... Так что я думаю, что моя проблема на самом деле скрыта в какой-то конфигурации спагетти в реальном проекте.. .. В любом случае спасибо, я должен исследовать немного больше. Если я узнаю что-нибудь, что ведет к моему поведению, и смогу это воспроизвести, я дам вам знать.
@AndyWilkinson Наконец-то я воспроизвел проблему. Вот минимальный воспроизводимый пример. github.com/fdeguibert/sample Добавление @ComponentScan на OldConfig
вызвало проблему. Но, насколько я понимаю, этого не должно быть, поскольку его также следует фильтровать с помощью @ConditionalOnBean.... Спасибо.
Настоящая беда с тестом "без TriggerBean". Он не должен активировать @ComponentScan, и тогда я не должен получать bean-компонент в контексте. Другой тест является "нормальной" ошибкой.
После дополнительных копаний и благодаря @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 не оценивается. поскольку это условие применяется на этапе настройки синтаксического анализа.
Я не уверен, что @ConditionalOnMissingBean предполагается использовать в классе конфигурации.