拦截方法调用并添加/丰富参数

Intercept method calls and add / enrich parameters

我正在编写一个休息 api 客户端,它需要连接到不同端点上的一些 API(相同的 API),所有端点都提供相同的数据。为此,我需要动态设置每个调用 url 和 auth header。由于我使用 spring 作为框架,我的计划是使用 feign 作为其余客户端。

下面是我需要在代码中做的事情

伪装客户:

@FeignClient(
    name = "foo",
    url = "http://placeholderThatWillNeverBeUsed.io",
    fallbackFactory = ArticleFeignClient.ArticleClientFallbackFactory.class
)
public interface ArticleFeignClient {
    @GetMapping(value = "articles/{id}", consumes = "application/json", produces = "application/json")
    public ArticleResponse getArticles(URI baseUrl, @RequestHeader("Authorization") String token, @PathVariable Integer id);

    @GetMapping(value = "articles", consumes = "application/json", produces = "application/json")
    public MultiArticleResponse getArticles(URI baseUrl, @RequestHeader("Authorization") String token);
}

手动丰富参数的ArticleClient:

@Service
public class ArticleClient extends AbstractFeignClientSupport {
    private final ArticleFeignClient articleFeignClient;

    @Autowired
    public ArticleClient(ArticleFeignClient articleFeignClient, AccessDataService accessDataService) {
        super(accessDataService);
        this.articleFeignClient = articleFeignClient;
    }

    public ArticleResponse getArticles(String connection, Integer id) {
        var accessData = getAccessDataByConnection(connection);
        return articleFeignClient.getArticles(URI.create(accessData.getEndpoint()), "Basic " + getAuthToken(accessData),id);
    }

    public MultiArticleResponse getArticles(String connection) {
        var accessData = getAccessDataByConnection(connection);
        return articleFeignClient.getArticles(URI.create(accessData.getEndpoint()), "Basic " + getAuthToken(accessData));
    }
}

拥有丰富内容的客户支持

public abstract class AbstractFeignClientSupport {
    private final AccessDataService accessDataService;

    public AbstractFeignClientSupport(AccessDataService accessDataService) {
        this.accessDataService = accessDataService;
    }

    final public AccessData getAccessDataByConnection(@NotNull String connection) {
        return accessDataService.findOneByConnection(connection).orElseThrow();
    }
}

如您所见,

会有很多重复
var accessData = getAccessDataByConnection(connection);
return clientToCall.methodToCall(URI.create(accessData.getEndpoint()), "Basic " + getAuthToken(accessData),id);

这只是将请求的 URI 和 Auth Header 添加到实际假客户端的方法调用中。

我想知道是否有更好的方法并且一直在研究使用 AOP 或注释来拦截我的方法调用,为给定包(或注释方法)中的每个调用添加两个参数,以便我只需担心一次,无需重复 40 种左右的方法。

在吗?如果可以,怎么做?

方面往往是一个非常肮脏的事情,typesafety-wise。

例如,要操作传递给方法的 List,您首先需要从连接点提供的 meta-information 中提取它。这看起来有点像这样:

@Pointcut("within(@com.your.company.SomeAnnotationType *)")
public void methodsYouWantToAdvise() {};

@Aspect
public class AddToList {
@Around("methodsYouWantToAdvise()")
public Object addToList(ProceedingJoinPoint thisJoinPoint) throws Throwable {
    Object[] args = thisJoinPoint.getArgs();
    // you know the first parameter is the list you want to adjust
    List l = (List) args[0];
    l.add("new Value");

    thisJoinPoint.proceed(args);
}

这绝对可以做得更好,但这几乎是您如何实现这一方面的要点。

也许 check out this article 至少要打好基础。

因为用户 daniu asked how to use args(), here is an MCVE 使用 AspectJ(不是 Spring AOP,但相同的切入点语法可以在那里工作):

package de.scrum_master.app;

import java.util.ArrayList;
import java.util.List;

@SomeAnnotationType
public class Application {
  public void doSomething() {}
  public void doSomething(List<String> names) {}
  public void doSomethingDifferent(List<String> names) {}
  public void doSomethingInteresting(String... names) {}
  public void doSomethingElse(List<Integer> numbers) {}
  public void doSomethingGeneric(List objects) {}

  public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("Albert Einstein");
    names.add("Werner Heisenberg");
    List<Integer> numbers = new ArrayList<>();
    numbers.add(11);
    numbers.add(22);

    Application application = new Application();
    application.doSomething();
    application.doSomething(names);
    application.doSomethingDifferent(names);
    application.doSomethingInteresting("Niels Bohr", "Enrico Fermi");
    application.doSomethingElse(numbers);
    application.doSomethingGeneric(names);
    application.doSomethingGeneric(numbers);

    System.out.println();
    for (String name : names)
      System.out.println(name);

    System.out.println();
    for (Integer number : numbers)
      System.out.println(number);
  }
}

没有应用任何方面,控制台日志是这样的:

Albert Einstein
Werner Heisenberg

11
22

现在我们添加一个类似于 daniu 的方面,只是使用 args() 以便将 List<String> 参数绑定到方面切入点参数:

