From 6658309f9c9071393d356fb6c5137a2b3fde47a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Wokacz?= Date: Tue, 28 Jul 2026 17:37:17 +0200 Subject: [PATCH 1/2] fix(micrometer): add MicrometerOutboxListener to ConditionalOnClass for Okapi Spring auto configuration class --- .../OkapiMicrometerAutoConfiguration.kt | 13 ++++- .../OkapiMicrometerAutoConfigurationTest.kt | 47 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt b/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt index 364eaf7..e5acd65 100644 --- a/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt +++ b/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt @@ -39,7 +39,18 @@ import java.time.Clock "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration", ], ) -@ConditionalOnClass(name = ["io.micrometer.core.instrument.MeterRegistry"]) +@ConditionalOnClass( + name = [ + // A consuming app can easily have MeterRegistry on the classpath (e.g. via Spring Boot + // Actuator) without depending on okapi-micrometer at all -- MeterRegistry alone is not + // evidence okapi-micrometer is present. This class directly references + // MicrometerOutboxListener/MicrometerOutboxMetrics/OutboxMetricsRefresher below, so without + // this guard, Spring's condition/annotation evaluation would try to load this class on + // such a classpath and fail with NoClassDefFoundError instead of just skipping it. + "io.micrometer.core.instrument.MeterRegistry", + "com.softwaremill.okapi.micrometer.MicrometerOutboxListener", + ], +) @ConditionalOnBean(MeterRegistry::class) @EnableConfigurationProperties(OkapiMetricsProperties::class, OkapiProperties::class) class OkapiMicrometerAutoConfiguration { diff --git a/okapi-spring-boot/src/test/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfigurationTest.kt b/okapi-spring-boot/src/test/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfigurationTest.kt index cd3bbac..dbf6378 100644 --- a/okapi-spring-boot/src/test/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfigurationTest.kt +++ b/okapi-spring-boot/src/test/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfigurationTest.kt @@ -2,7 +2,9 @@ package com.softwaremill.okapi.springboot import com.softwaremill.okapi.core.OutboxStatus 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.core.spec.style.FunSpec import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull @@ -12,6 +14,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry import org.h2.jdbcx.JdbcDataSource import org.springframework.beans.factory.NoSuchBeanDefinitionException import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.test.context.FilteredClassLoader import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.context.support.GenericApplicationContext import org.springframework.jdbc.datasource.DataSourceTransactionManager @@ -27,6 +30,13 @@ import javax.sql.DataSource * regardless of `okapi.transaction-manager-qualifier`. The fix reuses * [OutboxAutoConfiguration.resolvePlatformTransactionManagerByQualifier], the same qualifier * resolution [OutboxAutoConfiguration] uses for its (required) PTM lookup. + * + * Also covers the `okapi-micrometer`-missing-from-the-classpath bug: `MeterRegistry` alone + * (`@ConditionalOnClass`'s original guard) is not evidence `okapi-micrometer` is present — plenty + * of apps have `MeterRegistry` on the classpath via Spring Boot Actuator without ever adding + * `okapi-micrometer`. [OkapiMicrometerAutoConfiguration] directly references + * [MicrometerOutboxListener] et al., so on such a classpath the class must be skipped by the + * class-level `@ConditionalOnClass`, not merely have individual `@Bean` methods fail. */ class OkapiMicrometerAutoConfigurationTest : FunSpec({ @@ -181,6 +191,43 @@ class OkapiMicrometerAutoConfigurationTest : FunSpec({ ds2Bound shouldBe false } } + + context("@ConditionalOnClass(MicrometerOutboxListener) class-level skip path (okapi-micrometer missing from classpath)") { + // MeterRegistry stays genuinely on the classpath and as a bean here (via SimpleMeterRegistry + // below and micrometer-core being a real test dependency) -- only MicrometerOutboxListener is + // hidden, isolating exactly the variable this guard is meant to catch: MeterRegistry present, + // okapi-micrometer absent. FilteredClassLoader only intercepts loadClass(), so (like the + // analogous SpringLiquibase guard test) this proves the conditional-skip mechanism, not the + // exact JVM-native NoClassDefFoundError timing a real missing-dependency classpath would hit -- + // that mechanism is what the class-level @ConditionalOnClass guard exists to trigger before + // Spring (or the JVM) ever needs to resolve MicrometerOutboxListener as a method return type. + test("FilteredClassLoader hides MicrometerOutboxListener → context loads, no okapi-micrometer beans registered") { + ApplicationContextRunner() + .withClassLoader(FilteredClassLoader(MicrometerOutboxListener::class.java)) + .withConfiguration(AutoConfigurations.of(OkapiMicrometerAutoConfiguration::class.java)) + .withBean(OutboxStore::class.java, { stubStore() }) + .withBean(MeterRegistry::class.java, { SimpleMeterRegistry() }) + .run { ctx -> + ctx.startupFailure.shouldBeNull() + ctx.getBeansOfType(MicrometerOutboxListener::class.java).isEmpty() shouldBe true + ctx.getBeansOfType(MicrometerOutboxMetrics::class.java).isEmpty() shouldBe true + ctx.getBeansOfType(OutboxMetricsRefresher::class.java).isEmpty() shouldBe true + } + } + + test("MicrometerOutboxListener present (normal classpath) → autoconfiguration still fires") { + // Sanity check the guard's positive path too, so a future typo in the class name + // string can't silently disable OkapiMicrometerAutoConfiguration on every classpath. + ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OkapiMicrometerAutoConfiguration::class.java)) + .withBean(OutboxStore::class.java, { stubStore() }) + .withBean(MeterRegistry::class.java, { SimpleMeterRegistry() }) + .run { ctx -> + ctx.startupFailure.shouldBeNull() + ctx.getBeansOfType(MicrometerOutboxListener::class.java).isEmpty() shouldBe false + } + } + } }) private fun h2DataSource(): DataSource = JdbcDataSource().apply { From 4a1b27d2183eb326bc8e7f10fbe5306d5a1c80d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Wokacz?= Date: Wed, 29 Jul 2026 09:27:06 +0200 Subject: [PATCH 2/2] fix(micrometer): micrometer metrics conditional on class check 2 additional okapi specific classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Rafał Wokacz --- .../springboot/OkapiMicrometerAutoConfiguration.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt b/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt index e5acd65..d4b4037 100644 --- a/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt +++ b/okapi-spring-boot/src/main/kotlin/com/softwaremill/okapi/springboot/OkapiMicrometerAutoConfiguration.kt @@ -43,12 +43,14 @@ import java.time.Clock name = [ // A consuming app can easily have MeterRegistry on the classpath (e.g. via Spring Boot // Actuator) without depending on okapi-micrometer at all -- MeterRegistry alone is not - // evidence okapi-micrometer is present. This class directly references - // MicrometerOutboxListener/MicrometerOutboxMetrics/OutboxMetricsRefresher below, so without - // this guard, Spring's condition/annotation evaluation would try to load this class on - // such a classpath and fail with NoClassDefFoundError instead of just skipping it. + // evidence okapi-micrometer is present. This class directly references okapi-micrometer + // types (MicrometerOutboxListener/MicrometerOutboxMetrics/OutboxMetricsRefresher) below, so + // without this guard, Spring's condition/annotation evaluation would try to load this class + // on such a classpath and fail with NoClassDefFoundError instead of just skipping it. "io.micrometer.core.instrument.MeterRegistry", "com.softwaremill.okapi.micrometer.MicrometerOutboxListener", + "com.softwaremill.okapi.micrometer.MicrometerOutboxMetrics", + "com.softwaremill.okapi.micrometer.OutboxMetricsRefresher", ], ) @ConditionalOnBean(MeterRegistry::class)