From 7f8a75a6fb37b56e4b163c42b61ad1760082ede5 Mon Sep 17 00:00:00 2001 From: LordKay-sudo Date: Mon, 29 Jun 2026 16:51:41 +0200 Subject: [PATCH 1/2] Prevent Micrometer global registry from pinning application contexts Track meter registries added to Metrics.globalRegistry and remove them on context close. Disable use of the global registry in tests by default to avoid pinning cached test contexts. See gh-50886 Signed-off-by: LordKay-sudo --- ...MeterRegistryContextCustomizerFactory.java | 75 +++++++++++++++++++ .../boot/test/metrics/package-info.java | 23 ++++++ .../main/resources/META-INF/spring.factories | 3 +- ...RegistryContextCustomizerFactoryTests.java | 56 ++++++++++++++ .../MeterRegistryPostProcessor.java | 17 +++-- .../MetricsAutoConfiguration.java | 21 ++++-- .../MeterRegistryPostProcessorTests.java | 68 +++++++++++++---- 7 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java create mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java create mode 100644 core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java new file mode 100644 index 00000000000..d69c5da9860 --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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. + */ + +package org.springframework.boot.test.metrics; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ClassUtils; + +/** + * {@link ContextCustomizerFactory} to disable the use of Micrometer's + * {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in tests, + * preventing {@link io.micrometer.core.instrument.MeterRegistry meter registries} from + * pinning application contexts when many test contexts are cached. + */ +class DisableGlobalMeterRegistryContextCustomizerFactory implements ContextCustomizerFactory { + + private static final String METRICS_CLASS = "io.micrometer.core.instrument.Metrics"; + + private static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry"; + + @Override + public @Nullable ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + if (ClassUtils.isPresent(METRICS_CLASS, testClass.getClassLoader())) { + return new DisableGlobalMeterRegistryContextCustomizer(); + } + return null; + } + + static final class DisableGlobalMeterRegistryContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + ConfigurableEnvironment environment = context.getEnvironment(); + if (environment.getProperty(USE_GLOBAL_REGISTRY_PROPERTY) == null) { + TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(environment); + } + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof DisableGlobalMeterRegistryContextCustomizer; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + } + +} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java new file mode 100644 index 00000000000..4d2792dd73c --- /dev/null +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Spring Boot support for metrics testing. + */ +@NullMarked +package org.springframework.boot.test.metrics; + +import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-test/src/main/resources/META-INF/spring.factories b/core/spring-boot-test/src/main/resources/META-INF/spring.factories index 8a9b2c7d141..69d1c39b321 100644 --- a/core/spring-boot-test/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-test/src/main/resources/META-INF/spring.factories @@ -5,7 +5,8 @@ org.springframework.boot.test.context.PropertyMappingContextCustomizerFactory,\ org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\ org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactory,\ org.springframework.boot.test.http.client.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory,\ -org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory +org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory,\ +org.springframework.boot.test.metrics.DisableGlobalMeterRegistryContextCustomizerFactory # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java new file mode 100644 index 00000000000..fd11852f4bc --- /dev/null +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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. + */ + +package org.springframework.boot.test.metrics; + +import org.junit.jupiter.api.Test; +import org.springframework.test.context.MergedContextConfiguration; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DisableGlobalMeterRegistryContextCustomizerFactory}. + */ +class DisableGlobalMeterRegistryContextCustomizerFactoryTests { + + @Test + void disablesGlobalMeterRegistryByDefault() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setEnvironment(new StandardEnvironment()); + new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() + .customizeContext(context, mock(MergedContextConfiguration.class)); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) + .isEqualTo("false"); + } + + @Test + void doesNotOverrideExplicitProperty() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + StandardEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("management.metrics.use-global-registry=true").applyTo(environment); + context.setEnvironment(environment); + new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() + .customizeContext(context, mock(MergedContextConfiguration.class)); + assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) + .isEqualTo("true"); + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java index e3be67195b3..e90937278a1 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java @@ -54,25 +54,31 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing private final ObjectProvider binders; + private final ObjectProvider meterRegistryCloser; + private volatile boolean deferBinding = true; private final Set deferredBindings = new LinkedHashSet<>(); MeterRegistryPostProcessor(ApplicationContext applicationContext, - ObjectProvider metricsProperties, ObjectProvider> customizers, - ObjectProvider filters, ObjectProvider binders) { - this(CompositeMeterRegistries.of(applicationContext), metricsProperties, customizers, filters, binders); + ObjectProvider metricsProperties, + ObjectProvider> customizers, ObjectProvider filters, + ObjectProvider binders, + ObjectProvider meterRegistryCloser) { + this(CompositeMeterRegistries.of(applicationContext), metricsProperties, customizers, filters, binders, + meterRegistryCloser); } MeterRegistryPostProcessor(CompositeMeterRegistries compositeMeterRegistries, ObjectProvider properties, ObjectProvider> customizers, - ObjectProvider filters, ObjectProvider binders) { + ObjectProvider filters, ObjectProvider binders, + ObjectProvider meterRegistryCloser) { this.compositeMeterRegistries = compositeMeterRegistries; this.properties = properties; this.customizers = customizers; this.filters = filters; this.binders = binders; - + this.meterRegistryCloser = meterRegistryCloser; } @Override @@ -123,6 +129,7 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing private void addToGlobalRegistryIfNecessary(MeterRegistry meterRegistry) { if (this.properties.getObject().isUseGlobalRegistry() && !isGlobalRegistry(meterRegistry)) { Metrics.addRegistry(meterRegistry); + this.meterRegistryCloser.getObject().track(meterRegistry); } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java index 7a4d06b7fee..7143faf724a 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java @@ -16,6 +16,9 @@ package org.springframework.boot.micrometer.metrics.autoconfigure; +import java.util.LinkedHashSet; +import java.util.Set; + import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; @@ -65,9 +68,10 @@ public final class MetricsAutoConfiguration { static MeterRegistryPostProcessor meterRegistryPostProcessor(ApplicationContext applicationContext, ObjectProvider metricsProperties, ObjectProvider> meterRegistryCustomizers, - ObjectProvider meterFilters, ObjectProvider meterBinders) { + ObjectProvider meterFilters, ObjectProvider meterBinders, + ObjectProvider meterRegistryCloser) { return new MeterRegistryPostProcessor(applicationContext, metricsProperties, meterRegistryCustomizers, - meterFilters, meterBinders); + meterFilters, meterBinders, meterRegistryCloser); } @Bean @@ -102,20 +106,25 @@ public final class MetricsAutoConfiguration { private final ApplicationContext context; - private final Iterable meterRegistries; - private final boolean useGlobalRegistry; + private final Set trackedRegistries = new LinkedHashSet<>(); + MeterRegistryCloser(ApplicationContext context, boolean useGlobalRegistry) { - this.meterRegistries = context.getBeansOfType(MeterRegistry.class).values(); this.context = context; this.useGlobalRegistry = useGlobalRegistry; } + void track(MeterRegistry meterRegistry) { + this.trackedRegistries.add(meterRegistry); + } + @Override public void onApplicationEvent(ContextClosedEvent event) { if (this.context.equals(event.getApplicationContext())) { - for (MeterRegistry meterRegistry : this.meterRegistries) { + Set meterRegistries = new LinkedHashSet<>(this.trackedRegistries); + meterRegistries.addAll(this.context.getBeansOfType(MeterRegistry.class).values()); + for (MeterRegistry meterRegistry : meterRegistries) { if (this.useGlobalRegistry) { Metrics.globalRegistry.remove(meterRegistry); } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java index 0cf3e28d21c..18bc9402cb3 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java @@ -39,7 +39,9 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.micrometer.metrics.MaximumAllowableTagsMeterFilter; +import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration.MeterRegistryCloser; import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryPostProcessor.CompositeMeterRegistries; +import org.springframework.context.ApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -84,8 +86,12 @@ class MeterRegistryPostProcessorTests { @SuppressWarnings("NullAway.Init") private Config mockConfig; + private final MetricsAutoConfiguration.MeterRegistryCloser meterRegistryCloser; + MeterRegistryPostProcessorTests() { this.properties.setUseGlobalRegistry(false); + this.meterRegistryCloser = new MetricsAutoConfiguration.MeterRegistryCloser( + mock(ApplicationContext.class), true); } @Test @@ -94,7 +100,7 @@ class MeterRegistryPostProcessorTests { MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), - createObjectProvider(this.binders)); + createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); CompositeMeterRegistry composite = new CompositeMeterRegistry(); postProcessAndInitialize(processor, composite); then(this.mockCustomizer).should().customize(composite); @@ -105,7 +111,7 @@ class MeterRegistryPostProcessorTests { this.customizers.add(this.mockCustomizer); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createEmptyObjectProvider(), createObjectProvider(this.binders)); + createEmptyObjectProvider(), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, Collections.emptyList()); postProcessAndInitialize(processor, composite); @@ -118,7 +124,7 @@ class MeterRegistryPostProcessorTests { this.customizers.add(this.mockCustomizer); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockCustomizer).should().customize(this.mockRegistry); } @@ -129,7 +135,7 @@ class MeterRegistryPostProcessorTests { this.filters.add(this.mockFilter); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockConfig).should().meterFilter(this.mockFilter); } @@ -141,7 +147,7 @@ class MeterRegistryPostProcessorTests { this.filters.add(onlyOnceFilter); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, Collections.emptyList()); postProcessAndInitialize(processor, composite); @@ -156,7 +162,7 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockBinder).should().bindTo(this.mockRegistry); } @@ -167,7 +173,7 @@ class MeterRegistryPostProcessorTests { MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), - createObjectProvider(this.binders)); + createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); CompositeMeterRegistry composite = new CompositeMeterRegistry(); postProcessAndInitialize(processor, composite); then(this.mockBinder).should().bindTo(composite); @@ -179,7 +185,7 @@ class MeterRegistryPostProcessorTests { MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), createObjectProvider(this.customizers), createObjectProvider(this.filters), - createEmptyObjectProvider()); + createEmptyObjectProvider(), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockBinder).shouldHaveNoInteractions(); } @@ -189,7 +195,7 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createEmptyObjectProvider(), createObjectProvider(this.binders)); + createEmptyObjectProvider(), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, Collections.emptyList()); postProcessAndInitialize(processor, composite); @@ -201,7 +207,7 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createEmptyObjectProvider()); + createObjectProvider(this.filters), createEmptyObjectProvider(), createMeterRegistryCloserProvider(this.meterRegistryCloser)); CompositeMeterRegistry composite = new CompositeMeterRegistry(); postProcessAndInitialize(processor, composite); then(this.mockBinder).shouldHaveNoInteractions(); @@ -212,7 +218,7 @@ class MeterRegistryPostProcessorTests { given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createEmptyObjectProvider()); + createObjectProvider(this.filters), createEmptyObjectProvider(), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockBinder).shouldHaveNoInteractions(); } @@ -225,7 +231,7 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); InOrder ordered = inOrder(this.mockBinder, this.mockConfig, this.mockCustomizer); then(this.mockCustomizer).should(ordered).customize(this.mockRegistry); @@ -233,13 +239,29 @@ class MeterRegistryPostProcessorTests { then(this.mockBinder).should(ordered).bindTo(this.mockRegistry); } + @Test + void trackedRegistryIsRemovedFromGlobalRegistryOnContextClosedEvent() { + ApplicationContext applicationContext = mock(ApplicationContext.class); + MetricsAutoConfiguration.MeterRegistryCloser closer = new MetricsAutoConfiguration.MeterRegistryCloser( + applicationContext, true); + try { + Metrics.addRegistry(this.mockRegistry); + closer.track(this.mockRegistry); + closer.onApplicationEvent(new org.springframework.context.event.ContextClosedEvent(applicationContext)); + assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.mockRegistry); + } + finally { + Metrics.removeRegistry(this.mockRegistry); + } + } + @Test void postProcessAndInitializeWhenUseGlobalRegistryTrueAddsToGlobalRegistry() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.properties.setUseGlobalRegistry(true); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); try { postProcessAndInitialize(processor, this.mockRegistry); assertThat(Metrics.globalRegistry.getRegistries()).contains(this.mockRegistry); @@ -254,7 +276,7 @@ class MeterRegistryPostProcessorTests { given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.mockRegistry); } @@ -265,7 +287,7 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders)); + createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); processor.postProcessAfterInitialization(this.mockRegistry, "meterRegistry"); then(this.mockBinder).shouldHaveNoInteractions(); processor.afterSingletonsInstantiated(); @@ -307,4 +329,20 @@ class MeterRegistryPostProcessorTests { }; } + private ObjectProvider createMeterRegistryCloserProvider(MeterRegistryCloser closer) { + return new ObjectProvider<>() { + + @Override + public MeterRegistryCloser getObject() { + return closer; + } + + @Override + public Stream orderedStream() { + return Stream.of(closer); + } + + }; + } + } From 19153b69a4332bfd7ee1d2ef370b317b53a1d467 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 27 Jul 2026 13:43:40 +0200 Subject: [PATCH 2/2] Polish "Prevent Micrometer global registry from pinning application contexts" See gh-50886 --- ...MeterRegistryContextCustomizerFactory.java | 75 ------------ .../boot/test/metrics/package-info.java | 23 ---- .../main/resources/META-INF/spring.factories | 3 +- ...RegistryContextCustomizerFactoryTests.java | 56 --------- .../autoconfigure/MeterRegistryCloser.java | 75 ++++++++++++ .../MeterRegistryPostProcessor.java | 14 +-- .../MetricsAutoConfiguration.java | 49 +------- .../MeterRegistryCloserTests.java | 113 ++++++++++++++++++ .../MeterRegistryPostProcessorTests.java | 83 ++++++++----- .../MetricsAutoConfigurationTests.java | 1 - 10 files changed, 253 insertions(+), 239 deletions(-) delete mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java delete mode 100644 core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java delete mode 100644 core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java create mode 100644 module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloser.java create mode 100644 module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloserTests.java diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java deleted file mode 100644 index d69c5da9860..00000000000 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactory.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * 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 - * - * https://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. - */ - -package org.springframework.boot.test.metrics; - -import java.util.List; - -import org.jspecify.annotations.Nullable; - -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.test.context.ContextConfigurationAttributes; -import org.springframework.test.context.ContextCustomizer; -import org.springframework.test.context.ContextCustomizerFactory; -import org.springframework.test.context.MergedContextConfiguration; -import org.springframework.util.ClassUtils; - -/** - * {@link ContextCustomizerFactory} to disable the use of Micrometer's - * {@link io.micrometer.core.instrument.Metrics#globalRegistry global registry} in tests, - * preventing {@link io.micrometer.core.instrument.MeterRegistry meter registries} from - * pinning application contexts when many test contexts are cached. - */ -class DisableGlobalMeterRegistryContextCustomizerFactory implements ContextCustomizerFactory { - - private static final String METRICS_CLASS = "io.micrometer.core.instrument.Metrics"; - - private static final String USE_GLOBAL_REGISTRY_PROPERTY = "management.metrics.use-global-registry"; - - @Override - public @Nullable ContextCustomizer createContextCustomizer(Class testClass, - List configAttributes) { - if (ClassUtils.isPresent(METRICS_CLASS, testClass.getClassLoader())) { - return new DisableGlobalMeterRegistryContextCustomizer(); - } - return null; - } - - static final class DisableGlobalMeterRegistryContextCustomizer implements ContextCustomizer { - - @Override - public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - ConfigurableEnvironment environment = context.getEnvironment(); - if (environment.getProperty(USE_GLOBAL_REGISTRY_PROPERTY) == null) { - TestPropertyValues.of(USE_GLOBAL_REGISTRY_PROPERTY + "=false").applyTo(environment); - } - } - - @Override - public boolean equals(@Nullable Object obj) { - return obj instanceof DisableGlobalMeterRegistryContextCustomizer; - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } - - } - -} diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java deleted file mode 100644 index 4d2792dd73c..00000000000 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/metrics/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * 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 - * - * https://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. - */ - -/** - * Spring Boot support for metrics testing. - */ -@NullMarked -package org.springframework.boot.test.metrics; - -import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-test/src/main/resources/META-INF/spring.factories b/core/spring-boot-test/src/main/resources/META-INF/spring.factories index 69d1c39b321..8a9b2c7d141 100644 --- a/core/spring-boot-test/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-test/src/main/resources/META-INF/spring.factories @@ -5,8 +5,7 @@ org.springframework.boot.test.context.PropertyMappingContextCustomizerFactory,\ org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\ org.springframework.boot.test.context.filter.annotation.TypeExcludeFiltersContextCustomizerFactory,\ org.springframework.boot.test.http.client.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory,\ -org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory,\ -org.springframework.boot.test.metrics.DisableGlobalMeterRegistryContextCustomizerFactory +org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java deleted file mode 100644 index fd11852f4bc..00000000000 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/metrics/DisableGlobalMeterRegistryContextCustomizerFactoryTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * 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 - * - * https://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. - */ - -package org.springframework.boot.test.metrics; - -import org.junit.jupiter.api.Test; -import org.springframework.test.context.MergedContextConfiguration; - -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.env.StandardEnvironment; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link DisableGlobalMeterRegistryContextCustomizerFactory}. - */ -class DisableGlobalMeterRegistryContextCustomizerFactoryTests { - - @Test - void disablesGlobalMeterRegistryByDefault() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setEnvironment(new StandardEnvironment()); - new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() - .customizeContext(context, mock(MergedContextConfiguration.class)); - assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) - .isEqualTo("false"); - } - - @Test - void doesNotOverrideExplicitProperty() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - StandardEnvironment environment = new StandardEnvironment(); - TestPropertyValues.of("management.metrics.use-global-registry=true").applyTo(environment); - context.setEnvironment(environment); - new DisableGlobalMeterRegistryContextCustomizerFactory.DisableGlobalMeterRegistryContextCustomizer() - .customizeContext(context, mock(MergedContextConfiguration.class)); - assertThat(context.getEnvironment().getProperty("management.metrics.use-global-registry")) - .isEqualTo("true"); - } - -} diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloser.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloser.java new file mode 100644 index 00000000000..0e0dd488c13 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloser.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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. + */ + +package org.springframework.boot.micrometer.metrics.autoconfigure; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; + +/** + * Ensures that {@link MeterRegistry meter registries} are closed early in the shutdown + * process. Also unregisters the registries from the global registry if needed. + * + * @author Jon Schneider + * @author Stephane Nicoll + * @author Moritz Halbritter + * @author Michael Berry + * @author Phillip Webb + * @author Lordwill Kandiro + */ +class MeterRegistryCloser implements ApplicationListener { + + private final ApplicationContext context; + + private final Set registriesToClose = new CopyOnWriteArraySet<>(); + + private final Set registriesToRemoveFromGlobalRegistry = new CopyOnWriteArraySet<>(); + + MeterRegistryCloser(ApplicationContext context) { + this.context = context; + } + + void track(MeterRegistry meterRegistry) { + this.registriesToClose.add(meterRegistry); + } + + void trackAddedToGlobalRegistry(MeterRegistry meterRegistry) { + this.registriesToRemoveFromGlobalRegistry.add(meterRegistry); + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + if (!this.context.equals(event.getApplicationContext())) { + return; + } + for (MeterRegistry meterRegistry : this.registriesToRemoveFromGlobalRegistry) { + Metrics.removeRegistry(meterRegistry); + } + for (MeterRegistry meterRegistry : this.registriesToClose) { + if (!meterRegistry.isClosed()) { + meterRegistry.close(); + } + } + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java index e90937278a1..549b0d3bac8 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java @@ -54,17 +54,16 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing private final ObjectProvider binders; - private final ObjectProvider meterRegistryCloser; + private final ObjectProvider meterRegistryCloser; private volatile boolean deferBinding = true; private final Set deferredBindings = new LinkedHashSet<>(); MeterRegistryPostProcessor(ApplicationContext applicationContext, - ObjectProvider metricsProperties, - ObjectProvider> customizers, ObjectProvider filters, - ObjectProvider binders, - ObjectProvider meterRegistryCloser) { + ObjectProvider metricsProperties, ObjectProvider> customizers, + ObjectProvider filters, ObjectProvider binders, + ObjectProvider meterRegistryCloser) { this(CompositeMeterRegistries.of(applicationContext), metricsProperties, customizers, filters, binders, meterRegistryCloser); } @@ -72,7 +71,7 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing MeterRegistryPostProcessor(CompositeMeterRegistries compositeMeterRegistries, ObjectProvider properties, ObjectProvider> customizers, ObjectProvider filters, ObjectProvider binders, - ObjectProvider meterRegistryCloser) { + ObjectProvider meterRegistryCloser) { this.compositeMeterRegistries = compositeMeterRegistries; this.properties = properties; this.customizers = customizers; @@ -98,6 +97,7 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing } private void postProcessMeterRegistry(MeterRegistry meterRegistry) { + this.meterRegistryCloser.getObject().track(meterRegistry); // Customizers must be applied before binders, as they may add custom tags or // alter timer or summary configuration. applyCustomizers(meterRegistry); @@ -129,7 +129,7 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing private void addToGlobalRegistryIfNecessary(MeterRegistry meterRegistry) { if (this.properties.getObject().isUseGlobalRegistry() && !isGlobalRegistry(meterRegistry)) { Metrics.addRegistry(meterRegistry); - this.meterRegistryCloser.getObject().track(meterRegistry); + this.meterRegistryCloser.getObject().trackAddedToGlobalRegistry(meterRegistry); } } diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java index 7143faf724a..9f8ee142a05 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java @@ -16,13 +16,9 @@ package org.springframework.boot.micrometer.metrics.autoconfigure; -import java.util.LinkedHashSet; -import java.util.Set; - import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; @@ -38,9 +34,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.micrometer.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; -import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.annotation.Order; /** @@ -81,8 +75,8 @@ public final class MetricsAutoConfiguration { } @Bean - MeterRegistryCloser meterRegistryCloser(ApplicationContext context, MetricsProperties properties) { - return new MeterRegistryCloser(context, properties.isUseGlobalRegistry()); + MeterRegistryCloser meterRegistryCloser(ApplicationContext context) { + return new MeterRegistryCloser(context); } @Bean @@ -98,43 +92,4 @@ public final class MetricsAutoConfiguration { properties.getObservations().getIgnoredMeters().toArray(IgnoredMeters[]::new)); } - /** - * Ensures that {@link MeterRegistry meter registries} are closed early in the - * shutdown process. - */ - static class MeterRegistryCloser implements ApplicationListener { - - private final ApplicationContext context; - - private final boolean useGlobalRegistry; - - private final Set trackedRegistries = new LinkedHashSet<>(); - - MeterRegistryCloser(ApplicationContext context, boolean useGlobalRegistry) { - this.context = context; - this.useGlobalRegistry = useGlobalRegistry; - } - - void track(MeterRegistry meterRegistry) { - this.trackedRegistries.add(meterRegistry); - } - - @Override - public void onApplicationEvent(ContextClosedEvent event) { - if (this.context.equals(event.getApplicationContext())) { - Set meterRegistries = new LinkedHashSet<>(this.trackedRegistries); - meterRegistries.addAll(this.context.getBeansOfType(MeterRegistry.class).values()); - for (MeterRegistry meterRegistry : meterRegistries) { - if (this.useGlobalRegistry) { - Metrics.globalRegistry.remove(meterRegistry); - } - if (!meterRegistry.isClosed()) { - meterRegistry.close(); - } - } - } - } - - } - } diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloserTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloserTests.java new file mode 100644 index 00000000000..53c92be6e4a --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryCloserTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-present the original author or authors. + * + * 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 + * + * https://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. + */ + +package org.springframework.boot.micrometer.metrics.autoconfigure; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link MeterRegistryCloser}. + * + * @author Lordwill Kandiro + * @author Moritz Halbritter + */ +class MeterRegistryCloserTests { + + private final ApplicationContext context = mock(ApplicationContext.class); + + @Test + void trackedRegistryIsRemovedFromGlobalRegistryOnContextClosedEvent() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + MeterRegistryCloser closer = new MeterRegistryCloser(this.context); + try { + Metrics.addRegistry(meterRegistry); + closer.trackAddedToGlobalRegistry(meterRegistry); + closer.onApplicationEvent(new ContextClosedEvent(this.context)); + assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(meterRegistry); + } + finally { + Metrics.removeRegistry(meterRegistry); + } + } + + @Test + void trackedRegistryIsClosedOnContextClosedEvent() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + MeterRegistryCloser closer = new MeterRegistryCloser(this.context); + closer.track(meterRegistry); + closer.onApplicationEvent(new ContextClosedEvent(this.context)); + assertThat(meterRegistry.isClosed()).isTrue(); + } + + @Test + void trackedRegistriesAreClosedInTheOrderThatTheyWereTracked() { + MeterRegistry first = mock(MeterRegistry.class); + MeterRegistry second = mock(MeterRegistry.class); + MeterRegistry third = mock(MeterRegistry.class); + MeterRegistryCloser closer = new MeterRegistryCloser(this.context); + closer.track(first); + closer.track(second); + closer.track(third); + closer.onApplicationEvent(new ContextClosedEvent(this.context)); + InOrder ordered = inOrder(first, second, third); + then(first).should(ordered).close(); + then(second).should(ordered).close(); + then(third).should(ordered).close(); + } + + @Test + void alreadyClosedRegistryIsNotClosedAgain() { + MeterRegistry meterRegistry = mock(MeterRegistry.class); + given(meterRegistry.isClosed()).willReturn(true); + MeterRegistryCloser closer = new MeterRegistryCloser(this.context); + closer.track(meterRegistry); + closer.onApplicationEvent(new ContextClosedEvent(this.context)); + then(meterRegistry).should(never()).close(); + } + + @Test + void onApplicationEventIgnoresEventsFromOtherContexts() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + MeterRegistryCloser closer = new MeterRegistryCloser(this.context); + try { + Metrics.addRegistry(meterRegistry); + closer.track(meterRegistry); + closer.trackAddedToGlobalRegistry(meterRegistry); + ApplicationContext otherContext = mock(ApplicationContext.class); + closer.onApplicationEvent(new ContextClosedEvent(otherContext)); + assertThat(Metrics.globalRegistry.getRegistries()).contains(meterRegistry); + assertThat(meterRegistry.isClosed()).isFalse(); + } + finally { + Metrics.removeRegistry(meterRegistry); + } + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java index 18bc9402cb3..d0a5f1178f8 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessorTests.java @@ -39,9 +39,9 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.micrometer.metrics.MaximumAllowableTagsMeterFilter; -import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration.MeterRegistryCloser; import org.springframework.boot.micrometer.metrics.autoconfigure.MeterRegistryPostProcessor.CompositeMeterRegistries; import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -86,12 +86,13 @@ class MeterRegistryPostProcessorTests { @SuppressWarnings("NullAway.Init") private Config mockConfig; - private final MetricsAutoConfiguration.MeterRegistryCloser meterRegistryCloser; + private final ApplicationContext meterRegistryCloserContext = mock(ApplicationContext.class); + + private final MeterRegistryCloser meterRegistryCloser; MeterRegistryPostProcessorTests() { this.properties.setUseGlobalRegistry(false); - this.meterRegistryCloser = new MetricsAutoConfiguration.MeterRegistryCloser( - mock(ApplicationContext.class), true); + this.meterRegistryCloser = new MeterRegistryCloser(this.meterRegistryCloserContext); } @Test @@ -111,7 +112,8 @@ class MeterRegistryPostProcessorTests { this.customizers.add(this.mockCustomizer); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createEmptyObjectProvider(), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createEmptyObjectProvider(), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, Collections.emptyList()); postProcessAndInitialize(processor, composite); @@ -124,7 +126,8 @@ class MeterRegistryPostProcessorTests { this.customizers.add(this.mockCustomizer); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockCustomizer).should().customize(this.mockRegistry); } @@ -135,7 +138,8 @@ class MeterRegistryPostProcessorTests { this.filters.add(this.mockFilter); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockConfig).should().meterFilter(this.mockFilter); } @@ -147,7 +151,8 @@ class MeterRegistryPostProcessorTests { this.filters.add(onlyOnceFilter); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, Collections.emptyList()); postProcessAndInitialize(processor, composite); @@ -162,7 +167,8 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockBinder).should().bindTo(this.mockRegistry); } @@ -184,8 +190,8 @@ class MeterRegistryPostProcessorTests { given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor( CompositeMeterRegistries.ONLY_USER_DEFINED, createObjectProvider(this.properties), - createObjectProvider(this.customizers), createObjectProvider(this.filters), - createEmptyObjectProvider(), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.customizers), createObjectProvider(this.filters), createEmptyObjectProvider(), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockBinder).shouldHaveNoInteractions(); } @@ -195,7 +201,8 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createEmptyObjectProvider(), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createEmptyObjectProvider(), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); AutoConfiguredCompositeMeterRegistry composite = new AutoConfiguredCompositeMeterRegistry(Clock.SYSTEM, Collections.emptyList()); postProcessAndInitialize(processor, composite); @@ -207,7 +214,8 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createEmptyObjectProvider(), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createEmptyObjectProvider(), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); CompositeMeterRegistry composite = new CompositeMeterRegistry(); postProcessAndInitialize(processor, composite); then(this.mockBinder).shouldHaveNoInteractions(); @@ -218,7 +226,8 @@ class MeterRegistryPostProcessorTests { given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.AUTO_CONFIGURED, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createEmptyObjectProvider(), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createEmptyObjectProvider(), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); then(this.mockBinder).shouldHaveNoInteractions(); } @@ -231,7 +240,8 @@ class MeterRegistryPostProcessorTests { this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); InOrder ordered = inOrder(this.mockBinder, this.mockConfig, this.mockCustomizer); then(this.mockCustomizer).should(ordered).customize(this.mockRegistry); @@ -240,15 +250,16 @@ class MeterRegistryPostProcessorTests { } @Test - void trackedRegistryIsRemovedFromGlobalRegistryOnContextClosedEvent() { - ApplicationContext applicationContext = mock(ApplicationContext.class); - MetricsAutoConfiguration.MeterRegistryCloser closer = new MetricsAutoConfiguration.MeterRegistryCloser( - applicationContext, true); + void postProcessAndInitializeWhenUseGlobalRegistryTrueAddsToGlobalRegistry() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); + this.properties.setUseGlobalRegistry(true); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, + createObjectProvider(this.properties), createObjectProvider(this.customizers), + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); try { - Metrics.addRegistry(this.mockRegistry); - closer.track(this.mockRegistry); - closer.onApplicationEvent(new org.springframework.context.event.ContextClosedEvent(applicationContext)); - assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.mockRegistry); + postProcessAndInitialize(processor, this.mockRegistry); + assertThat(Metrics.globalRegistry.getRegistries()).contains(this.mockRegistry); } finally { Metrics.removeRegistry(this.mockRegistry); @@ -256,15 +267,17 @@ class MeterRegistryPostProcessorTests { } @Test - void postProcessAndInitializeWhenUseGlobalRegistryTrueAddsToGlobalRegistry() { + void addToGlobalRegistryIfNecessaryTracksRegistryWithMeterRegistryCloser() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.properties.setUseGlobalRegistry(true); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); try { postProcessAndInitialize(processor, this.mockRegistry); - assertThat(Metrics.globalRegistry.getRegistries()).contains(this.mockRegistry); + this.meterRegistryCloser.onApplicationEvent(new ContextClosedEvent(this.meterRegistryCloserContext)); + assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.mockRegistry); } finally { Metrics.removeRegistry(this.mockRegistry); @@ -276,18 +289,32 @@ class MeterRegistryPostProcessorTests { given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); postProcessAndInitialize(processor, this.mockRegistry); assertThat(Metrics.globalRegistry.getRegistries()).doesNotContain(this.mockRegistry); } + @Test + void postProcessMeterRegistryTracksRegistryForClosingWithMeterRegistryCloser() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); + MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, + createObjectProvider(this.properties), createObjectProvider(this.customizers), + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); + postProcessAndInitialize(processor, this.mockRegistry); + this.meterRegistryCloser.onApplicationEvent(new ContextClosedEvent(this.meterRegistryCloserContext)); + then(this.mockRegistry).should().close(); + } + @Test void postProcessDoesNotBindToUntilSingletonsInitialized() { given(this.mockRegistry.config()).willReturn(this.mockConfig); this.binders.add(this.mockBinder); MeterRegistryPostProcessor processor = new MeterRegistryPostProcessor(CompositeMeterRegistries.NONE, createObjectProvider(this.properties), createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), createMeterRegistryCloserProvider(this.meterRegistryCloser)); + createObjectProvider(this.filters), createObjectProvider(this.binders), + createMeterRegistryCloserProvider(this.meterRegistryCloser)); processor.postProcessAfterInitialization(this.mockRegistry, "meterRegistry"); then(this.mockBinder).shouldHaveNoInteractions(); processor.afterSingletonsInstantiated(); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationTests.java index 5146b80fb4a..03d42a8657f 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfigurationTests.java @@ -30,7 +30,6 @@ import io.micrometer.observation.ObservationHandler; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration.MeterRegistryCloser; import org.springframework.boot.micrometer.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean;