关于java:如何使用AOP和Feign调用

How to use AOP with Feign calls

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

API:

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

配置:

1
2
3
4
5
6
7
8
9
10
@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 类注解:

1
2
3
4
5
6
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MeteredRemoteCall {

    String serviceName();
}


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

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

那你能做什么?最好的选择是在 Spring 应用程序中通过 LTW(加载时编织)使用完整的 AspectJ。这使您能够使用 call() 切入点而不是 Spring AOP 隐式使用的 execution()。如果您在 AspectJ 中的方法上使用 @annotation() 切入点,它将匹配调用和执行,正如我将在一个独立示例中向您展示的那样(没有 Spring,但效果与 Spring 中带有 LTW 的 AspectJ 相同):

标记注释:

1
2
3
4
5
6
7
8
9
10
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 源代码)作为字符串抓取。

1
2
3
4
5
6
7
8
9
10
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 的规范用法
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    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("([^<]+)", Pattern.CASE_INSENSITIVE)
                .matcher(soClient.getQuestionPage(questionId));
            titleMatcher.find();
            System.out.println(" " + titleMatcher.group(1));
        }
    }

    没有方面的控制台日志:

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

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

    方面:

    这基本上是您的附加连接点日志记录的方面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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();
        }
    }

    带有方面的控制台日志:

    1
    2
    3
    4
    5
    6
    7
    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() 因为即使手动实例化,实现类也没有接口方法的注释。所以 execution() 无法匹配。
  • call()execution() 因为我们手动将标记注释添加到实现类。
  • 只有 call() 因为 Feign 创建的动态代理没有接口方法的注解。所以 execution() 无法匹配。
  • 我希望这可以帮助您了解发生了什么以及为什么。

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