It is possible to intercept method invocations on annotated interfaces by using a PointcutAdvisor.
It may be possible to do through a pointcut expression but I couldn't get it working as classes don't inherit type level annotations from interfaces.
The solution was to implement an Abstract pointcut advisor and add that as a bean to the spring application context.
This is heavily inspired by the blog post at http://blog.javaforge.net/post/76125490725/spring-aop-method-interceptor-annotation
Note: that this implementation is coupled to some internal classes but it should be easy to generify to use own annotations or to do different advise.
Note: this implementation is coupled to spring but that was the point.
Note: As with all spring implementations this is proxy based so it won't work with self calls and it won't work with private members, Also it will only proxy spring beans (as its the framework doing the proxying)
Implementation without comments
This implementation should be easier to scan read if you need to get an answer quickly.
See the complete class if you need the imports.
public class TimingAdvisor extends AbstractPointcutAdvisor {
private static final long serialVersionUID = 1L;
private final MethodInterceptor interceptor;
private final StaticMethodMatcherPointcut pointcut = new TimingAnnotationOnClassOrInheritedInterfacePointcut();
public TimingAdvisor(TimerContext timerContext) {
super();
this.interceptor = (MethodInvocation invocation) -> timerContext.runThrowable(invocation.getMethod().getName(),
invocation::proceed);
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.interceptor;
}
private final class TimingAnnotationOnClassOrInheritedInterfacePointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
if (AnnotationUtils.findAnnotation(method, Timed.class) != null) {
return true;
}
return AnnotationUtils.findAnnotation(targetClass, Timed.class) != null;
}
}
}
Implementation
import java.lang.reflect.Method;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.core.annotation.AnnotationUtils;
/**
* <p>
* Intercepts all calls to beans with methods annotated with {@link Timed}.
* </p>
*
* <p>
* The following use cases have been tested.
* </p>
* <ul>
* <li>Nested invocation Timed bean invokes another TimedBean.</li>
* <li>Annotated class.</li>
* <li>Annotated method on a class.</li>
* <li>Class implementing annotated interface.</li>
* <li>Class implementing an Interface with an annotated method</li>
* </ul>
*
* <p>
* Calls to timed methods will be passed though
* {@link TimerContext#runThrowable(String, TimerContext.ThrowableSupplier)}
* </p>
*
*
* <strong>Important Notes and Limitations</strong>
*
* <ul>
* <li>This will only work with Spring beans as its using spring own advising
* mechanism.</li>
* <li>This will only work with public method invocations as with all of springs
* proxies.</li>
* <li>This will not work for self calls.</li>
* </ul>
* <p>
* The limitations are described in further details in the <a href=
* "https://docs.spring.io/spring/docs/3.2.4.RELEASE/spring-framework-reference/html/aop.html#aop-proxying">spring
* manual</a>.
*
*/
public class TimingAdvisor extends AbstractPointcutAdvisor {
private static final long serialVersionUID = 1L;
private final MethodInterceptor interceptor;
private final StaticMethodMatcherPointcut pointcut = new TimingAnnotationOnClassOrInheritedInterfacePointcut();
/**
* Constructor.
*
* @param timerContext
* The context where the timing will be run on.
*/
public TimingAdvisor(TimerContext timerContext) {
super();
this.interceptor = (MethodInvocation invocation) -> timerContext.runThrowable(invocation.getMethod().getName(),
invocation::proceed);
}
/*
* (non-Javadoc)
*
* @see org.springframework.aop.PointcutAdvisor#getPointcut()
*/
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
/*
* (non-Javadoc)
*
* @see org.springframework.aop.Advisor#getAdvice()
*/
@Override
public Advice getAdvice() {
return this.interceptor;
}
/**
* A matcher that matches:
* <ul>
* <li>A method on a class annotated with Timed.</li>
* <li>A method on a class extending another class annotated with
* Timed.</li>
* <li>A method on a class implementing an interface annotated with
* Timed.</li>
* <li>A method implementing a method in a interface annotated with
* Timed.</li>
* </ul>
*
* <p>
* <strong>Note:</strong> this uses springs utils to find the annotation and will not be
* portable outside the spring environment.
* </p>
*/
private final class TimingAnnotationOnClassOrInheritedInterfacePointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
if (AnnotationUtils.findAnnotation(method, Timed.class) != null) {
return true;
}
return AnnotationUtils.findAnnotation(targetClass, Timed.class) != null;
}
}
}
Test case
Note that this test case is acutally testing the desired outcome and is specific to the needs for the application that I'm running. The desired implementation for my specific need is to submit a time to a guage service .
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.metrics.GaugeService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TimerContextTest.ContextConfig.class)
public class TimerContextTest {
@Autowired
private TimedClassA timedClass;
@Autowired
private RecordingGaugeService gaugeService;
@Autowired
private ClassWithTimedMethod partiallyTimed;
@Autowired
private TimedInterface timedInterface;
@Autowired
private PartiallyTimedInterface partiallyTimedInterface;
@Before
public void setup() {
gaugeService.clear();
}
@Test
public void mustRetainHirachy() {
timedClass.outer();
assertThat(gaugeService.entries()).hasSize(2).contains("timer.outer", "timer.outer.inner");
}
@Test
public void mustNotBeInvokedOnPrivateMethods() {
timedClass.somethingPrivate();
assertThat(gaugeService.entries()).isEmpty();
}
@Test
public void mustBeInvokedForMethodsAnnotatedWithTimed() {
String untimed = partiallyTimed.untimed();
assertThat(untimed).isEqualTo("untimed result");
assertThat(gaugeService.entries()).isEmpty();
String timed = partiallyTimed.timed();
assertThat(timed).isEqualTo("timed result");
assertThat(gaugeService.entries()).containsExactly("timer.timed");
assertThatThrownBy(() -> {
partiallyTimed.timedExceptionThrower();
}).hasMessage("timedExceptionThrower");
assertThat(gaugeService.entries()).containsExactly("timer.timed", "timer.timedExceptionThrower");
}
@Test
public void mustBeInvokedAsTopLevelMoreThanOnce() {
partiallyTimed.timed();
partiallyTimed.timed();
assertThat(gaugeService.entries()).containsExactly("timer.timed", "timer.timed");
}
@Test
public void mustTimeInterfaceImplementations() {
timedInterface.interfaceMethod();
assertThat(gaugeService.entries()).containsExactly("timer.interfaceMethod");
}
@Test
public void mustTimeAnnotatedInterfaceMethods() {
partiallyTimedInterface.timedMethod();
partiallyTimedInterface.untimedMethod();
partiallyTimedInterface.timedDefaultMethod();
partiallyTimedInterface.untimedDefaultMethod();
assertThat(gaugeService.entries()).containsExactly("timer.timedMethod", "timer.timedDefaultMethod");
}
//////////////////////////////
// Configuration and Helpers
//////////////////////////////
@Configuration
@EnableAspectJAutoProxy
public static class ContextConfig {
@Bean
public GaugeService gaugeService() {
return new RecordingGaugeService();
}
@Bean
public TimerContext timerContext(GaugeService gaugeService) {
return new TimerContext(gaugeService);
}
@Bean
public TimedClassB inner() {
return new TimedClassB();
}
@Bean
public TimedClassA outer(TimedClassB inner) {
return new TimedClassA(inner);
}
@Bean
public TimingAdvisor timingAdvisor(TimerContext ctx) {
return new TimingAdvisor(ctx);
}
@Bean
public ClassWithTimedMethod partiallyTimed() {
return new ClassWithTimedMethod();
}
@Bean
public TimedInterface timedInterface() {
return new TimedInterfaceImplementation();
}
@Bean
public PartiallyTimedInterface partiallyTimedInterface() {
return new ClassImplementingPartiallyTimedInterface();
}
}
@Timed
public static class TimedClassA {
private TimedClassB inner;
public TimedClassA(TimedClassB inner) {
this.inner = inner;
}
public String outer() {
return this.inner.inner();
}
private String somethingPrivate() {
return "private";
}
}
@Timed
public static class TimedClassB {
public String inner() {
return "inner";
}
}
@Timed
public static interface TimedInterface {
public void interfaceMethod();
}
public static class TimedInterfaceImplementation implements TimedInterface {
@Override
public void interfaceMethod() {
//NO-OP
}
}
public static interface PartiallyTimedInterface {
@Timed public void timedMethod();
public void untimedMethod();
@Timed public default void timedDefaultMethod() {}
public default void untimedDefaultMethod() {}
}
public static class ClassImplementingPartiallyTimedInterface implements PartiallyTimedInterface {
@Override
public void timedMethod() {
// NO-OP
}
@Override
public void untimedMethod() {
// NO-OP
}
}
public static class ClassWithTimedMethod {
public String untimed() {
return "untimed result";
}
@Timed
public String timed() {
return "timed result";
}
@Timed
public String timedExceptionThrower() {
throw new IllegalStateException("timedExceptionThrower");
}
}
private static class RecordingGaugeService implements GaugeService {
private List<String> recordedMetrics = new ArrayList<>();
@Override
public void submit(String metricName, double value) {
this.recordedMetrics.add(metricName);
System.out.println(metricName);
}
public void clear() {
recordedMetrics = new ArrayList<>();
}
public List<String> entries() {
return recordedMetrics;
};
}
}