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 <lkandiro@gmail.com>
This commit is contained in:
LordKay-sudo
2026-07-28 10:14:06 +02:00
committed by Moritz Halbritter
parent 39c87d595e
commit 7f8a75a6fb
7 changed files with 236 additions and 27 deletions
@@ -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<ContextConfigurationAttributes> 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();
}
}
}
@@ -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;
@@ -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=\
@@ -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");
}
}
@@ -54,25 +54,31 @@ class MeterRegistryPostProcessor implements BeanPostProcessor, SmartInitializing
private final ObjectProvider<MeterBinder> binders;
private final ObjectProvider<MetricsAutoConfiguration.MeterRegistryCloser> meterRegistryCloser;
private volatile boolean deferBinding = true;
private final Set<MeterRegistry> deferredBindings = new LinkedHashSet<>();
MeterRegistryPostProcessor(ApplicationContext applicationContext,
ObjectProvider<MetricsProperties> metricsProperties, ObjectProvider<MeterRegistryCustomizer<?>> customizers,
ObjectProvider<MeterFilter> filters, ObjectProvider<MeterBinder> binders) {
this(CompositeMeterRegistries.of(applicationContext), metricsProperties, customizers, filters, binders);
ObjectProvider<MetricsProperties> metricsProperties,
ObjectProvider<MeterRegistryCustomizer<?>> customizers, ObjectProvider<MeterFilter> filters,
ObjectProvider<MeterBinder> binders,
ObjectProvider<MetricsAutoConfiguration.MeterRegistryCloser> meterRegistryCloser) {
this(CompositeMeterRegistries.of(applicationContext), metricsProperties, customizers, filters, binders,
meterRegistryCloser);
}
MeterRegistryPostProcessor(CompositeMeterRegistries compositeMeterRegistries,
ObjectProvider<MetricsProperties> properties, ObjectProvider<MeterRegistryCustomizer<?>> customizers,
ObjectProvider<MeterFilter> filters, ObjectProvider<MeterBinder> binders) {
ObjectProvider<MeterFilter> filters, ObjectProvider<MeterBinder> binders,
ObjectProvider<MetricsAutoConfiguration.MeterRegistryCloser> 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);
}
}
@@ -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> metricsProperties,
ObjectProvider<MeterRegistryCustomizer<?>> meterRegistryCustomizers,
ObjectProvider<MeterFilter> meterFilters, ObjectProvider<MeterBinder> meterBinders) {
ObjectProvider<MeterFilter> meterFilters, ObjectProvider<MeterBinder> meterBinders,
ObjectProvider<MeterRegistryCloser> 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<MeterRegistry> meterRegistries;
private final boolean useGlobalRegistry;
private final Set<MeterRegistry> 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<MeterRegistry> meterRegistries = new LinkedHashSet<>(this.trackedRegistries);
meterRegistries.addAll(this.context.getBeansOfType(MeterRegistry.class).values());
for (MeterRegistry meterRegistry : meterRegistries) {
if (this.useGlobalRegistry) {
Metrics.globalRegistry.remove(meterRegistry);
}
@@ -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<MeterRegistryCloser> createMeterRegistryCloserProvider(MeterRegistryCloser closer) {
return new ObjectProvider<>() {
@Override
public MeterRegistryCloser getObject() {
return closer;
}
@Override
public Stream<MeterRegistryCloser> orderedStream() {
return Stream.of(closer);
}
};
}
}