Skip to content

Commit 15fe418

Browse files
committed
Polish 'Support @timed annotation for WebFlux'
Extract common method logic to a `HandlerMethodTimedAnnotations` utility class and also add some caching. Also move the test code into a single class. See gh-23112
1 parent 4ff4a9b commit 15fe418

File tree

6 files changed

+240
-199
lines changed

6 files changed

+240
-199
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
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+
* https://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 org.springframework.boot.actuate.metrics.web.method;
18+
19+
import java.lang.reflect.AnnotatedElement;
20+
import java.util.Collections;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import io.micrometer.core.annotation.Timed;
25+
26+
import org.springframework.core.annotation.MergedAnnotationCollectors;
27+
import org.springframework.core.annotation.MergedAnnotations;
28+
import org.springframework.util.ConcurrentReferenceHashMap;
29+
import org.springframework.web.method.HandlerMethod;
30+
31+
/**
32+
* Utility used to obtain {@link Timed @Timed} annotations from a {@link HandlerMethod}.
33+
*
34+
* @author Phillip Webb
35+
* @since 2.5.0
36+
*/
37+
public final class HandlerMethodTimedAnnotations {
38+
39+
private static Map<AnnotatedElement, Set<Timed>> cache = new ConcurrentReferenceHashMap<>();
40+
41+
private HandlerMethodTimedAnnotations() {
42+
}
43+
44+
public static Set<Timed> get(HandlerMethod handler) {
45+
Set<Timed> methodAnnotations = findTimedAnnotations(handler.getMethod());
46+
if (!methodAnnotations.isEmpty()) {
47+
return methodAnnotations;
48+
}
49+
return findTimedAnnotations(handler.getBeanType());
50+
}
51+
52+
private static Set<Timed> findTimedAnnotations(AnnotatedElement element) {
53+
Set<Timed> result = cache.get(element);
54+
if (result != null) {
55+
return result;
56+
}
57+
MergedAnnotations annotations = MergedAnnotations.from(element);
58+
result = (!annotations.isPresent(Timed.class)) ? Collections.emptySet()
59+
: annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
60+
cache.put(element, result);
61+
return result;
62+
}
63+
64+
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.boot.actuate.metrics.web.reactive.server;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.util.Collections;
2120
import java.util.Set;
2221
import java.util.concurrent.TimeUnit;
@@ -25,14 +24,14 @@
2524
import io.micrometer.core.instrument.MeterRegistry;
2625
import io.micrometer.core.instrument.Tag;
2726
import io.micrometer.core.instrument.Timer;
27+
import io.micrometer.core.instrument.Timer.Builder;
2828
import org.reactivestreams.Publisher;
2929
import reactor.core.publisher.Mono;
3030

3131
import org.springframework.boot.actuate.metrics.AutoTimer;
32+
import org.springframework.boot.actuate.metrics.web.method.HandlerMethodTimedAnnotations;
3233
import org.springframework.boot.web.reactive.error.ErrorAttributes;
3334
import org.springframework.core.Ordered;
34-
import org.springframework.core.annotation.MergedAnnotationCollectors;
35-
import org.springframework.core.annotation.MergedAnnotations;
3635
import org.springframework.core.annotation.Order;
3736
import org.springframework.http.server.reactive.ServerHttpResponse;
3837
import org.springframework.web.method.HandlerMethod;
@@ -100,48 +99,24 @@ private void onTerminalSignal(ServerWebExchange exchange, Throwable cause, long
10099
}
101100

102101
private void record(ServerWebExchange exchange, Throwable cause, long start) {
103-
if (cause == null) {
104-
cause = exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
105-
}
102+
cause = (cause != null) ? cause : exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE);
106103
Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
107-
Set<Timed> annotations = getTimedAnnotations(handler);
104+
Set<Timed> annotations = (handler instanceof HandlerMethod)
105+
? HandlerMethodTimedAnnotations.get((HandlerMethod) handler) : Collections.emptySet();
108106
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(exchange, cause);
109107
long duration = System.nanoTime() - start;
110108
if (annotations.isEmpty()) {
111109
if (this.autoTimer.isEnabled()) {
112-
this.autoTimer.builder(this.metricName).tags(tags).register(this.registry).record(duration,
113-
TimeUnit.NANOSECONDS);
110+
Builder builder = this.autoTimer.builder(this.metricName);
111+
builder.tags(tags).register(this.registry).record(duration, TimeUnit.NANOSECONDS);
114112
}
115113
}
116114
else {
117115
for (Timed annotation : annotations) {
118-
Timer.builder(annotation, this.metricName).tags(tags).register(this.registry).record(duration,
119-
TimeUnit.NANOSECONDS);
116+
Builder builder = Timer.builder(annotation, this.metricName);
117+
builder.tags(tags).register(this.registry).record(duration, TimeUnit.NANOSECONDS);
120118
}
121119
}
122120
}
123121

124-
private Set<Timed> getTimedAnnotations(Object handler) {
125-
if (!(handler instanceof HandlerMethod)) {
126-
return Collections.emptySet();
127-
}
128-
return getTimedAnnotations((HandlerMethod) handler);
129-
}
130-
131-
private Set<Timed> getTimedAnnotations(HandlerMethod handler) {
132-
Set<Timed> methodAnnotations = findTimedAnnotations(handler.getMethod());
133-
if (!methodAnnotations.isEmpty()) {
134-
return methodAnnotations;
135-
}
136-
return findTimedAnnotations(handler.getBeanType());
137-
}
138-
139-
private Set<Timed> findTimedAnnotations(AnnotatedElement element) {
140-
MergedAnnotations annotations = MergedAnnotations.from(element);
141-
if (!annotations.isPresent(Timed.class)) {
142-
return Collections.emptySet();
143-
}
144-
return annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
145-
}
146-
147122
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.boot.actuate.metrics.web.servlet;
1818

