繁体   English   中英

Spring 方面拦截带注释的接口

[英]Spring aspects intercept annotated interface

编辑

考虑到它们实现的接口上存在注释,如何拦截 spring bean 上的任一方法?

我添加了这个,因为它更准确地描述了试图解决的实际问题。 下面是我解决问题的尝试。 然而,完全不同的方法是可以接受的。

原题

鉴于以下类

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

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

@Advise哪些实现可以通过检测@Timed注解来拦截对interfaceMethod的方法调用。

我当前版本的 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();
    }

}

这适用于具体注释的类(类型注释)。 这适用于带类的带注释的方法(方法注释)。

但是我想改变它以适用于实现带注释接口的所有方法。

请注意,我不介意从 aspectj 风格的建议切换到任何其他风格。 但是,spring 创建的一些 bean 没有具体的类,我需要我的定时代码来拦截它们。

使用 AspectJ 模拟接口和方法的注解继承不起作用,因为这意味着为每个接口编写一个处理器。

Spring 方面调用接口方法上的自定义注释表明这是不可能的。 但是我知道 spring 在其他注释中为@Transactional执行此操作。

可以使用PointcutAdvisor拦截带注释的接口上的方法调用。

可能可以通过切入点表达式来完成,但我无法让它工作,因为类从接口继承类型级别的注释。

解决方案是实现一个抽象切入点顾问,并将其作为 bean 添加到 spring 应用程序上下文中。

这很大程度上受到了http://blog.javaforge.net/post/76125490725/spring-aop-method-interceptor-annotation 上的博客文章的启发

注意:这个实现与一些内部类耦合,但应该很容易泛化以使用自己的注释或做不同的建议。

注意:此实现与 spring 耦合,但这就是重点。

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

没有评论的实施

如果您需要快速获得答案,则此实现应该更易于扫描阅读。

如果您需要导入,请查看完整的类。

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

实施

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

测试用例

请注意,此测试用例实际上是在测试所需的结果,并且特定于我正在运行的应用程序的需求。 我的特定需求的理想实现是向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