简体   繁体   English

Spring 方面拦截带注释的接口

[英]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?考虑到它们实现的接口上存在注释,如何拦截 spring bean 上的任一方法?

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. @Advise哪些实现可以通过检测@Timed注解来拦截对interfaceMethod的方法调用。

My current version of the spring aspect is我当前版本的 spring 方面是

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.请注意,我不介意从 aspectj 风格的建议切换到任何其他风格。 However there are some beans being created by spring that don't have concrete classes that I need my Timing code to intercept.但是,spring 创建的一些 bean 没有具体的类,我需要我的定时代码来拦截它们。

Emulate annotation inheritance for interfaces and methods with AspectJ doesn't work as it means writing a processor to every interface. 使用 AspectJ 模拟接口和方法的注解继承不起作用,因为这意味着为每个接口编写一个处理器。

Spring aspect call on custom annotation on interface method states thats its not possible. Spring 方面调用接口方法上的自定义注释表明这是不可能的。 However I know spring does this for @Transactional among other annotations.但是我知道 spring 在其他注释中为@Transactional执行此操作。

It is possible to intercept method invocations on annotated interfaces by using a PointcutAdvisor .可以使用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.解决方案是实现一个抽象切入点顾问,并将其作为 bean 添加到 spring 应用程序上下文中。

This is heavily inspired by the blog post at http://blog.javaforge.net/post/76125490725/spring-aop-method-interceptor-annotation这很大程度上受到了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.注意:此实现与 spring 耦合,但这就是重点。

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)注意:与所有 spring 实现一样,这是基于代理的,因此它不适用于自调用,也不适用于私有成员,而且它只会代理 spring bean(作为它的框架进行代理)

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 .我的特定需求的理想实现是向guage 服务提交时间。

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

    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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