diff --git a/ff4j-spring-boot-autoconfigure-common/pom.xml b/ff4j-spring-boot-autoconfigure-common/pom.xml index 37ab5f2..1d1ea8a 100644 --- a/ff4j-spring-boot-autoconfigure-common/pom.xml +++ b/ff4j-spring-boot-autoconfigure-common/pom.xml @@ -58,6 +58,16 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-actuator + test + + + org.springframework.boot + spring-boot-starter-web + test + ${project.artifactId} diff --git a/ff4j-spring-boot-autoconfigure-common/src/main/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiConfiguration.kt b/ff4j-spring-boot-autoconfigure-common/src/main/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiConfiguration.kt index 8471e90..7ed2177 100644 --- a/ff4j-spring-boot-autoconfigure-common/src/main/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiConfiguration.kt +++ b/ff4j-spring-boot-autoconfigure-common/src/main/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiConfiguration.kt @@ -24,34 +24,52 @@ import org.slf4j.LoggerFactory import org.springdoc.core.configuration.SpringDocConfiguration import org.springdoc.core.models.GroupedOpenApi import org.springdoc.core.properties.SpringDocConfigProperties +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.ObjectProvider import org.springframework.boot.autoconfigure.AutoConfiguration import org.springframework.boot.autoconfigure.AutoConfigureAfter import org.springframework.boot.autoconfigure.condition.ConditionalOnClass import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.bind.Binder import org.springframework.context.annotation.Bean +import org.springframework.core.env.Environment +/** + * This configuration must remain instantiable with a default constructor and must not rely on + * autowired state: when `springdoc.show-actuator=true` and the management port differs from the + * server port, springdoc registers a BeanFactoryPostProcessor depending on all [GroupedOpenApi] + * beans, so this class is instantiated before autowiring and configuration-properties binding + * are available. + */ @AutoConfiguration @AutoConfigureAfter(FF4JConfiguration::class, SpringDocConfiguration::class) @ConditionalOnClass(SpringDocConfiguration::class) -class FF4JOpenApiConfiguration( - private val config: FF4JConfigurationProperties, - private val springDocConfigProperties: SpringDocConfigProperties = SpringDocConfigProperties() -) { +class FF4JOpenApiConfiguration { private val log: Logger = LoggerFactory.getLogger(FF4JOpenApiConfiguration::class.java) - init { - if (!config.api.springDoc.enabled) { - log.info("Exclude FF4J OpenAPI configuration") - springDocConfigProperties.pathsToExclude = listOf("${config.api.contextPath}/**") - } - } - @Bean @ConditionalOnProperty(name = ["ff4j.api.spring-doc.enabled"], havingValue = "true", matchIfMissing = false) - fun groupApiEnabled(springDocConfigProperties: SpringDocConfigProperties): GroupedOpenApi { + fun groupApiEnabled(environment: Environment): GroupedOpenApi { log.info("Initializing FF4J GroupedOpenApi configuration") + // Bind from the Environment instead of injecting FF4JConfigurationProperties: this bean can be + // created during the BeanFactoryPostProcessor phase, where the properties bean would exist but + // never get bound (ConfigurationPropertiesBindingPostProcessor is not registered yet). + val config = Binder.get(environment) + .bindOrCreate("ff4j", FF4JConfigurationProperties::class.java) return GroupedOpenApi.builder().group(config.api.springDoc.group).pathsToMatch("${config.api.contextPath}/**") .build() } + + @Bean + @ConditionalOnProperty(name = ["ff4j.api.spring-doc.enabled"], havingValue = "false", matchIfMissing = true) + fun ff4jOpenApiPathsExclusion( + config: FF4JConfigurationProperties, + springDocConfigProperties: ObjectProvider + ): InitializingBean = InitializingBean { + springDocConfigProperties.ifAvailable { + log.info("Exclude FF4J OpenAPI configuration") + it.pathsToExclude = listOf("${config.api.contextPath}/**") + } + } } diff --git a/ff4j-spring-boot-autoconfigure-common/src/test/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiActuatorConfigurationTest.kt b/ff4j-spring-boot-autoconfigure-common/src/test/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiActuatorConfigurationTest.kt new file mode 100644 index 0000000..adeba58 --- /dev/null +++ b/ff4j-spring-boot-autoconfigure-common/src/test/kotlin/org/ff4j/spring/boot/autoconfigure/common/FF4JOpenApiActuatorConfigurationTest.kt @@ -0,0 +1,54 @@ +/*- + * #%L + * ff4j-spring-boot-autoconfigure-common + * %% + * Copyright (C) 2013 - 2026 FF4J + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.ff4j.spring.boot.autoconfigure.common + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springdoc.core.models.GroupedOpenApi +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.TestPropertySource + +/** + * Reproduces GH issue: with `springdoc.show-actuator=true` and a management port + * different from the server port, springdoc registers a BeanFactoryPostProcessor + * depending on all GroupedOpenApi beans, forcing early instantiation of + * FF4JOpenApiConfiguration before autowiring infrastructure is ready. + */ +@SpringBootTest(classes = [Application::class]) +@TestPropertySource( + properties = [ + "ff4j.api.spring-doc.enabled=true", + "ff4j.api.spring-doc.group=ff4j-custom-group", + "springdoc.show-actuator=true", + "server.port=8080", + "management.server.port=9090" + ] +) +class FF4JOpenApiActuatorConfigurationTest { + + @Autowired + private lateinit var groupedOpenApi: GroupedOpenApi + + @Test + fun testBootWithActuatorOnDifferentPort() { + Assertions.assertThat(groupedOpenApi.group).isEqualTo("ff4j-custom-group") + } +}