diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index f13ca1b51d3f..a4fafba13d23 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -29,6 +29,7 @@ dependencies { optional("io.lettuce:lettuce-core") optional("io.micrometer:micrometer-observation") optional("io.micrometer:micrometer-jakarta9") + optional("io.micrometer:micrometer-java21") optional("io.micrometer:micrometer-tracing") optional("io.micrometer:micrometer-tracing-bridge-brave") optional("io.micrometer:micrometer-tracing-bridge-otel") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java index 1ac29acea7d4..d87834a43523 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 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. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.io.Closeable; + import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; import io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics; @@ -25,12 +27,21 @@ import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.util.ClassUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for JVM metrics. @@ -44,6 +55,8 @@ @ConditionalOnBean(MeterRegistry.class) public class JvmMetricsAutoConfiguration { + private static final String VIRTUAL_THREAD_METRICS_CLASS = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics"; + @Bean @ConditionalOnMissingBean public JvmGcMetrics jvmGcMetrics() { @@ -86,4 +99,62 @@ public JvmCompilationMetrics jvmCompilationMetrics() { return new JvmCompilationMetrics(); } + @Bean + @ConditionalOnClass(name = VIRTUAL_THREAD_METRICS_CLASS) + @ConditionalOnMissingBean(type = VIRTUAL_THREAD_METRICS_CLASS) + @ImportRuntimeHints(VirtualThreadMetricsRuntimeHintsRegistrar.class) + VirtualThreadMetricsFactoryBean virtualThreadMetrics() { + return new VirtualThreadMetricsFactoryBean(); + } + + static final class VirtualThreadMetricsFactoryBean + implements FactoryBean, BeanClassLoaderAware, DisposableBean { + + private ClassLoader classLoader; + + private Class instanceType; + + private Object instance; + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public Object getObject() { + if (this.instance == null) { + this.instance = BeanUtils.instantiateClass(getObjectType()); + } + return this.instance; + } + + @Override + public Class getObjectType() { + if (this.instanceType == null) { + this.instanceType = ClassUtils.resolveClassName(VIRTUAL_THREAD_METRICS_CLASS, this.classLoader); + } + return this.instanceType; + } + + @Override + public void destroy() throws Exception { + if (this.instance instanceof Closeable closeable) { + closeable.close(); + } + } + + } + + static final class VirtualThreadMetricsRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.reflection() + .registerTypeIfPresent(classLoader, VIRTUAL_THREAD_METRICS_CLASS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfigurationTests.java index 16d1e473894e..491246ce7398 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; import io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics; import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; @@ -24,7 +25,14 @@ import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; import org.junit.jupiter.api.Test; - +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.BeanUtils; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; @@ -32,6 +40,7 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -95,6 +104,36 @@ void allowsCustomJvmCompilationMetricsToBeUsed() { .run(assertMetricsBeans().andThen((context) -> assertThat(context).hasBean("customJvmCompilationMetrics"))); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void autoConfiguresJvmMetricsWithVirtualThreadsMetrics() { + this.contextRunner.run(assertMetricsBeans() + .andThen((context) -> assertThat(context).hasSingleBean(getVirtualThreadMetricsClass()))); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void allowCustomVirtualThreadMetricsToBeUsed() { + Class virtualThreadMetricsClass = getVirtualThreadMetricsClass(); + this.contextRunner + .withBean("customVirtualThreadMetrics", virtualThreadMetricsClass, + () -> BeanUtils.instantiateClass(virtualThreadMetricsClass)) + .run(assertMetricsBeans() + .andThen((context) -> assertThat(context).hasSingleBean(getVirtualThreadMetricsClass()) + .hasBean("customVirtualThreadMetrics"))); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldRegisterVirtualThreadMetricsRuntimeHints() { + RuntimeHints hints = new RuntimeHints(); + new JvmMetricsAutoConfiguration.VirtualThreadMetricsRuntimeHintsRegistrar().registerHints(hints, + getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.reflection() + .onType(TypeReference.of(getVirtualThreadMetricsClass())) + .withMemberCategories(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)).accepts(hints); + } + private ContextConsumer assertMetricsBeans() { return (context) -> assertThat(context).hasSingleBean(JvmGcMetrics.class) .hasSingleBean(JvmHeapPressureMetrics.class) @@ -105,6 +144,12 @@ private ContextConsumer assertMetricsBeans() { .hasSingleBean(JvmCompilationMetrics.class); } + @SuppressWarnings("unchecked") + private static Class getVirtualThreadMetricsClass() { + return (Class) ClassUtils + .resolveClassName("io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics", null); + } + @Configuration(proxyBeanMethods = false) static class CustomJvmGcMetricsConfiguration {