如何在Feign调用中使用AOP

时间:2017-01-25 16:28:09

标签: java aop aspectj spring-aop feign

我对如何在AOP中使用Feign客户感兴趣。例如:

API:

public interface LoanClient {
    @RequestLine("GET /loans/{loanId}")
    @MeteredRemoteCall("loans")
    Loan getLoan(@Param("loanId") Long loanId);
}

配置:

@Aspect
@Component // Spring Component annotation
public class MetricAspect {

    @Around(value = "@annotation(annotation)", argNames = "joinPoint, annotation")
    public Object meterRemoteCall(ProceedingJoinPoint joinPoint, 
                        MeteredRemoteCall annotation) throws Throwable {
    // do something
  }
}

但我不知道如何拦截" api方法调用。我哪里出错了?

更新

我的Spring类注释:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MeteredRemoteCall {

    String serviceName();
}

1 个答案:

答案 0 :(得分:6)

你的情况有点复杂,因为你有几个问题:

  • 你使用Spring AOP,一个" AOP lite"基于动态代理的框架(接口的JDK代理,类的CGLIB代理)。它仅适用于Spring bean /组件,但从我看到的LoanClient不是Spring @Component
  • 即使它是一个Spring组件,Feign也会通过反射创建自己的JDK动态代理。它们不受Spring控制。可能有一种方法可以通过编程方式或通过XML配置手动将它们连接到Spring。但是我无法帮助你,因为我不使用Spring。
  • Spring AOP仅支持AspectJ切入点的子集。具体来说,它不支持call(),只支持execution()。即它只编织到执行方法的地方,而不是它被调用的地方。
  • 但是执行发生在一个实现接口的方法和接口方法上的注释,例如你的@MeteredRemoteCall永远不会被它们的实现类继承。事实上,方法注释永远在Java中继承,只有类级注释从类(不是接口!)到相应的子类。即即使您的注记类具有@Inherited元注释,但对@Target({ElementType.METHOD})也没有帮助,仅适用于@Target({ElementType.TYPE})更新:因为我之前已多次回答过这个问题,所以我刚刚记录了问题,并在Emulate annotation inheritance for interfaces and methods with AspectJ中解决了这个问题。

那你能做什么?最好的选择是从Spring应用程序中use full AspectJ via LTW(加载时编织)。这使您可以使用call()切入点而不是Spring {AOP隐式使用的execution()。如果你在AspectJ中的方法上使用@annotation()切入点,它将匹配调用和执行,因为我将在一个独立的示例中显示(没有Spring,但效果与在Spring中使用LTW的AspectJ相同) :

标记注释:

package de.scrum_master.app;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MeteredRemoteCall {}

Feign客户:

此示例客户端将完整的StackOverflow问题页面(HTML源代码)作为字符串抓取。

package de.scrum_master.app;

import feign.Param;
import feign.RequestLine;

public interface StackOverflowClient {
    @RequestLine("GET /questions/{questionId}")
    @MeteredRemoteCall
    String getQuestionPage(@Param("questionId") Long questionId);
}

驱动程序应用程序:

此应用程序以三种不同的方式使用Feign客户端界面进行演示:

  1. 没有Feign,通过匿名子类进行手动实例化
  2. 与#1类似,但这次添加了额外的标记注释到实现方法
  3. 通过Feign的典型用法
  4. package de.scrum_master.app;
    
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    import feign.Feign;
    import feign.codec.StringDecoder;
    
    public class Application {
        public static void main(String[] args) {
            StackOverflowClient soClient;
            long questionId = 41856687L;
    
            soClient = new StackOverflowClient() {
                @Override
                public String getQuestionPage(Long loanId) {
                    return "StackOverflowClient without Feign";
                }
            };
            System.out.println("  " + soClient.getQuestionPage(questionId));
    
            soClient = new StackOverflowClient() {
                @Override
                @MeteredRemoteCall
                public String getQuestionPage(Long loanId) {
                    return "StackOverflowClient without Feign + extra annotation";
                }
            };
            System.out.println("  " + soClient.getQuestionPage(questionId));
    
            // Create StackOverflowClient via Feign
            String baseUrl = "http://stackoverflow.com";
            soClient = Feign
                .builder()
                .decoder(new StringDecoder())
                .target(StackOverflowClient.class, baseUrl);
            Matcher titleMatcher = Pattern
                .compile("<title>([^<]+)</title>", Pattern.CASE_INSENSITIVE)
                .matcher(soClient.getQuestionPage(questionId));
            titleMatcher.find();
            System.out.println("  " + titleMatcher.group(1));
        }
    }
    

    没有方面的控制台日志:

      StackOverflowClient without Feign
      StackOverflowClient without Feign + extra annotation
      java - How to use AOP with Feign calls - Stack Overflow
    

    正如您所看到的,在#3的情况下,它只打印了这个StackOverflow问题的问题标题。 ;-)我正在使用正则表达式匹配器从HTML代码中提取它,因为我不想打印完整的网页。

    <强>方面:

    这基本上是您使用其他连接点记录的方面。

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    import de.scrum_master.app.MeteredRemoteCall;
    
    @Aspect
    public class MetricAspect {
        @Around(value = "@annotation(annotation)", argNames = "joinPoint, annotation")
        public Object meterRemoteCall(ProceedingJoinPoint joinPoint, MeteredRemoteCall annotation)
            throws Throwable
        {
            System.out.println(joinPoint);
            return joinPoint.proceed();
        }
    }
    

    带方面的控制台日志:

    call(String de.scrum_master.app.StackOverflowClient.getQuestionPage(Long))
      StackOverflowClient without Feign
    call(String de.scrum_master.app.StackOverflowClient.getQuestionPage(Long))
    execution(String de.scrum_master.app.Application.2.getQuestionPage(Long))
      StackOverflowClient without Feign + extra annotation
    call(String de.scrum_master.app.StackOverflowClient.getQuestionPage(Long))
      java - How to use AOP with Feign calls - Stack Overflow
    

    如您所见,以下三个案例中的每一个都会拦截以下连接点:

    1. call()因为即使使用手动实例化,实现类也没有接口方法的注释。因此execution()无法匹配。
    2. call()execution()都是因为我们手动将标记注释添加到实现类中。
    3. call(),因为Feign创建的动态代理没有接口方法的注释。因此execution()无法匹配。
    4. 我希望这可以帮助您了解发生的事情和原因。

      底线:使用完整的AspectJ,以使您的切入点与call()个连接点匹配。然后你的问题就解决了。