Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ springJdbc = { module = "org.springframework:spring-jdbc", version.ref = "spring
springBootAutoconfigure = { module = "org.springframework.boot:spring-boot-autoconfigure", version.ref = "springBoot" }
springBootStarterValidation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "springBoot" }
springBootTest = { module = "org.springframework.boot:spring-boot-test", version.ref = "springBoot" }
springBootStarterActuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springBoot" }
assertjCore = { module = "org.assertj:assertj-core", version.ref = "assertj" }
micrometerCore = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" }
micrometerTest = { module = "io.micrometer:micrometer-test", version.ref = "micrometer" }
Expand Down
2 changes: 2 additions & 0 deletions okapi-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ dependencies {
testImplementation(libs.wiremock)
testImplementation(project(":okapi-micrometer"))
testImplementation(libs.micrometerCore)
// Brings in the metrics auto-config jar so @AutoConfigureAfter targets are resolvable in tests.
testImplementation(libs.springBootStarterActuator)
}

// CI version override: ./gradlew :okapi-spring-boot:test -PspringBootVersion=4.0.4 -PspringVersion=7.0.6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ import java.time.Clock
*/
@AutoConfiguration
@AutoConfigureAfter(
name = ["org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration"],
name = [
// Spring Boot 3.5.x
"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration",
// Spring Boot 4.0.x
"org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration",
],
)
@ConditionalOnClass(name = ["io.micrometer.core.instrument.MeterRegistry"])
@ConditionalOnBean(MeterRegistry::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import com.softwaremill.okapi.core.OutboxStore
import com.softwaremill.okapi.micrometer.MicrometerOutboxListener
import com.softwaremill.okapi.micrometer.MicrometerOutboxMetrics
import com.softwaremill.okapi.micrometer.OutboxMetricsRefresher
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.test.context.runner.ApplicationContextRunner
import org.springframework.jdbc.datasource.SimpleDriverDataSource
import java.time.Duration.ofMillis
Expand Down Expand Up @@ -90,7 +93,7 @@ class OutboxProcessorAutoConfigurationTest : FunSpec({
}
}

test("listener, metrics and refresher are wired when MeterRegistry is present") {
test("listener, metrics and refresher are wired when a MeterRegistry bean is provided directly") {
contextRunner
.withBean(io.micrometer.core.instrument.MeterRegistry::class.java, {
io.micrometer.core.instrument.simple.SimpleMeterRegistry()
Expand Down Expand Up @@ -124,8 +127,87 @@ class OutboxProcessorAutoConfigurationTest : FunSpec({
props.refreshInterval shouldBe ofSeconds(15)
}
}

// Exercises real Spring Boot metrics auto-config ordering: MeterRegistry is created by SimpleMetricsExportAutoConfiguration
// (not pre-registered as a user bean), so @AutoConfigureAfter on OkapiMicrometerAutoConfiguration must actually resolve
// and order correctly for the listener to be wired.
test("listener is wired under real Spring Boot metrics auto-config ordering") {
// Each pair lists the same auto-config in 3.5.x (`actuate.autoconfigure.metrics`) and 4.0.x (`micrometer.metrics.autoconfigure`) layouts.
val metricsAutoConfig = resolveSpringBootClass(
"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration",
"org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration",
)
val compositeMeterRegistryAutoConfig = resolveSpringBootClass(
"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration",
"org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration",
)
val simpleMetricsExportAutoConfig = resolveSpringBootClass(
"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration",
"org.springframework.boot.micrometer.metrics.autoconfigure.export.simple.SimpleMetricsExportAutoConfiguration",
)

ApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(
OutboxAutoConfiguration::class.java,
OkapiMicrometerAutoConfiguration::class.java,
metricsAutoConfig,
compositeMeterRegistryAutoConfig,
simpleMetricsExportAutoConfig,
),
)
.withBean(OutboxStore::class.java, { stubStore() })
.withBean(MessageDeliverer::class.java, { stubDeliverer() })
.withBean(DataSource::class.java, { SimpleDriverDataSource() })
.run { ctx ->
ctx.getBean(io.micrometer.core.instrument.MeterRegistry::class.java).shouldNotBeNull()
ctx.getBean(MicrometerOutboxListener::class.java).shouldNotBeNull()
ctx.getBean(MicrometerOutboxMetrics::class.java).shouldNotBeNull()
ctx.getBean(OutboxMetricsRefresher::class.java).shouldNotBeNull()
}
}

// @AutoConfigureAfter(name = ...) silently drops entries whose class is missing — if none resolve, the ordering hint is a no-op.
test("AutoConfigureAfter on OkapiMicrometerAutoConfiguration resolves on the runtime classpath") {
val annotation = OkapiMicrometerAutoConfiguration::class.java.getAnnotation(AutoConfigureAfter::class.java)
annotation.shouldNotBeNull()

val declaredNames = annotation.name.toList()
declaredNames.shouldNotBeEmpty()

val classLoader = OkapiMicrometerAutoConfiguration::class.java.classLoader
val resolvable = declaredNames.filter { name ->
try {
Class.forName(name, false, classLoader)
true
} catch (_: ClassNotFoundException) {
false
}
}

withClue(
"None of the @AutoConfigureAfter targets $declaredNames resolve on this Spring Boot runtime; " +
"the ordering hint is silently ignored and OkapiMicrometerAutoConfiguration may be evaluated " +
"before MeterRegistry is registered.",
) {
resolvable.shouldNotBeEmpty()
}
}
})

// Loads a Spring Boot auto-config class by trying version-specific FQCNs in order.
// Lets a single test exercise both the 3.5.x (`...actuate.autoconfigure.metrics...`) and 4.0.x (`...micrometer.metrics.autoconfigure...`) layouts.
private fun resolveSpringBootClass(vararg candidateFqcns: String): Class<*> {
val classLoader = OkapiMicrometerAutoConfiguration::class.java.classLoader
return candidateFqcns.firstNotNullOfOrNull { fqcn ->
try {
Class.forName(fqcn, false, classLoader)
} catch (_: ClassNotFoundException) {
null
}
} ?: error("None of $candidateFqcns resolves on this Spring Boot runtime; check spring-boot-starter-actuator on the test classpath.")
}

private fun stubStore() = object : OutboxStore {
override fun persist(entry: OutboxEntry) = entry
override fun claimPending(limit: Int) = emptyList<OutboxEntry>()
Expand Down