Skip to content

Commit b7ee63f

Browse files
authored
fix reactor#1383 metrics() without Micrometer triggers NoClassDefFoundError
Reorganizes the micrometer detection into a dedicated class `Metrics`, which avoids loading the entire `FluxMetrics` class, including Micrometer-dependent code.
1 parent a0d5de1 commit b7ee63f

File tree

11 files changed

+225
-46
lines changed

11 files changed

+225
-46
lines changed

build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,21 @@ project('reactor-core') {
236236
apply plugin: 'idea' //needed to avoid IDEA seeing the jmh folder as source
237237
apply plugin: 'me.champeau.gradle.jmh'
238238

239+
sourceSets {
240+
noMicrometerTest {
241+
java.srcDir 'src/test/java'
242+
resources.srcDir 'src/test/resources'
243+
}
244+
}
245+
239246
configurations {
240247
compileOnly.extendsFrom jsr166backport
241248
testCompile.extendsFrom jsr166backport
242249
baseline
250+
noMicrometerTestRuntime {
251+
extendsFrom testRuntime
252+
exclude group:"io.micrometer", module:"micrometer-core"
253+
}
243254
}
244255

245256
dependencies {
@@ -283,6 +294,10 @@ project('reactor-core') {
283294
force = true
284295
}
285296
}
297+
298+
noMicrometerTestCompile sourceSets.main.output
299+
noMicrometerTestCompile sourceSets.test.output
300+
noMicrometerTestCompile configurations.testCompile
286301
}
287302

288303
jmh {
@@ -410,11 +425,22 @@ project('reactor-core') {
410425
}
411426
}
412427

