【问题标题】:Intercept method calls and add / enrich parameters拦截方法调用并添加/丰富参数
【发布时间】:2019-10-22 18:46:47
【问题描述】:

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

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

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 添加到实际 feign 客户端的方法调用中。

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

有吗?如果有,怎么做?

【问题讨论】:

  • 是的,AOP 可以工作;创建一个调整方法参数的“Around”建议。我会说这里有点太复杂了,无法正确介绍这里需要什么。您需要首先决定建议哪些方法以及如何确定它们(带有注释的 eq 标记),然后创建一个实现您想要的逻辑的建议。

标签: java spring aop feign


【解决方案1】:

在类型安全方面,Aspects 往往是一项相当肮脏的业务。

要操作传递给方法的List,首先需要从连接点提供的元信息中提取它。这看起来有点像这样:

@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 至少可以打好基础。

【讨论】:

  • IMO 的问题在于许多人对方面的了解不够,而不是后者有任何类型安全问题。如果您使用args() 来绑定方法参数而不是使用JoinPoint.getArgs(),那么它是完全类型安全的,并且没有丑陋的强制转换。
  • @kriegaex 很有趣。 args() 声明的内容是什么,但我找不到。
  • 我添加了一个广泛的答案来回答你的问题,最后关闭循环回到 OP 的原始问题。
【解决方案2】:

因为用户daniu 询问如何使用args(),所以这里有一个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

现在我们添加一个类似于大牛的切面,只是使用args() 来将List&lt;String&gt; 参数绑定到切面切入点参数:

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

请注意:

  • 我使用更专业的@within(de.scrum_master.app.SomeAnnotationType),而不是大牛建议的within(@de.scrum_master.app.SomeAnnotationType *)

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

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

  • 使用 AspectJ 编译器编译此方面时,您将看到警告:unchecked match of List&lt;String&gt; 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&lt;String&gt; 参数的方法,并排除例如doSomethingElse(List&lt;Integer&gt;)它也匹配 doSomethingGeneric(List),即具有原始泛型类型的方法。它甚至匹配了两次,都在使用List&lt;String&gt; 参数和List&lt;Integer&gt; 参数调用时。

现在这主要不是 AspectJ 问题,而是称为类型擦除的 Java 泛型限制。如果你愿意,你可以用谷歌搜索,在这里详细解释它是题外话。无论如何,通常这意味着在运行时您可以将任何内容添加到通用列表中,JVM 不知道您可能正在将字符串添加到整数列表中,这正是方面在这种情况下所做的。因此,稍后在 for 循环中我们假设所有列表元素都是整数时,我们会得到您可以在上面的控制台日志中看到的异常。

现在让我们把最后一个 for 循环改成这样:

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

然后异常消失,for循环打印:

11
22
doSomethingGeneric

现在至于最初的问题,我们在泛型方面没有任何问题,这要容易得多。切入点看起来像

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

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

【讨论】:

    【解决方案3】:

    您不需要使用 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 将标头添加到使用 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));
        }
    }
    

    此示例显示了一种使用拦截器修改标头的方法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-11
      • 1970-01-01
      • 2012-04-01
      • 2021-09-16
      • 1970-01-01
      • 2015-08-14
      相关资源
      最近更新 更多