Skip to content

Commit ec94e07

Browse files
committed
Merge pull request #43852 from nosan
* pr/43852: Auto-configure VirtualThreadMetrics Closes gh-43852
2 parents 1e43b00 + fc5c285 commit ec94e07

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
optional("io.lettuce:lettuce-core")
3030
optional("io.micrometer:micrometer-observation")
3131
optional("io.micrometer:micrometer-jakarta9")
32+
optional("io.micrometer:micrometer-java21")
3233
optional("io.micrometer:micrometer-tracing")
3334
optional("io.micrometer:micrometer-tracing-bridge-brave")
3435
optional("io.micrometer:micrometer-tracing-bridge-otel")

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java

+72-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics;
1818

19+
import java.io.Closeable;
20+
1921
import io.micrometer.core.instrument.MeterRegistry;
2022
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
2123
import io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics;
@@ -25,12 +27,21 @@
2527
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
2628
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
2729

30+
import org.springframework.aot.hint.MemberCategory;
31+
import org.springframework.aot.hint.RuntimeHints;
32+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
33+
import org.springframework.beans.BeanUtils;
34+
import org.springframework.beans.factory.BeanClassLoaderAware;
35+
import org.springframework.beans.factory.DisposableBean;
36+
import org.springframework.beans.factory.FactoryBean;
2837
import org.springframework.boot.autoconfigure.AutoConfiguration;
2938
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3039
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3140
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3241
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3342
import org.springframework.context.annotation.Bean;
43+
import org.springframework.context.annotation.ImportRuntimeHints;
44+
import org.springframework.util.ClassUtils;
3445

