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")
+ }
+}