Skip to content

Commit 12c1f08

Browse files
authored
Fix completion stage returns null in aspects (#5742)
Handling the case was missing when `null` was returned from an annotated method with return type CompletionStage, resulting in a NullPointerException. Fixes #5741
1 parent a15f69c commit 12c1f08

File tree

6 files changed

+161
-8
lines changed

6 files changed

+161
-8
lines changed

micrometer-core/src/main/java/io/micrometer/core/aop/CountedAspect.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
*
7070
* @author Ali Dehghani
7171
* @author Jonatan Ivanov
72+
* @author Jeonggi Kim
7273
* @since 1.2.0
7374
* @see Counted
7475
*/
@@ -212,8 +213,17 @@ public Object interceptAndRecord(ProceedingJoinPoint pjp, Counted counted) throw
212213

213214
if (stopWhenCompleted) {
214215
try {
215-
return ((CompletionStage<?>) pjp.proceed())
216-
.whenComplete((result, throwable) -> recordCompletionResult(pjp, counted, throwable));
216+
Object result = pjp.proceed();
217+
if (result == null) {
218+
if (!counted.recordFailuresOnly()) {
219+
record(pjp, counted, DEFAULT_EXCEPTION_TAG_VALUE, RESULT_TAG_SUCCESS_VALUE);
220+
}
221+
return result;
222+
}
223+
else {
224+
CompletionStage<?> stage = ((CompletionStage<?>) result);
225+
return stage.whenComplete((res, throwable) -> recordCompletionResult(pjp, counted, throwable));
226+
}
217227
}
218228
catch (Throwable e) {
219229
record(pjp, counted, e.getClass().getSimpleName(), RESULT_TAG_FAILURE_VALUE);

micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
* @author Johnny Lim
7878
* @author Nejc Korasa
7979
* @author Jonatan Ivanov
80+
* @author Jeonggi Kim
8081
* @since 1.0.0
8182
*/
8283
@Aspect
@@ -230,8 +231,16 @@ private Object processWithTimer(ProceedingJoinPoint pjp, Timed timed, String met
230231

231232
if (stopWhenCompleted) {
232233
try {
233-
return ((CompletionStage<?>) pjp.proceed()).whenComplete(
234-
(result, throwable) -> record(pjp, timed, metricName, sample, getExceptionTag(throwable)));
234+
Object result = pjp.proceed();
235+
if (result == null) {
236+
record(pjp, timed, metricName, sample, DEFAULT_EXCEPTION_TAG_VALUE);
237+
return result;
238+
}
239+
else {
240+
CompletionStage<?> stage = ((CompletionStage<?>) result);
241+
return stage.whenComplete(
242+
(res, throwable) -> record(pjp, timed, metricName, sample, getExceptionTag(throwable)));
243+
}
235244
}
236245
catch (Throwable e) {
237246
record(pjp, timed, metricName, sample, e.getClass().getSimpleName());
@@ -297,8 +306,15 @@ private Object processWithLongTaskTimer(ProceedingJoinPoint pjp, Timed timed, St
297306

298307
if (stopWhenCompleted) {
299308
try {
300-
return ((CompletionStage<?>) pjp.proceed())
301-
.whenComplete((result, throwable) -> sample.ifPresent(this::stopTimer));
309+
Object result = pjp.proceed();
310+
if (result == null) {
311+
sample.ifPresent(this::stopTimer);
312+
return result;
313+
}
314+
else {
315+
CompletionStage<?> stage = ((CompletionStage<?>) result);
316+
return stage.whenComplete((res, throwable) -> sample.ifPresent(this::stopTimer));
317+
}
302318
}
303319
catch (Throwable e) {
304320
sample.ifPresent(this::stopTimer);

micrometer-core/src/test/java/io/micrometer/core/aop/CountedAspectTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,29 @@ void pjpFunctionThrows() {
211211
assertThat(counter.getId().getDescription()).isNull();
212212
}
213213

214+
@Test
215+
void countedWithSuccessfulMetricsWhenReturnsCompletionStageNull() {
216+
CompletableFuture<?> completableFuture = asyncCountedService.successButNull();
217+
assertThat(completableFuture).isNull();
218+
219+
assertThat(meterRegistry.get("metric.success")
220+
.tag("method", "successButNull")
221+
.tag("class", getClass().getName() + "$AsyncCountedService")
222+
.tag("extra", "tag")
223+
.tag("exception", "none")
224+
.tag("result", "success")
225+
.counter()
226+
.count()).isOne();
227+
}
228+
229+
@Test
230+
void countedWithoutSuccessfulMetricsWhenReturnsCompletionStageNull() {
231+
CompletableFuture<?> completableFuture = asyncCountedService.successButNullWithoutMetrics();
232+
assertThat(completableFuture).isNull();
233+
234+
assertThatThrownBy(() -> meterRegistry.get("metric.none").counter()).isInstanceOf(MeterNotFoundException.class);
235+
}
236+
214237
static class CountedService {
215238

216239
@Counted(value = "metric.none", recordFailuresOnly = true)
@@ -272,6 +295,16 @@ CompletableFuture<?> emptyMetricName(GuardedResult guardedResult) {
272295
return supplyAsync(guardedResult::get);
273296
}
274297

298+
@Counted(value = "metric.success", extraTags = { "extra", "tag" })
299+
CompletableFuture<?> successButNull() {
300+
return null;
301+
}
302+
303+
@Counted(value = "metric.none", recordFailuresOnly = true)
304+
CompletableFuture<?> successButNullWithoutMetrics() {
305+
return null;
306+
}
307+
275308
}
276309

277310
static class GuardedResult {

micrometer-core/src/test/java/io/micrometer/core/aop/TimedAspectTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,29 @@ void timeMethodWhenCompletedExceptionally() {
217217
.count()).isEqualTo(1);
218218
}
219219

220+
@Test
221+
void timeMethodWhenReturnCompletionStageNull() {
222+
MeterRegistry registry = new SimpleMeterRegistry();
223+
224+
AspectJProxyFactory pf = new AspectJProxyFactory(new AsyncTimedService());
225+
pf.addAspect(new TimedAspect(registry));
226+
227+
AsyncTimedService service = pf.getProxy();
228+
229+
CompletableFuture<?> completableFuture = service.callNull();
230+
assertThat(completableFuture).isNull();
231+
232+
assertThat(registry.getMeters()).isNotEmpty();
233+
234+
assertThat(registry.get("callNull")
235+
.tag("class", getClass().getName() + "$AsyncTimedService")
236+
.tag("method", "callNull")
237+
.tag("extra", "tag")
238+
.tag("exception", "none")
239+
.timer()
240+
.count()).isEqualTo(1);
241+
}
242+
220243
@Test
221244
void timeMethodWithLongTaskTimerWhenCompleted() {
222245
MeterRegistry registry = new SimpleMeterRegistry();
@@ -277,6 +300,32 @@ void timeMethodWithLongTaskTimerWhenCompletedExceptionally() {
277300
.activeTasks()).isEqualTo(0);
278301
}
279302

303+
@Test
304+
void timeMethodWithLongTaskTimerWhenReturnCompletionStageNull() {
305+
MeterRegistry registry = new SimpleMeterRegistry();
306+
307+
AspectJProxyFactory pf = new AspectJProxyFactory(new AsyncTimedService());
308+
pf.addAspect(new TimedAspect(registry));
309+
310+
AsyncTimedService service = pf.getProxy();
311+
312+
CompletableFuture<?> completableFuture = service.longCallNull();
313+
assertThat(completableFuture).isNull();
314+
315+
assertThat(registry.get("longCallNull")
316+
.tag("class", getClass().getName() + "$AsyncTimedService")
317+
.tag("method", "longCallNull")
318+
.tag("extra", "tag")
319+
.longTaskTimers()).hasSize(1);
320+
321+
assertThat(registry.find("longCallNull")
322+
.tag("class", getClass().getName() + "$AsyncTimedService")
323+
.tag("method", "longCallNull")
324+
.tag("extra", "tag")
325+
.longTaskTimer()
326+
.activeTasks()).isEqualTo(0);
327+
}
328+
280329
@Test
281330
void timeMethodFailureWhenCompletedExceptionally() {
282331
MeterRegistry failingRegistry = new FailingMeterRegistry();
@@ -641,11 +690,21 @@ CompletableFuture<?> call(GuardedResult guardedResult) {
641690
return supplyAsync(guardedResult::get);
642691
}
643692

693+
@Timed(value = "callNull", extraTags = { "extra", "tag" })
694+
CompletableFuture<?> callNull() {
695+
return null;
696+
}
697+
644698
@Timed(value = "longCall", extraTags = { "extra", "tag" }, longTask = true)
645699
CompletableFuture<?> longCall(GuardedResult guardedResult) {
646700
return supplyAsync(guardedResult::get);
647701
}
648702

703+
@Timed(value = "longCallNull", extraTags = { "extra", "tag" }, longTask = true)
704+
CompletableFuture<?> longCallNull() {
705+
return null;
706+
}
707+
649708
}
650709

651710
static class GuardedResult {

micrometer-observation/src/main/java/io/micrometer/observation/aop/ObservedAspect.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
* </pre>
6969
*
7070
* @author Jonatan Ivanov
71+
* @author Jeonggi Kim
7172
* @since 1.10.0
7273
*/
7374
@Aspect
@@ -135,8 +136,15 @@ private Object observe(ProceedingJoinPoint pjp, Method method, Observed observed
135136
observation.start();
136137
Observation.Scope scope = observation.openScope();
137138
try {
138-
return ((CompletionStage<?>) pjp.proceed())
139-
.whenComplete((result, error) -> stopObservation(observation, scope, error));
139+
Object result = pjp.proceed();
140+
if (result == null) {
141+
stopObservation(observation, scope, null);
142+
return result;
143+
}
144+
else {
145+
CompletionStage<?> stage = (CompletionStage<?>) result;
146+
return stage.whenComplete((res, error) -> stopObservation(observation, scope, error));
147+
}
140148
}
141149
catch (Throwable error) {
142150
stopObservation(observation, scope, error);

micrometer-observation/src/test/java/io/micrometer/observation/aop/ObservedAspectTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,29 @@ void skipPredicateShouldTakeEffectForClass() {
360360
TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation();
361361
}
362362

363+
@Test
364+
void annotatedAsyncClassCallWithNullShouldBeObserved() {
365+
registry.observationConfig().observationHandler(new ObservationTextPublisher());
366+
367+
AspectJProxyFactory pf = new AspectJProxyFactory(new ObservedClassLevelAnnotatedService());
368+
pf.addAspect(new ObservedAspect(registry));
369+
370+
ObservedClassLevelAnnotatedService service = pf.getProxy();
371+
CompletableFuture<String> asyncResult = service.asyncNull();
372+
assertThat(asyncResult).isNull();
373+
374+
TestObservationRegistryAssert.assertThat(registry)
375+
.doesNotHaveAnyRemainingCurrentObservation()
376+
.hasSingleObservationThat()
377+
.hasNameEqualTo("test.class")
378+
.hasContextualNameEqualTo("test.class#call")
379+
.hasLowCardinalityKeyValue("abc", "123")
380+
.hasLowCardinalityKeyValue("test", "42")
381+
.hasLowCardinalityKeyValue("class", ObservedClassLevelAnnotatedService.class.getName())
382+
.hasLowCardinalityKeyValue("method", "asyncNull")
383+
.doesNotHaveError();
384+
}
385+
363386
static class ObservedService {
364387

365388
@Observed(name = "test.call", contextualName = "test#call",
@@ -444,6 +467,10 @@ CompletableFuture<String> async(FakeAsyncTask fakeAsyncTask) {
444467
contextSnapshot.wrapExecutor(Executors.newSingleThreadExecutor()));
445468
}
446469

470+
CompletableFuture<String> asyncNull() {
471+
return null;
472+
}
473+
447474
}
448475

449476
static class FakeAsyncTask implements Supplier<String> {

0 commit comments

Comments
 (0)