428+
task testNoMicrometer(type: Test, group: 'verification') {
429+
testClassesDirs = sourceSets.noMicrometerTest.output.classesDirs
430+
classpath = sourceSets.noMicrometerTest.runtimeClasspath
431+
include '**/*NoMicrometerTest.*'
432+
433+
doFirst {
434+
println "Additional tests without Micrometer ($includes)"
435+
}
436+
}
437+
413438
//inherit basic test task + common configuration in root
414439
//always depend on testStaticInit, skip testNG on Travis, skip loops when not releasing
415440
//note that this way the tasks can be run individually
416441
test {
417442
dependsOn testStaticInit
443+
dependsOn testNoMicrometer
418444
if (System.env.TRAVIS != "true") {
419445
dependsOn testNG
420446
}

reactor-core/src/main/java/reactor/core/publisher/Flux.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import reactor.core.Disposable;
5454
import reactor.core.Exceptions;
5555
import reactor.core.Fuseable;
56+
import reactor.util.Metrics;
5657
import reactor.core.Scannable;
5758
import reactor.core.publisher.FluxSink.OverflowStrategy;
5859
import reactor.core.scheduler.Scheduler;
@@ -5738,7 +5739,7 @@ public final Flux<T> mergeWith(Publisher<? extends T> other) {
57385739
* @return an instrumented {@link Flux}
57395740
*/
57405741
public final Flux<T> metrics() {
5741-
if (!FluxMetrics.isMicrometerAvailable()) {
5742+
if (!Metrics.isInstrumentationAvailable()) {
57425743
return this;
57435744
}
57445745

reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
/**
4444
* Activate metrics gathering on a {@link Flux}, assuming Micrometer is on the classpath.
4545
*
46-
* @implNote Metrics.isMicrometerAvailable() test should be performed BEFORE instantiating
46+
* @implNote Metrics.isInstrumentationAvailable() test should be performed BEFORE instantiating
4747
* or referencing this class, otherwise a {@link NoClassDefFoundError} will be thrown if
4848
* Micrometer is not there.
4949
*
@@ -53,27 +53,6 @@ final class FluxMetrics<T> extends FluxOperator<T, T> {
5353

5454
private static final Logger LOGGER = Loggers.getLogger(FluxMetrics.class);
5555

56-
private static final boolean isMicrometerAvailable;
57-
58-
static {
59-
boolean micrometer;
60-
try {
61-
io.micrometer.core.instrument.Metrics.globalRegistry.getRegistries();
62-
micrometer = true;
63-
}
64-
catch (Throwable t) {
65-
micrometer = false;
66-
}
67-
isMicrometerAvailable = micrometer;
68-
}
69-
70-
/**
71-
* @return true if the Micrometer instrumentation facade is available
72-
*/
73-
static boolean isMicrometerAvailable() {
74-
return isMicrometerAvailable;
75-
}
76-
7756
//=== Constants ===
7857

7958
/**
@@ -184,8 +163,6 @@ static Tuple2<String, List<Tag>> resolveNameAndTags(Publisher<?> source) {
184163

185164
final String name;
186165
final List<Tag> tags;
187-
188-
@Nullable
189166
final MeterRegistry registryCandidate;
190167

191168
FluxMetrics(Flux<? extends T> flux) {
@@ -204,16 +181,18 @@ static Tuple2<String, List<Tag>> resolveNameAndTags(Publisher<?> source) {
204181
this.name = nameAndTags.getT1();
205182
this.tags = nameAndTags.getT2();
206183

207-
this.registryCandidate = registry;
184+
if (registry == null) {
185+
this.registryCandidate = Metrics.globalRegistry;
186+
}
187+
else {
188+
this.registryCandidate = registry;
189+
}
190+
208191
}
209192

210193
@Override
211194
public void subscribe(CoreSubscriber<? super T> actual) {
212-
MeterRegistry registry = Metrics.globalRegistry;
213-
if (registryCandidate != null) {
214-
registry = registryCandidate;
215-
}
216-
source.subscribe(new MicrometerFluxMetricsSubscriber<>(actual, registry,
195+
source.subscribe(new MicrometerFluxMetricsSubscriber<>(actual, registryCandidate,
217196
Clock.SYSTEM, this.name, this.tags));
218197
}
219198

reactor-core/src/main/java/reactor/core/publisher/FluxMetricsFuseable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
* Activate metrics gathering on a {@link Flux} (Fuseable version), assumes Micrometer is
3434
* on the classpath.
3535
*
36-
* @implNote Metrics.isMicrometerAvailable() test should be performed BEFORE instantiating
36+
* @implNote Metrics.isInstrumentationAvailable() test should be performed BEFORE instantiating
3737
* or referencing this class, otherwise a {@link NoClassDefFoundError} will be thrown if
3838
* Micrometer is not there.
3939
*

reactor-core/src/main/java/reactor/core/publisher/Mono.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import reactor.core.CoreSubscriber;
4545
import reactor.core.Disposable;
4646
import reactor.core.Fuseable;
47+
import reactor.util.Metrics;
4748
import reactor.core.Scannable;
4849
import reactor.core.scheduler.Scheduler;
4950
import reactor.core.scheduler.Scheduler.Worker;
@@ -2803,7 +2804,7 @@ public final Flux<T> mergeWith(Publisher<? extends T> other) {
28032804
* @return an instrumented {@link Mono}
28042805
*/
28052806
public final Mono<T> metrics() {
2806-
if (!FluxMetrics.isMicrometerAvailable()) {
2807+
if (!Metrics.isInstrumentationAvailable()) {
28072808
return this;
28082809
}
28092810

reactor-core/src/main/java/reactor/core/publisher/MonoMetrics.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
/**
3535
* Activate metrics gathering on a {@link Mono}, assumes Micrometer is on the classpath.
3636
*
37-
* @implNote Metrics.isMicrometerAvailable() test should be performed BEFORE instantiating
37+
* @implNote Metrics.isInstrumentationAvailable() test should be performed BEFORE instantiating
3838
* or referencing this class, otherwise a {@link NoClassDefFoundError} will be thrown if
3939
* Micrometer is not there.
4040
*
@@ -44,8 +44,6 @@ final class MonoMetrics<T> extends MonoOperator<T, T> {
4444

4545
final String name;
4646
final List<Tag> tags;
47-
48-
@Nullable
4947
final MeterRegistry meterRegistry;
5048

5149
MonoMetrics(Mono<? extends T> mono) {
@@ -64,16 +62,17 @@ final class MonoMetrics<T> extends MonoOperator<T, T> {
6462
this.name = nameAndTags.getT1();
6563
this.tags = nameAndTags.getT2();
6664

67-
this.meterRegistry = meterRegistry;
65+
if (meterRegistry == null) {
66+
this.meterRegistry = Metrics.globalRegistry;
67+
}
68+
else {
69+
this.meterRegistry = meterRegistry;
70+
}
6871
}
6972

7073
@Override
7174
public void subscribe(CoreSubscriber<? super T> actual) {
72-
MeterRegistry registry = Metrics.globalRegistry;
73-
if (meterRegistry != null) {
74-
registry = meterRegistry;
75-
}
76-
source.subscribe(new MicrometerMonoMetricsSubscriber<>(actual, registry,
75+
source.subscribe(new MicrometerMonoMetricsSubscriber<>(actual, meterRegistry,
7776
Clock.SYSTEM, this.name, this.tags));
7877
}
7978

reactor-core/src/main/java/reactor/core/publisher/MonoMetricsFuseable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* Activate metrics gathering on a {@link Mono} (Fuseable version), assumes Micrometer is
3333
* on the classpath.
3434
*
35-
* @implNote Metrics.isMicrometerAvailable() test should be performed BEFORE instantiating
35+
* @implNote Metrics.isInstrumentationAvailable() test should be performed BEFORE instantiating
3636
* or referencing this class, otherwise a {@link NoClassDefFoundError} will be thrown if
3737
* Micrometer is not there.
3838
*
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package reactor.util;
18+
19+
import static io.micrometer.core.instrument.Metrics.globalRegistry;
20+
21+
/**
22+
* Utilities around instrumentation and metrics with Micrometer.
23+
*
24+
* @author Simon Baslé
25+
*/
26+
public class Metrics {
27+
28+
static final boolean isMicrometerAvailable;
29+
30+
static {
31+
boolean micrometer;
32+
try {
33+
globalRegistry.getRegistries();
34+
micrometer = true;
35+
}
36+
catch (Throwable t) {
37+
micrometer = false;
38+
}
39+
isMicrometerAvailable = micrometer;
40+
}
41+
42+
/**
43+
* Check if the current runtime supports metrics / instrumentation, by
44+
* verifying if Micrometer is on the classpath.
45+
*
46+
* @return true if the Micrometer instrumentation facade is available
47+
*/
48+
public static final boolean isInstrumentationAvailable() {
49+
return isMicrometerAvailable;
50+
}
51+
52+
}

reactor-core/src/test/java/reactor/core/publisher/MonoSourceTest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,15 @@
1818
import java.util.concurrent.Callable;
1919
import java.util.concurrent.atomic.AtomicBoolean;
2020

21-
import jdk.nashorn.internal.codegen.CompilerConstants;
2221
import org.junit.Test;
2322
import org.reactivestreams.Publisher;
2423
import org.reactivestreams.Subscriber;
24+
2525
import reactor.core.Fuseable;
2626
import reactor.core.Scannable;
2727
import reactor.test.StepVerifier;
28-
import reactor.test.publisher.PublisherProbe;
2928

3029
import static org.assertj.core.api.Assertions.assertThat;
31-
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3230
import static org.junit.Assert.assertTrue;
3331

3432
public class MonoSourceTest {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package reactor.util;
18+
19+
import org.assertj.core.api.Assumptions;
20+
import org.junit.BeforeClass;
21+
import org.junit.Test;
22+
23+
import reactor.core.publisher.Flux;
24+
import reactor.core.publisher.Mono;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.Assertions.assertThatCode;
28+
29+
/**
30+
* This test case should be OK to run in the normal test source set, but is intended
31+
* for a test sourceset that doesn't include Micrometer dependency, and which only runs
32+
* tests validating the likes of {@link Flux#metrics()} are NO-OP in that context.
33+
*
34+
* @author Simon Baslé
35+
*/
36+
public class MetricsNoMicrometerTest {
37+
38+
39+
@BeforeClass
40+
public static void assumeNoMicrometer() {
41+
Assumptions.assumeThat(Metrics.isInstrumentationAvailable())
42+
.as("Micrometer on the classpath").isFalse();
43+
}
44+
45+
@Test
46+
public void isMicrometerAvailable() {
47+
assertThat(Metrics.isInstrumentationAvailable()).isFalse();
48+
}
49+
50+
@Test
51+
public void FluxMetricsNoOp() {
52+
assertThatCode(() -> Flux.just("foo").hide().metrics().blockLast())
53+
.doesNotThrowAnyException();
54+
}
55+
56+
@Test
57+
public void FluxMetricsFusedNoOp() {
58+
assertThatCode(() -> Flux.just("foo").metrics().blockLast())
59+
.doesNotThrowAnyException();
60+
}
61+
62+
@Test
63+
public void MonoMetricsNoOp() {
64+
assertThatCode(() -> Mono.just("foo").hide().metrics().block())
65+
.doesNotThrowAnyException();
66+
}
67+
68+
@Test
69+
public void MonoMetricsFusedNoOp() {
70+
assertThatCode(() -> Mono.just("foo").metrics().block())
71+
.doesNotThrowAnyException();
72+
}
73+
74+
}

0 commit comments

Comments
 (0)