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

时间:2019-06-07 09:08:26

标签: java spring aop feign

我正在编写一个rest api客户端,该客户端需要连接到不同端点上的一些API(相同API),所有这些API均提供相同的数据。为此,我需要动态设置每个调用的url和auth标头。由于我使用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多种方法。

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

3 个答案:

答案 0 :(得分:1)

从类型安全的角度来看,方面往往是一个相当肮脏的生意。

例如,要操作传递给方法的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至少可以为基础奠定基础。

答案 1 :(得分:1)

因为用户daniu询问了如何使用@Select(MyState.fullName) fullName$: Observable<string>; ,所以这里是使用AspectJ的MCVE(不是Spring AOP,但是在这里可以使用相同的切入点语法):

args()

在未应用任何方面的情况下,控制台日志如下:

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

现在,我们添加一个类似于daniu的方面,只需使用Albert Einstein Werner Heisenberg 11 22 即可将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),因为在AspectJ中,不仅有&& execution(* *(..))个连接点,例如execution(),并且我不想每个方法调用+执行两次匹配切入点。在Spring AOP中,您可以根据需要省略call()

  • && execution(* *(..))切入点指示符仅匹配具有单个args(names)参数的方法,而不匹配具有附加参数的方法。如果要使用第一个参数为List但所有其他参数都可能跟在后面的匹配方法,请使用List

  • 使用AspectJ编译器编译此方面时,您将看到警告:args(names, ..)。这将意味着我们将在一分钟内看到。

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

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>参数进行调用。

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

现在让我们将最后一个for循环更改为此:

List<Integer>

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

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

现在,对于原始问题,泛型没有任何问题,这要容易得多。切入点看起来就像

11
22
doSomethingGeneric

这应该与上面示例中的两个@Pointcut("@within(org.springframework.stereotype.Service) && execution(* *(..)) && args(connection, ..)") public void methodsYouWantToAdvise(String connection) {} 方法都匹配,但是那又如何呢?请注意,您要排除的代码并不完全相同。一次您拥有一个ID,一次您没有。因此,您可以制作两个切入点+相应的建议(也可以内联切入点,如果不重用它们则无需单独指定它们),或者做一些难看的if-else事情,然后再次通过{获得第二个可选参数{1}}。我认为您应该使用两条建议,因为您还会调用具有不同签名(即不同的参数列表和不同的返回类型)的两个不同的重载Feign客户端方法。

答案 2 :(得分:0)

您无需使用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实例发送的每个请求中。

在您的示例中,您可以创建一个依赖于您的richer组件的拦截器来添加其他参数。

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

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