From 7b1308bd3f0ef5a5f5cf9eb9d356938de99455f2 Mon Sep 17 00:00:00 2001 From: aheil Date: Mon, 25 Jan 2016 17:11:08 +0000 Subject: [PATCH] Support for meta-annotations #163 --- .../AbstractMetricMethodInterceptor.java | 3 +- .../AnnotationClassOrMethodPointcut.java | 100 ++++++++++++ .../metrics/spring/AnnotationFilter.java | 3 +- .../spring/CountedMethodInterceptor.java | 11 +- .../ExceptionMeteredMethodInterceptor.java | 11 +- .../spring/MeteredMethodInterceptor.java | 11 +- .../spring/TimedMethodInterceptor.java | 13 +- .../metrics/spring/MetaAnnotationTest.java | 148 ++++++++++++++++++ src/test/resources/meta-annotation.xml | 33 ++++ 9 files changed, 302 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/ryantenney/metrics/spring/AnnotationClassOrMethodPointcut.java create mode 100644 src/test/java/com/ryantenney/metrics/spring/MetaAnnotationTest.java create mode 100755 src/test/resources/meta-annotation.xml diff --git a/src/main/java/com/ryantenney/metrics/spring/AbstractMetricMethodInterceptor.java b/src/main/java/com/ryantenney/metrics/spring/AbstractMetricMethodInterceptor.java index 3ea83162..9a6d78b7 100644 --- a/src/main/java/com/ryantenney/metrics/spring/AbstractMetricMethodInterceptor.java +++ b/src/main/java/com/ryantenney/metrics/spring/AbstractMetricMethodInterceptor.java @@ -24,6 +24,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodCallback; import org.springframework.util.ReflectionUtils.MethodFilter; @@ -65,7 +66,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { @Override public void doWith(Method method) throws IllegalAccessException { - final A annotation = method.getAnnotation(annotationClass); + final A annotation = AnnotationUtils.findAnnotation(method, annotationClass); if (annotation != null) { final MethodKey methodKey = MethodKey.forMethod(method); final String metricName = buildMetricName(targetClass, method, annotation); diff --git a/src/main/java/com/ryantenney/metrics/spring/AnnotationClassOrMethodPointcut.java b/src/main/java/com/ryantenney/metrics/spring/AnnotationClassOrMethodPointcut.java new file mode 100644 index 00000000..75370dd4 --- /dev/null +++ b/src/main/java/com/ryantenney/metrics/spring/AnnotationClassOrMethodPointcut.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2012 Ryan W Tenney (ryan@10e.us) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ryantenney.metrics.spring; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicBoolean; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.aop.support.annotation.AnnotationClassFilter; +import org.springframework.aop.support.annotation.AnnotationMethodMatcher; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +/** + Based on spring retry (org.springframework.retry.annotation.RetryConfiguration). + */ +public class AnnotationClassOrMethodPointcut extends StaticMethodMatcherPointcut { + + private final MethodMatcher methodResolver; + + AnnotationClassOrMethodPointcut(Class annotationType) { + this.methodResolver = new AnnotationMethodMatcher(annotationType); + setClassFilter(new AnnotationClassOrMethodFilter(annotationType)); + } + + @Override + public boolean matches(Method method, Class targetClass) { + return getClassFilter().matches(targetClass) || this.methodResolver.matches(method, targetClass); + } + + private static final class AnnotationClassOrMethodFilter extends AnnotationClassFilter { + + private final AnnotationMethodsResolver methodResolver; + + AnnotationClassOrMethodFilter(Class annotationType) { + super(annotationType, true); + this.methodResolver = new AnnotationMethodsResolver(annotationType); + } + + @Override + public boolean matches(Class clazz) { + return super.matches(clazz) || this.methodResolver.hasAnnotatedMethods(clazz); + } + + @Override + public boolean equals(Object other) { + + return super.equals(other); + } + + @Override + public int hashCode() { + + return super.hashCode(); + } + } + + private static class AnnotationMethodsResolver { + + private Class annotationType; + + public AnnotationMethodsResolver(Class annotationType) { + this.annotationType = annotationType; + } + + public boolean hasAnnotatedMethods(Class clazz) { + final AtomicBoolean found = new AtomicBoolean(false); + ReflectionUtils.doWithMethods(clazz, + new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException, + IllegalAccessException { + if (found.get()) { + return; + } + Annotation annotation = AnnotationUtils.findAnnotation(method, + annotationType); + if (annotation != null) { found.set(true); } + } + }); + return found.get(); + } + + } + +} diff --git a/src/main/java/com/ryantenney/metrics/spring/AnnotationFilter.java b/src/main/java/com/ryantenney/metrics/spring/AnnotationFilter.java index 7e25fe57..467f29a2 100644 --- a/src/main/java/com/ryantenney/metrics/spring/AnnotationFilter.java +++ b/src/main/java/com/ryantenney/metrics/spring/AnnotationFilter.java @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ReflectionUtils.FieldFilter; import org.springframework.util.ReflectionUtils.MethodFilter; @@ -69,7 +70,7 @@ public AnnotationFilter(final Class clazz, final int metho @Override public boolean matches(Method method) { - if (USER_DECLARED_METHODS.matches(method) && method.isAnnotationPresent(clazz)) { + if (USER_DECLARED_METHODS.matches(method) && AnnotationUtils.findAnnotation(method, clazz) != null) { if (checkModifiers(method, methodModifiers)) { return true; } diff --git a/src/main/java/com/ryantenney/metrics/spring/CountedMethodInterceptor.java b/src/main/java/com/ryantenney/metrics/spring/CountedMethodInterceptor.java index 41946636..634c6c76 100644 --- a/src/main/java/com/ryantenney/metrics/spring/CountedMethodInterceptor.java +++ b/src/main/java/com/ryantenney/metrics/spring/CountedMethodInterceptor.java @@ -15,24 +15,21 @@ */ package com.ryantenney.metrics.spring; +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import com.ryantenney.metrics.annotation.Counted; import java.lang.reflect.Method; - import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; -import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; import org.springframework.util.ReflectionUtils.MethodFilter; -import com.codahale.metrics.Counter; -import com.codahale.metrics.MetricRegistry; -import com.ryantenney.metrics.annotation.Counted; - import static com.ryantenney.metrics.spring.AnnotationFilter.PROXYABLE_METHODS; class CountedMethodInterceptor extends AbstractMetricMethodInterceptor { public static final Class ANNOTATION = Counted.class; - public static final Pointcut POINTCUT = new AnnotationMatchingPointcut(null, ANNOTATION); + public static final Pointcut POINTCUT = new AnnotationClassOrMethodPointcut(ANNOTATION); public static final MethodFilter METHOD_FILTER = new AnnotationFilter(ANNOTATION, PROXYABLE_METHODS); public CountedMethodInterceptor(final MetricRegistry metricRegistry, final Class targetClass) { diff --git a/src/main/java/com/ryantenney/metrics/spring/ExceptionMeteredMethodInterceptor.java b/src/main/java/com/ryantenney/metrics/spring/ExceptionMeteredMethodInterceptor.java index 2ab88e63..80bfec9e 100644 --- a/src/main/java/com/ryantenney/metrics/spring/ExceptionMeteredMethodInterceptor.java +++ b/src/main/java/com/ryantenney/metrics/spring/ExceptionMeteredMethodInterceptor.java @@ -15,25 +15,22 @@ */ package com.ryantenney.metrics.spring; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.annotation.ExceptionMetered; import java.lang.reflect.Method; - import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; -import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; import org.springframework.core.Ordered; import org.springframework.util.ReflectionUtils.MethodFilter; -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.annotation.ExceptionMetered; - import static com.ryantenney.metrics.spring.AnnotationFilter.PROXYABLE_METHODS; class ExceptionMeteredMethodInterceptor extends AbstractMetricMethodInterceptor implements Ordered { public static final Class ANNOTATION = ExceptionMetered.class; - public static final Pointcut POINTCUT = new AnnotationMatchingPointcut(null, ANNOTATION); + public static final Pointcut POINTCUT = new AnnotationClassOrMethodPointcut(ANNOTATION); public static final MethodFilter METHOD_FILTER = new AnnotationFilter(ANNOTATION, PROXYABLE_METHODS); public ExceptionMeteredMethodInterceptor(final MetricRegistry metricRegistry, final Class targetClass) { diff --git a/src/main/java/com/ryantenney/metrics/spring/MeteredMethodInterceptor.java b/src/main/java/com/ryantenney/metrics/spring/MeteredMethodInterceptor.java index 415648ed..21abf5e3 100644 --- a/src/main/java/com/ryantenney/metrics/spring/MeteredMethodInterceptor.java +++ b/src/main/java/com/ryantenney/metrics/spring/MeteredMethodInterceptor.java @@ -15,24 +15,21 @@ */ package com.ryantenney.metrics.spring; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.annotation.Metered; import java.lang.reflect.Method; - import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; -import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; import org.springframework.util.ReflectionUtils.MethodFilter; -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.annotation.Metered; - import static com.ryantenney.metrics.spring.AnnotationFilter.PROXYABLE_METHODS; class MeteredMethodInterceptor extends AbstractMetricMethodInterceptor { public static final Class ANNOTATION = Metered.class; - public static final Pointcut POINTCUT = new AnnotationMatchingPointcut(null, ANNOTATION); + public static final Pointcut POINTCUT = new AnnotationClassOrMethodPointcut(ANNOTATION); public static final MethodFilter METHOD_FILTER = new AnnotationFilter(ANNOTATION, PROXYABLE_METHODS); public MeteredMethodInterceptor(final MetricRegistry metricRegistry, final Class targetClass) { diff --git a/src/main/java/com/ryantenney/metrics/spring/TimedMethodInterceptor.java b/src/main/java/com/ryantenney/metrics/spring/TimedMethodInterceptor.java index c635acbe..8127917f 100644 --- a/src/main/java/com/ryantenney/metrics/spring/TimedMethodInterceptor.java +++ b/src/main/java/com/ryantenney/metrics/spring/TimedMethodInterceptor.java @@ -15,26 +15,23 @@ */ package com.ryantenney.metrics.spring; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.codahale.metrics.Timer.Context; +import com.codahale.metrics.annotation.Timed; import java.lang.reflect.Method; - import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.Pointcut; -import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; import org.springframework.core.Ordered; import org.springframework.util.ReflectionUtils.MethodFilter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; -import com.codahale.metrics.Timer.Context; -import com.codahale.metrics.annotation.Timed; - import static com.ryantenney.metrics.spring.AnnotationFilter.PROXYABLE_METHODS; class TimedMethodInterceptor extends AbstractMetricMethodInterceptor implements Ordered { public static final Class ANNOTATION = Timed.class; - public static final Pointcut POINTCUT = new AnnotationMatchingPointcut(null, ANNOTATION); + public static final Pointcut POINTCUT = new AnnotationClassOrMethodPointcut(ANNOTATION); public static final MethodFilter METHOD_FILTER = new AnnotationFilter(ANNOTATION, PROXYABLE_METHODS); public TimedMethodInterceptor(final MetricRegistry metricRegistry, final Class targetClass) { diff --git a/src/test/java/com/ryantenney/metrics/spring/MetaAnnotationTest.java b/src/test/java/com/ryantenney/metrics/spring/MetaAnnotationTest.java new file mode 100644 index 00000000..64155915 --- /dev/null +++ b/src/test/java/com/ryantenney/metrics/spring/MetaAnnotationTest.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2012 Ryan W Tenney (ryan@10e.us) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ryantenney.metrics.spring; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.codahale.metrics.annotation.ExceptionMetered; +import com.codahale.metrics.annotation.Metered; +import com.codahale.metrics.annotation.Timed; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.SortedSet; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:meta-annotation.xml") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class MetaAnnotationTest { + + private static final String ERROR_MESSAGE = "error message"; + + private static final int EXPECTED_NUMBERS_OF_KEYS = 3; + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Metered + @ExceptionMetered(cause = RuntimeException.class) + public @interface MetaAnnotationMeteredAndExceptionMetered { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Timed + public @interface MetaAnnotationTimed { + + } + + public static class MetaAnnotatedClass { + + public MetaAnnotatedClass() { + } + + @MetaAnnotationMeteredAndExceptionMetered + public void doItMetered() { + throw new RuntimeException(ERROR_MESSAGE); + } + + @MetaAnnotationTimed + public void doItTimed() { + + } + + } + + @Autowired + MetricRegistry metricRegistry; + + @Autowired + MetaAnnotatedClass metaAnnotatedClass; + + @Test + public void assureThatMetaAnnotationsWorkForMeteredAndExceptionMetered() { + + try { + metaAnnotatedClass.doItMetered(); + Assert.fail("exception expected"); + } catch (RuntimeException e) { + Assert.assertEquals(ERROR_MESSAGE, e.getMessage()); + Assert.assertEquals(EXPECTED_NUMBERS_OF_KEYS, metricRegistry.getNames().size()); + assertThatDoItMeteredIsCalled(1); + assertThatDoItMeteredExceptionIsCalled(1); + assertThatDoItTimedIsCalled(0); + } + } + + @Test + public void assureThatMetaAnnotationsWorkForTimed() { + + metaAnnotatedClass.doItTimed(); + Assert.assertEquals(EXPECTED_NUMBERS_OF_KEYS, metricRegistry.getNames().size()); + assertThatDoItMeteredIsCalled(0); + assertThatDoItMeteredExceptionIsCalled(0); + assertThatDoItTimedIsCalled(1); + } + + private void assertThatDoItTimedIsCalled(int times) { + + String key = assertThatMetricRegistryContains("doItTimed"); + assertThatTimerIsCalled(key, times); + } + + private void assertThatDoItMeteredExceptionIsCalled(int times) { + + String key = assertThatMetricRegistryContains("doItMetered.exceptions"); + assertThatMeterIsCalled(key, times); + } + + private void assertThatDoItMeteredIsCalled(int times) { + + String key = assertThatMetricRegistryContains("doItMetered"); + assertThatMeterIsCalled(key, times); + } + + private void assertThatTimerIsCalled(String key, int times) { + + Timer meter = metricRegistry.getTimers().get(key); + Assert.assertEquals(times, meter.getCount()); + } + + private void assertThatMeterIsCalled(String key, int times) { + + Meter meter = metricRegistry.getMeters().get(key); + Assert.assertEquals(times, meter.getCount()); + } + + private String assertThatMetricRegistryContains(String suffix) { + + String prefix = MetaAnnotatedClass.class.getCanonicalName(); + SortedSet names = metricRegistry.getNames(); + String key = prefix + "." + suffix; + Assert.assertTrue(names.contains(key)); + return key; + } + +} diff --git a/src/test/resources/meta-annotation.xml b/src/test/resources/meta-annotation.xml new file mode 100755 index 00000000..5c6cbe83 --- /dev/null +++ b/src/test/resources/meta-annotation.xml @@ -0,0 +1,33 @@ + + + + + + + + +