3546
/**
3647
* {@link EnableAutoConfiguration Auto-configuration} for JVM metrics.
@@ -44,6 +55,8 @@
4455
@ConditionalOnBean(MeterRegistry.class)
4556
public class JvmMetricsAutoConfiguration {
4657

58+
private static final String VIRTUAL_THREAD_METRICS_CLASS = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics";
59+
4760
@Bean
4861
@ConditionalOnMissingBean
4962
public JvmGcMetrics jvmGcMetrics() {
@@ -86,4 +99,62 @@ public JvmCompilationMetrics jvmCompilationMetrics() {
8699
return new JvmCompilationMetrics();
87100
}
88101

102+
@Bean
103+
@ConditionalOnClass(name = VIRTUAL_THREAD_METRICS_CLASS)
104+
@ConditionalOnMissingBean(type = VIRTUAL_THREAD_METRICS_CLASS)
105+
@ImportRuntimeHints(VirtualThreadMetricsRuntimeHintsRegistrar.class)
106+
VirtualThreadMetricsFactoryBean virtualThreadMetrics() {
107+
return new VirtualThreadMetricsFactoryBean();
108+
}
109+
110+
static final class VirtualThreadMetricsFactoryBean
111+
implements FactoryBean<Object>, BeanClassLoaderAware, DisposableBean {
112+
113+
private ClassLoader classLoader;
114+
115+
private Class<?> instanceType;
116+
117+
private Object instance;
118+
119+
@Override
120+
public void setBeanClassLoader(ClassLoader classLoader) {
121+
this.classLoader = classLoader;
122+
}
123+
124+
@Override
125+
public Object getObject() {
126+
if (this.instance == null) {
127+
this.instance = BeanUtils.instantiateClass(getObjectType());
128+
}
129+
return this.instance;
130+
}
131+
132+
@Override
133+
public Class<?> getObjectType() {
134+
if (this.instanceType == null) {
135+
this.instanceType = ClassUtils.resolveClassName(VIRTUAL_THREAD_METRICS_CLASS, this.classLoader);
136+
}
137+
return this.instanceType;
138+
}
139+
140+
@Override
141+
public void destroy() throws Exception {
142+
if (this.instance instanceof Closeable closeable) {
143+
closeable.close();
144+
}
145+
}
146+
147+
}
148+
149+
static final class VirtualThreadMetricsRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
150+
151+
@Override
152+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
153+
hints.reflection()
154+
.registerTypeIfPresent(classLoader, VIRTUAL_THREAD_METRICS_CLASS,
155+
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
156+
}
157+
158+
}
159+
89160
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfigurationTests.java

+47-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.metrics;
1818

19+
import io.micrometer.core.instrument.binder.MeterBinder;
1920
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
2021
import io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics;
2122
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
@@ -24,14 +25,22 @@
2425
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
2526
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
2627
import org.junit.jupiter.api.Test;
27-
28+
import org.junit.jupiter.api.condition.EnabledForJreRange;
29+
import org.junit.jupiter.api.condition.JRE;
30+
31+
import org.springframework.aot.hint.MemberCategory;
32+
import org.springframework.aot.hint.RuntimeHints;
33+
import org.springframework.aot.hint.TypeReference;
34+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
35+
import org.springframework.beans.BeanUtils;
2836
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
2937
import org.springframework.boot.autoconfigure.AutoConfigurations;
3038
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
3139
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3240
import org.springframework.boot.test.context.runner.ContextConsumer;
3341
import org.springframework.context.annotation.Bean;
3442
import org.springframework.context.annotation.Configuration;
43+
import org.springframework.util.ClassUtils;
3544

3645
import static org.assertj.core.api.Assertions.assertThat;
3746

@@ -95,6 +104,36 @@ void allowsCustomJvmCompilationMetricsToBeUsed() {
95104
.run(assertMetricsBeans().andThen((context) -> assertThat(context).hasBean("customJvmCompilationMetrics")));
96105
}
97106

107+
@Test
108+
@EnabledForJreRange(min = JRE.JAVA_21)
109+
void autoConfiguresJvmMetricsWithVirtualThreadsMetrics() {
110+
this.contextRunner.run(assertMetricsBeans()
111+
.andThen((context) -> assertThat(context).hasSingleBean(getVirtualThreadMetricsClass())));
112+
}
113+
114+
@Test
115+
@EnabledForJreRange(min = JRE.JAVA_21)
116+
void allowCustomVirtualThreadMetricsToBeUsed() {
117+
Class<MeterBinder> virtualThreadMetricsClass = getVirtualThreadMetricsClass();
118+
this.contextRunner
119+
.withBean("customVirtualThreadMetrics", virtualThreadMetricsClass,
120+
() -> BeanUtils.instantiateClass(virtualThreadMetricsClass))
121+
.run(assertMetricsBeans()
122+
.andThen((context) -> assertThat(context).hasSingleBean(getVirtualThreadMetricsClass())
123+
.hasBean("customVirtualThreadMetrics")));
124+
}
125+
126+
@Test
127+
@EnabledForJreRange(min = JRE.JAVA_21)
128+
void shouldRegisterVirtualThreadMetricsRuntimeHints() {
129+
RuntimeHints hints = new RuntimeHints();
130+
new JvmMetricsAutoConfiguration.VirtualThreadMetricsRuntimeHintsRegistrar().registerHints(hints,
131+
getClass().getClassLoader());
132+
assertThat(RuntimeHintsPredicates.reflection()
133+
.onType(TypeReference.of(getVirtualThreadMetricsClass()))
134+
.withMemberCategories(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)).accepts(hints);
135+
}
136+
98137
private ContextConsumer<AssertableApplicationContext> assertMetricsBeans() {
99138
return (context) -> assertThat(context).hasSingleBean(JvmGcMetrics.class)
100139
.hasSingleBean(JvmHeapPressureMetrics.class)
@@ -105,6 +144,12 @@ private ContextConsumer<AssertableApplicationContext> assertMetricsBeans() {
105144
.hasSingleBean(JvmCompilationMetrics.class);
106145
}
107146

147+
@SuppressWarnings("unchecked")
148+
private static Class<MeterBinder> getVirtualThreadMetricsClass() {
149+
return (Class<MeterBinder>) ClassUtils
150+
.resolveClassName("io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics", null);
151+
}
152+
108153
@Configuration(proxyBeanMethods = false)
109154
static class CustomJvmGcMetricsConfiguration {
110155

0 commit comments

Comments
 (0)