From c61ff156057949bc40b79c19b02210672797b1ba Mon Sep 17 00:00:00 2001 From: LordKay-sudo Date: Mon, 29 Jun 2026 16:51:41 +0200 Subject: [PATCH] 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. 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 000000000000..d69c5da98606 --- /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 000000000000..4d2792dd73c3 --- /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 8a9b2c7d1412..69d1c39b3217 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 000000000000..fd11852f4bc7 --- /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 e3be67195b30..e90937278a1a 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 @@ private void applyFilters(MeterRegistry meterRegistry) { 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 7a4d06b7fee7..7143faf724a0 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 @@ Clock micrometerClock() { 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 @@ static class MeterRegistryCloser implements ApplicationListener 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 0cf3e28d21c2..18bc9402cb38 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.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 @@ void postProcessAndInitializeWhenUserDefinedCompositeAppliesCustomizer() { 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 @@ void postProcessAndInitializeWhenAutoConfiguredCompositeAppliesCustomizer() { 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 @@ void postProcessAndInitializeAppliesCustomizer() { 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 @@ void postProcessAndInitializeAppliesFilter() { 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 @@ void postProcessAndInitializeOnlyAppliesLmiitedFiltersToAutoConfigured() { 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 @@ void postProcessAndInitializeBindsTo() { 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 @@ void whenUserDefinedCompositeThenPostProcessAndInitializeCompositeBindsTo() { 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 @@ void whenUserDefinedCompositeThenPostProcessAndInitializeStandardRegistryDoesNot 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 @@ void whenAutoConfiguredCompositeThenPostProcessAndInitializeAutoConfiguredCompos 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 @@ void whenAutoConfiguredCompositeThenPostProcessAndInitializeCompositeDoesNotBind 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 @@ void whenAutoConfiguredCompositeThenPostProcessAndInitializeStandardRegistryDoes 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 @@ void postProcessAndInitializeIsOrderedCustomizerThenFilterThenBindTo() { 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 @@ void postProcessAndInitializeIsOrderedCustomizerThenFilterThenBindTo() { 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 @@ void postProcessAndInitializeWhenUseGlobalRegistryFalseDoesNotAddToGlobalRegistr 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 @@ void postProcessDoesNotBindToUntilSingletonsInitialized() { 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 @@ public Stream orderedStream() { }; } + private ObjectProvider createMeterRegistryCloserProvider(MeterRegistryCloser closer) { + return new ObjectProvider<>() { + + @Override + public MeterRegistryCloser getObject() { + return closer; + } + + @Override + public Stream orderedStream() { + return Stream.of(closer); + } + + }; + } + }