java - 如何在 Feign 调用中使用 AOP

标签 java aop aspectj spring-aop feign

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

接口(interface):

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

最佳答案

您的情况有些复杂,因为您有几个问题:

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

  • 所以,你可以做什么?最好的选择是use full AspectJ via LTW (加载时编织)来自您的 Spring 应用程序。这使您可以使用 call()切入点而不是 execution()这是 Spring AOP 隐式使用的。如果您使用 @annotation() AspectJ 中方法的切入点,它将匹配调用和执行,正如我将在一个独立示例中向您展示的那样(没有 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 {}
    

    假客户:

    此示例客户端将完整的 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 客户端界面进行演示:
  • 没有 Feign,通过匿名子类手动实例化
  • 与 #1 类似,但这次在实现方法
  • 中添加了额外的标记注释
  • 通过 Feign
  • 的规范用法

    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
    

    如您所见,对于这三种情况中的每一种情况,以下连接点都会被拦截:
  • 仅限 call()因为即使手动实例化,实现类也没有接口(interface)方法的注释。所以execution()无法匹配。
  • 两个call()execution()因为我们手动将标记注释添加到实现类中。
  • 仅限 call()因为Feign创建的动态代理没有接口(interface)方法的注解。所以execution()无法匹配。

  • 我希望这可以帮助您了解发生了什么以及为什么。

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

    关于java - 如何在 Feign 调用中使用 AOP,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41856687/

    相关文章:

    c# - 使用 PostSharp,无法让 Multicast 为 WinForm 控件单击处理程序工作

    java - “特殊” APP用例

    java - Camel 单元测试 - 我们如何编写单元测试来断言 bean 的特定方法被调用?

    java - Eclipse XML 编辑器显示 android1 不是 android 并且没有自动完成

    java - 如何为一组 Pascal 库函数编写 Java JNI 包装器?

    java - AspectJ:用于选择类上方法的目标与签名模式

    java - 哪种 Java 静态分析工具最容易扩展?

    java - Dozer 将 map 复制到另一个 map

    java - 通过 AOP 从 JdbcTemplate 或 SimpleJdbcTemplate 记录 SQL 语句

    java - AspectJ 切入点到方法调用(即使它是在外部库上调用的)