1919
import java.io.IOException;
20-
import java.lang.reflect.AnnotatedElement;
2120
import java.util.Collections;
2221
import java.util.Set;
2322

@@ -33,9 +32,8 @@
3332
import io.micrometer.core.instrument.Timer.Sample;
3433

3534
import org.springframework.boot.actuate.metrics.AutoTimer;
35+
import org.springframework.boot.actuate.metrics.web.method.HandlerMethodTimedAnnotations;
3636
import org.springframework.boot.web.servlet.error.ErrorAttributes;
37-
import org.springframework.core.annotation.MergedAnnotationCollectors;
38-
import org.springframework.core.annotation.MergedAnnotations;
3937
import org.springframework.http.HttpStatus;
4038
import org.springframework.web.filter.OncePerRequestFilter;
4139
import org.springframework.web.method.HandlerMethod;
@@ -130,7 +128,8 @@ private Throwable fetchException(HttpServletRequest request) {
130128
private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response,
131129
Throwable exception) {
132130
Object handler = getHandler(request);
133-
Set<Timed> annotations = getTimedAnnotations(handler);
131+
Set<Timed> annotations = (handler instanceof HandlerMethod)
132+
? HandlerMethodTimedAnnotations.get((HandlerMethod) handler) : Collections.emptySet();
134133
Timer.Sample timerSample = timingContext.getTimerSample();
135134
if (annotations.isEmpty()) {
136135
if (this.autoTimer.isEnabled()) {
@@ -150,29 +149,6 @@ private Object getHandler(HttpServletRequest request) {
150149
return request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
151150
}
152151

153-
private Set<Timed> getTimedAnnotations(Object handler) {
154-
if (!(handler instanceof HandlerMethod)) {
155-
return Collections.emptySet();
156-
}
157-
return getTimedAnnotations((HandlerMethod) handler);
158-
}
159-
160-
private Set<Timed> getTimedAnnotations(HandlerMethod handler) {
161-
Set<Timed> methodAnnotations = findTimedAnnotations(handler.getMethod());
162-
if (!methodAnnotations.isEmpty()) {
163-
return methodAnnotations;
164-
}
165-
return findTimedAnnotations(handler.getBeanType());
166-
}
167-
168-
private Set<Timed> findTimedAnnotations(AnnotatedElement element) {
169-
MergedAnnotations annotations = MergedAnnotations.from(element);
170-
if (!annotations.isPresent(Timed.class)) {
171-
return Collections.emptySet();
172-
}
173-
return annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet());
174-
}
175-
176152
private Timer getTimer(Builder builder, Object handler, HttpServletRequest request, HttpServletResponse response,
177153
Throwable exception) {
178154
return builder.tags(this.tagsProvider.getTags(request, response, handler, exception)).register(this.registry);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
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+
* https://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 org.springframework.boot.actuate.metrics.web.method;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.Set;
21+
22+
import io.micrometer.core.annotation.Timed;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.util.ReflectionUtils;
26+
import org.springframework.web.method.HandlerMethod;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Tests for {@link HandlerMethodTimedAnnotations}.
32+
*
33+
* @author Phillip Webb
34+
*/
35+
class HandlerMethodTimedAnnotationsTests {
36+
37+
@Test
38+
void getWhenNoneReturnsEmptySet() {
39+
Object bean = new None();
40+
Method method = ReflectionUtils.findMethod(bean.getClass(), "handle");
41+
Set<Timed> annotations = HandlerMethodTimedAnnotations.get(new HandlerMethod(bean, method));
42+
assertThat(annotations).isEmpty();
43+
}
44+
45+
@Test
46+
void getWhenOnMethodReturnsMethodAnnotations() {
47+
Object bean = new OnMethod();
48+
Method method = ReflectionUtils.findMethod(bean.getClass(), "handle");
49+
Set<Timed> annotations = HandlerMethodTimedAnnotations.get(new HandlerMethod(bean, method));
50+
assertThat(annotations).extracting(Timed::value).containsOnly("y", "z");
51+
}
52+
53+
@Test
54+
void getWhenNonOnMethodReturnsBeanAnnotations() {
55+
Object bean = new OnBean();
56+
Method method = ReflectionUtils.findMethod(bean.getClass(), "handle");
57+
Set<Timed> annotations = HandlerMethodTimedAnnotations.get(new HandlerMethod(bean, method));
58+
assertThat(annotations).extracting(Timed::value).containsOnly("y", "z");
59+
}
60+
61+
static class None {
62+
63+
void handle() {
64+
}
65+
66+
}
67+
68+
@Timed("x")
69+
static class OnMethod {
70+
71+
@Timed("y")
72+
@Timed("z")
73+
void handle() {
74+
}
75+
76+
}
77+
78+
@Timed("y")
79+
@Timed("z")
80+
static class OnBean {
81+
82+
void handle() {
83+
}
84+
85+
}
86+
87+
}

0 commit comments

Comments
 (0)