diff --git a/src/main/java/com/ryantenney/metrics/annotation/InjectMetric.java b/src/main/java/com/ryantenney/metrics/annotation/InjectMetric.java index 995b0466..fe8eee9b 100755 --- a/src/main/java/com/ryantenney/metrics/annotation/InjectMetric.java +++ b/src/main/java/com/ryantenney/metrics/annotation/InjectMetric.java @@ -21,6 +21,8 @@ import java.lang.annotation.Target; /** + * Deprecated, use @Metric instead. + * * An annotation requesting that a metric be injected *

* Given a field like this: @@ -32,6 +34,7 @@ * A meter for the defining class with the name {@code someTimer} will be created. It will be up to the user * to mark the meter. This annotation can be used on fields of type Meter, Timer, Counter, and Histogram. */ +@Deprecated @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface InjectMetric { diff --git a/src/main/java/com/ryantenney/metrics/annotation/Metric.java b/src/main/java/com/ryantenney/metrics/annotation/Metric.java new file mode 100755 index 00000000..4b90afaf --- /dev/null +++ b/src/main/java/com/ryantenney/metrics/annotation/Metric.java @@ -0,0 +1,50 @@ +/** + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation requesting that a metric be injected + *

+ * Given a field like this: + *


+ *     \@Metric
+ *     public Meter someTimer;
+ * 
+ *

+ * A meter for the defining class with the name {@code someTimer} will be created. It will be up to the user + * to mark the meter. This annotation can be used on fields of type Meter, Timer, Counter, and Histogram. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Metric { + + /** + * The name of the metric. + */ + String name() default ""; + + /** + * If {@code true}, use the given name an as absolute name. If {@code false}, use the given name + * relative to the annotated class. + */ + boolean absolute() default false; + +} diff --git a/src/main/java/com/ryantenney/metrics/spring/InjectMetricAnnotationBeanPostProcessor.java b/src/main/java/com/ryantenney/metrics/spring/InjectMetricAnnotationBeanPostProcessor.java index 5ceb55a9..a5d0fb28 100755 --- a/src/main/java/com/ryantenney/metrics/spring/InjectMetricAnnotationBeanPostProcessor.java +++ b/src/main/java/com/ryantenney/metrics/spring/InjectMetricAnnotationBeanPostProcessor.java @@ -33,6 +33,7 @@ import com.codahale.metrics.Timer; import com.ryantenney.metrics.annotation.InjectMetric; +@SuppressWarnings("deprecation") class InjectMetricAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered { private static final Logger LOG = LoggerFactory.getLogger(InjectMetricAnnotationBeanPostProcessor.class); diff --git a/src/main/java/com/ryantenney/metrics/spring/MetricAnnotationBeanPostProcessor.java b/src/main/java/com/ryantenney/metrics/spring/MetricAnnotationBeanPostProcessor.java new file mode 100755 index 00000000..f8477ad9 --- /dev/null +++ b/src/main/java/com/ryantenney/metrics/spring/MetricAnnotationBeanPostProcessor.java @@ -0,0 +1,132 @@ +/** + * 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.reflect.Field; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.ryantenney.metrics.annotation.Metric; + +class MetricAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered { + + private static final Logger LOG = LoggerFactory.getLogger(MetricAnnotationBeanPostProcessor.class); + + private static final AnnotationFilter FILTER = new AnnotationFilter(Metric.class); + + private final MetricRegistry metrics; + + public MetricAnnotationBeanPostProcessor(final MetricRegistry metrics) { + this.metrics = metrics; + } + + @Override + public Object postProcessBeforeInitialization(final Object bean, String beanName) { + final Class targetClass = AopUtils.getTargetClass(bean); + + ReflectionUtils.doWithFields(targetClass, new FieldCallback() { + @Override + public void doWith(Field field) throws IllegalAccessException { + final Metric annotation = field.getAnnotation(Metric.class); + final String metricName = Util.forMetricField(targetClass, field, annotation); + + final Class type = field.getType(); + if (!com.codahale.metrics.Metric.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Field " + targetClass.getCanonicalName() + "." + + field.getName() + " must be a subtype of " + com.codahale.metrics.Metric.class.getCanonicalName()); + } + + ReflectionUtils.makeAccessible(field); + + // Get the value of the field annotated with @Metric + com.codahale.metrics.Metric metric = (com.codahale.metrics.Metric) ReflectionUtils.getField(field, bean); + + if (metric == null) { + // If null, create a metric of the appropriate type and inject it + metric = getMetric(metrics, type, metricName); + ReflectionUtils.setField(field, bean, metric); + LOG.debug("Injected metric {} for field {}.{}", metricName, targetClass.getCanonicalName(), field.getName()); + } + else { + // If non-null, register that instance of the metric + try { + // Attempt to register that instance of the metric + metrics.register(metricName, metric); + LOG.debug("Registered metric {} for field {}.{}", metricName, targetClass.getCanonicalName(), field.getName()); + } + catch (IllegalArgumentException ex1) { + // A metric is already registered under that name + // (Cannot determine the cause without parsing the Exception's message) + try { + metric = getMetric(metrics, type, metricName); + ReflectionUtils.setField(field, bean, metric); + LOG.debug("Injected metric {} for field {}.{}", metricName, targetClass.getCanonicalName(), field.getName()); + } + catch (IllegalArgumentException ex2) { + // A metric of a different type is already registered under that name + throw new IllegalArgumentException("Error injecting metric for field " + + targetClass.getCanonicalName() + "." + field.getName(), ex2); + } + } + } + } + }, FILTER); + + return bean; + } + + private com.codahale.metrics.Metric getMetric(MetricRegistry metricRegistry, Class type, String metricName) { + com.codahale.metrics.Metric metric; + if (Meter.class == type) { + metric = metrics.meter(metricName); + } + else if (Timer.class == type) { + metric = metrics.timer(metricName); + } + else if (Counter.class == type) { + metric = metrics.counter(metricName); + } + else if (Histogram.class == type) { + metric = metrics.histogram(metricName); + } + else { + throw new IllegalArgumentException("Invalid @Metric type " + type.getCanonicalName()); + } + return metric; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public int getOrder() { + return LOWEST_PRECEDENCE - 2; + } + +} diff --git a/src/main/java/com/ryantenney/metrics/spring/MetricsBeanPostProcessorFactory.java b/src/main/java/com/ryantenney/metrics/spring/MetricsBeanPostProcessorFactory.java index 8a61064d..55bb13f0 100644 --- a/src/main/java/com/ryantenney/metrics/spring/MetricsBeanPostProcessorFactory.java +++ b/src/main/java/com/ryantenney/metrics/spring/MetricsBeanPostProcessorFactory.java @@ -53,6 +53,11 @@ public static CachedGaugeAnnotationBeanPostProcessor cachedGauge(final MetricReg return new CachedGaugeAnnotationBeanPostProcessor(metricRegistry); } + public static MetricAnnotationBeanPostProcessor metric(final MetricRegistry metricRegistry) { + return new MetricAnnotationBeanPostProcessor(metricRegistry); + } + + @Deprecated public static InjectMetricAnnotationBeanPostProcessor injectMetric(final MetricRegistry metricRegistry) { return new InjectMetricAnnotationBeanPostProcessor(metricRegistry); } diff --git a/src/main/java/com/ryantenney/metrics/spring/Util.java b/src/main/java/com/ryantenney/metrics/spring/Util.java index f243612b..630b73ba 100755 --- a/src/main/java/com/ryantenney/metrics/spring/Util.java +++ b/src/main/java/com/ryantenney/metrics/spring/Util.java @@ -26,7 +26,9 @@ import com.ryantenney.metrics.annotation.CachedGauge; import com.ryantenney.metrics.annotation.Counted; import com.ryantenney.metrics.annotation.InjectMetric; +import com.ryantenney.metrics.annotation.Metric; +@SuppressWarnings("deprecation") class Util { private Util() { @@ -60,6 +62,10 @@ static String forInjectMetricField(Class klass, Member member, InjectMetric a return chooseName(annotation.name(), annotation.absolute(), klass, member); } + static String forMetricField(Class klass, Member member, Metric annotation) { + return chooseName(annotation.name(), annotation.absolute(), klass, member); + } + static String chooseName(String explicitName, boolean absolute, Class klass, Member member, String... suffixes) { if (explicitName != null && !explicitName.isEmpty()) { if (absolute) { diff --git a/src/main/java/com/ryantenney/metrics/spring/config/AnnotationDrivenBeanDefinitionParser.java b/src/main/java/com/ryantenney/metrics/spring/config/AnnotationDrivenBeanDefinitionParser.java index acd8bea6..dda6c00f 100755 --- a/src/main/java/com/ryantenney/metrics/spring/config/AnnotationDrivenBeanDefinitionParser.java +++ b/src/main/java/com/ryantenney/metrics/spring/config/AnnotationDrivenBeanDefinitionParser.java @@ -97,6 +97,11 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { .setFactoryMethod("cachedGauge") .addConstructorArgReference(metricsBeanName)); + registerComponent(parserContext, + build(MetricsBeanPostProcessorFactory.class, source, ROLE_INFRASTRUCTURE) + .setFactoryMethod("metric") + .addConstructorArgReference(metricsBeanName)); + registerComponent(parserContext, build(MetricsBeanPostProcessorFactory.class, source, ROLE_INFRASTRUCTURE) .setFactoryMethod("injectMetric") diff --git a/src/main/java/com/ryantenney/metrics/spring/config/annotation/MetricsConfigurationSupport.java b/src/main/java/com/ryantenney/metrics/spring/config/annotation/MetricsConfigurationSupport.java index ea7b27ed..13006505 100755 --- a/src/main/java/com/ryantenney/metrics/spring/config/annotation/MetricsConfigurationSupport.java +++ b/src/main/java/com/ryantenney/metrics/spring/config/annotation/MetricsConfigurationSupport.java @@ -98,6 +98,13 @@ public BeanPostProcessor cachedGaugeAnnotationBeanPostProcessor() { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanPostProcessor metricAnnotationBeanPostProcessor() { + return MetricsBeanPostProcessorFactory.metric(getMetricRegistry()); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @SuppressWarnings("deprecation") public BeanPostProcessor injectMetricAnnotationBeanPostProcessor() { return MetricsBeanPostProcessorFactory.injectMetric(getMetricRegistry()); } diff --git a/src/test/java/com/ryantenney/metrics/spring/InjectMetricTest.java b/src/test/java/com/ryantenney/metrics/spring/InjectMetricTest.java index 435afc8b..facf71d1 100755 --- a/src/test/java/com/ryantenney/metrics/spring/InjectMetricTest.java +++ b/src/test/java/com/ryantenney/metrics/spring/InjectMetricTest.java @@ -34,6 +34,7 @@ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:injected-metrics.xml") +@SuppressWarnings("deprecation") public class InjectMetricTest { @Autowired diff --git a/src/test/java/com/ryantenney/metrics/spring/MetricAnnotationTest.java b/src/test/java/com/ryantenney/metrics/spring/MetricAnnotationTest.java new file mode 100755 index 00000000..8e3dba50 --- /dev/null +++ b/src/test/java/com/ryantenney/metrics/spring/MetricAnnotationTest.java @@ -0,0 +1,107 @@ +/** + * 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 static com.ryantenney.metrics.spring.TestUtil.forMetricField; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.codahale.metrics.UniformReservoir; +import com.ryantenney.metrics.annotation.Metric; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:metric-annotation.xml") +public class MetricAnnotationTest { + + @Autowired + @Qualifier("target1") + MetricAnnotationTest.Target target; + + @Autowired + @Qualifier("target2") + MetricAnnotationTest.Target target2; + + @Autowired + MetricRegistry metricRegistry; + + @Test + public void targetIsNotNull() throws Exception { + assertNotNull(target); + assertNotNull(target2); + + assertNotNull(target.theNameForTheMeter); + assertNotNull(target2.theNameForTheMeter); + Meter meter = (Meter) forMetricField(metricRegistry, MetricAnnotationTest.Target.class, "theNameForTheMeter"); + assertSame(target.theNameForTheMeter, meter); + assertSame(target2.theNameForTheMeter, meter); + + assertNotNull(target.timer); + assertNotNull(target2.timer); + Timer timer = (Timer) forMetricField(metricRegistry, MetricAnnotationTest.Target.class, "timer"); + assertSame(target.timer, timer); + assertSame(target2.timer, timer); + + assertNotNull(target.counter); + assertNotNull(target2.counter); + Counter ctr = (Counter) forMetricField(metricRegistry, MetricAnnotationTest.Target.class, "counter"); + assertSame(target.counter, ctr); + assertSame(target2.counter, ctr); + + assertNotNull(target.histogram); + assertNotNull(target2.histogram); + Histogram hist = (Histogram) forMetricField(metricRegistry, MetricAnnotationTest.Target.class, "histogram"); + assertSame(target.histogram, hist); + assertSame(target2.histogram, hist); + + assertNotNull(target.uniformHistogram); + assertNotNull(target2.uniformHistogram); + Histogram uniHist = (Histogram) forMetricField(metricRegistry, MetricAnnotationTest.Target.class, "uniformHistogram"); + assertSame(target.uniformHistogram, uniHist); + assertSame(target2.uniformHistogram, uniHist); + } + + public static class Target { + + @Metric + public Meter theNameForTheMeter; + + @Metric(name = "theNameForTheTimer") + private Timer timer; + + @Metric(name = "group.type.counter", absolute = true) + Counter counter; + + @Metric + Histogram histogram; + + @Metric + Histogram uniformHistogram = new Histogram(new UniformReservoir()); + + } + +} diff --git a/src/test/java/com/ryantenney/metrics/spring/TestSuite.java b/src/test/java/com/ryantenney/metrics/spring/TestSuite.java index 5bd56259..b77a5777 100644 --- a/src/test/java/com/ryantenney/metrics/spring/TestSuite.java +++ b/src/test/java/com/ryantenney/metrics/spring/TestSuite.java @@ -33,6 +33,7 @@ MeteredClassImpementsInterfaceTest.class, MeteredClassTest.class, MeteredInterfaceTest.class, + MetricAnnotationTest.class, ProxyTargetClassTest.class, RegistryTest.class, ReporterTest.class, diff --git a/src/test/java/com/ryantenney/metrics/spring/TestUtil.java b/src/test/java/com/ryantenney/metrics/spring/TestUtil.java index d9ff2f17..756a9c1b 100755 --- a/src/test/java/com/ryantenney/metrics/spring/TestUtil.java +++ b/src/test/java/com/ryantenney/metrics/spring/TestUtil.java @@ -29,7 +29,6 @@ import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; -import com.codahale.metrics.Metric; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.codahale.metrics.annotation.ExceptionMetered; @@ -37,7 +36,9 @@ import com.codahale.metrics.annotation.Timed; import com.ryantenney.metrics.annotation.Counted; import com.ryantenney.metrics.annotation.InjectMetric; +import com.ryantenney.metrics.annotation.Metric; +@SuppressWarnings("deprecation") class TestUtil { private static final Logger log = LoggerFactory.getLogger(TestUtil.class); @@ -66,6 +67,10 @@ static String forCountedMethod(Class klass, Member member, Counted annotation return Util.forCountedMethod(klass, member, annotation); } + static String forMetricField(Class klass, Member member, Metric annotation) { + return Util.forMetricField(klass, member, annotation); + } + static String forInjectMetricField(Class klass, Member member, InjectMetric annotation) { return Util.forInjectMetricField(klass, member, annotation); } @@ -119,11 +124,21 @@ static Counter forCountedMethod(MetricRegistry metricRegistry, Class clazz, S return metricRegistry.getCounters().get(metricName); } - static Metric forInjectMetricField(MetricRegistry metricRegistry, Class clazz, String fieldName) { + @Deprecated + static com.codahale.metrics.Metric forInjectMetricField(MetricRegistry metricRegistry, Class clazz, String fieldName) { Field field = findField(clazz, fieldName); String metricName = forInjectMetricField(clazz, field, field.getAnnotation(InjectMetric.class)); + return getMetric(metricRegistry, field.getType(), metricName); + } + + static com.codahale.metrics.Metric forMetricField(MetricRegistry metricRegistry, Class clazz, String fieldName) { + Field field = findField(clazz, fieldName); + String metricName = forMetricField(clazz, field, field.getAnnotation(Metric.class)); + return getMetric(metricRegistry, field.getType(), metricName); + } + + private static com.codahale.metrics.Metric getMetric(MetricRegistry metricRegistry, Class type, String metricName) { log.info("Looking up injected metric field named '{}'", metricName); - Class type = field.getType(); if (type.isAssignableFrom(Meter.class)) { return metricRegistry.getMeters().get(metricName); } diff --git a/src/test/resources/metric-annotation.xml b/src/test/resources/metric-annotation.xml new file mode 100755 index 00000000..970d16eb --- /dev/null +++ b/src/test/resources/metric-annotation.xml @@ -0,0 +1,34 @@ + + + + + + + + + +