package de.scrum_master.aspect;

import java.util.List;

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 AddToList {
  @Pointcut("@within(de.scrum_master.app.SomeAnnotationType) && execution(* *(..)) && args(names)")
  public void methodsYouWantToAdvise(List<String> names) {}

  @Around("methodsYouWantToAdvise(names)")
  public Object addToList(ProceedingJoinPoint thisJoinPoint, List<String> names) throws Throwable {
    System.out.println(thisJoinPoint);
    names.add(thisJoinPoint.getSignature().getName());
    return thisJoinPoint.proceed();
  }
}

请注意:

  • 而不是 daniu 建议的 within(@de.scrum_master.app.SomeAnnotationType *) 我正在使用更专业的 @within(de.scrum_master.app.SomeAnnotationType).

  • 我正在添加 && execution(* *(..)) 因为在 AspectJ 中不仅仅有 execution() 连接点,例如call() 并且我不想在每次方法调用 + 执行时匹配切入点两次。在 Spring AOP 中,您可以根据需要省略 && execution(* *(..))

  • args(names) 切入点指示符仅匹配具有单个 List 参数的方法,而不匹配具有其他参数的方法。如果你想要第一个参数是 List 但其他参数可能跟随的所有匹配方法,只需使用 args(names, ..).

  • 当用AspectJ编译器编译这个方面时,你会看到一个警告:unchecked match of List<String> with List when argument is an instance of List at join point method-execution(void de.scrum_master.app.Application.doSomethingGeneric(List)) [Xlint:uncheckedArgument]。这意味着什么,我们稍后会看到。

现在让我们看一下控制台日志:

execution(void de.scrum_master.app.Application.doSomething(List))
execution(void de.scrum_master.app.Application.doSomethingDifferent(List))
execution(void de.scrum_master.app.Application.doSomethingGeneric(List))
execution(void de.scrum_master.app.Application.doSomethingGeneric(List))

Albert Einstein
Werner Heisenberg
doSomething
doSomethingDifferent
doSomethingGeneric

11
22
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
    at de.scrum_master.app.Application.main(Application.java:37)

如您所见,切入点仅匹配具有单个 List<String> 参数的方法并排除例如doSomethingElse(List<Integer>) 它也匹配 doSomethingGeneric(List),即具有原始泛型类型的方法。它甚至匹配了两次,分别是在使用 List<String> 参数和 List<Integer> 参数调用时。

现在这主要不是 AspectJ 问题,而是 Java 称为类型擦除的泛型限制。喜欢的可以google那个,这里详细解释一下就off-topic了。无论如何,这通常意味着在运行时您可以将任何内容添加到通用列表中,JVM 不知道您可能将字符串添加到整数列表中,这正是方面在这种情况下所做的。因此,当稍后在 for-loop 中我们假设所有列表元素都是整数时,我们会得到异常,您可以在上面的控制台日志中看到。

现在让我们把最后一个 for-loop 改成这个:

for (Object number : numbers)
  System.out.println(number);

然后异常消失并且 for-loop 打印:

11
22
doSomethingGeneric

现在至于原来的问题,我们那里的泛型没有任何问题,它更容易。切入点看起来像

@Pointcut("@within(org.springframework.stereotype.Service) && execution(* *(..)) && args(connection, ..)")
public void methodsYouWantToAdvise(String connection) {}

这应该匹配上面示例中的两个 getArticles(..) 方法,但是然后呢?请注意,您要提取的代码并不完全相同。一次你有身份证,一次你没有。所以要么你做两个切入点+相应的建议(你也可以内联切入点,如果你不重用它们就不需要单独指定它们)或者你做一些丑陋的 if-else 东西然后再次获得第二个可选参数通过 getArgs()。我认为您应该使用两个建议,因为您还调用了两个具有不同签名(即不同的参数列表和不同的 return 类型)的不同重载 Feign 客户端方法。

您不需要使用 AOP 来实现此目的。 Feign 支持 RequestInterceptors,可以在 发送请求之前 应用。

这是来自 OpenFeign documentation

的示例
static class ForwardedForInterceptor implements RequestInterceptor {
  @Override public void apply(RequestTemplate template) {
     template.header("X-Forwarded-For", "origin.host.com");
  }
}

public class Example {
  public static void main(String[] args) {
  Bank bank = Feign.builder()
             .decoder(accountDecoder)
             .requestInterceptor(new ForwardedForInterceptor())
             .target(Bank.class, "https://api.examplebank.com");
  }
}

在此示例中,ForwardedForInteceptor 将 header 添加到使用 Bank 实例发送的每个请求。

在您的示例中,您可以创建一个拦截器,它依赖于您的增强器组件来添加额外的参数。

 @Component
 public class EnrichInterceptor implements RequestInterceptor {

    public AccessDataService accessDataService;

    public EnrichInterceptor(AccessDataService accessDataService) {
        this.accessDataService = accessDataService;
    }

    public void apply(RequestTemplate template) {
        AccessData data = this.accessDataService.getAccessByConnection(template.url());
        template.header("Authorization: Basic " + getToken(data));
    }
}

此示例展示了一种使用拦截器修改 header 的方法。