简体   繁体   中英

Spring aspects intercept annotated interface

Edit

How can either methods on spring beans be intercepted given the presence of an annotation on an interface that they implement?

I've added this as it more accurately describes the actual problem trying to be solved. Below is my attempt to solve the problem. However a completely different approach is acceptable.

Origional question

Given the following classes

@Timed
public static interface TimedInterface {
        
        
        public void interfaceMethod();
}
    
public static class TimedInterfaceImplementation implements TimedInterface {

        @Override
        public void interfaceMethod() {
            //NO-OP
        }
        
}

What implementation of @Advise can intercept the method invocations to interfaceMethod by detecting the @Timed annotation.

My current version of the spring aspect is

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class TimingAspect {

    private TimerContext timerContext;

    public TimingAspect(TimerContext ctx) {
        this.timerContext = ctx;
    }

    @Pointcut(value = "@within(timed)")
    public void beanAnnotatedWithTimer(Timed timed) {}
    
    @Pointcut("execution(public * *(..))")
    public void publicMethod() {}
    
    @Pointcut("publicMethod() && beanAnnotatedWithTimer(timed)")
    public void publicMethodInsideAClassMarkedWithAtTimer(Timed timed) {}
    
    @Around(value = "execution(public * *+.*(..))"
            + " && @annotation(timed)", argNames = "timed")
    public Object aroundAnnotatedMethod(final ProceedingJoinPoint joinPoint, Timed timed) throws Throwable {
        return timerContext.runThrowable(joinPoint.getSignature().getName(), joinPoint::proceed);
    }
    
    
    @Around(value = "publicMethodInsideAClassMarkedWithAtTimer(timed)", argNames="timed")
    public Object aroundAnnotatedClass(final ProceedingJoinPoint joinPoint, Timed timed) throws Throwable {
        return timerContext.runThrowable(joinPoint.getSignature().getName(), joinPoint::proceed);
    } 
    
    /**
     * This is here to ensure that the correct annotation is imported.
     * 
     * It allows for refactoring. I'm not expecting this method to actually get
     * called anywhere.
     * 
     * @return The fully qualified name of the {@link Timer} annotation.
     */
    public String annotationName() {
        return Timed.class.getName();
    }

}

This works for concretely annotated classes (Type annotation). This works for annotated methods with classes (Method annotation).

However I'd like to alter this to work for all methods that Implement an annotated interface.

Note that I don't mind switching from aspectj style advise to any other. However there are some beans being created by spring that don't have concrete classes that I need my Timing code to intercept.

Emulate annotation inheritance for interfaces and methods with AspectJ doesn't work as it means writing a processor to every interface.

Spring aspect call on custom annotation on interface method states thats its not possible. However I know spring does this for @Transactional among other annotations.

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;
        };

    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM