Перехватывать вызовы методов и добавлять / обогащать параметры - PullRequest
0 голосов
/ 07 июня 2019

Я пишу api-клиент rest rest, который должен подключаться к некоторым API (одинаковым API) на разных конечных точках, и все они обслуживают одни и те же данные.Для этого мне нужно динамически устанавливать каждый вызовы url и auth header.Так как я использую Spring в качестве фреймворка, я планировал использовать feign в качестве остального клиента.

Ниже показано, что мне нужно сделать в коде

Feign Client:

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

Clientподдержка, которая содержит обогащенный

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 и заголовок запроса к вызову метода для фактического отображенияклиент.

Мне интересно, есть ли лучший способ и я искал использование AOP или аннотаций, которые бы перехватывали мой вызов метода, добавив два параметра для каждого вызова в данном пакете (или аннотированный метод), чтобыМне придется беспокоиться об этом только один раз, и мне не нужно повторять это для примерно 40 методов.

Есть?Если да, то как?

Ответы [ 3 ]

1 голос
/ 10 июня 2019

Поскольку пользователь daniu спросил, как использовать args(), вот MCVE с использованием AspectJ (не Spring AOP, но там будет работать тот же синтаксис pointcut):

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

Обратите внимание:

  • Вместо within(@de.scrum_master.app.SomeAnnotationType *), как предлагает Даниу, я использую более специализированный @within(de.scrum_master.app.SomeAnnotationType).

  • Я добавляю && execution(* *(..)), потому что в AspectJ есть больше, чем просто execution() точек соединения, например call() и я не хочу сопоставлять pointcut дважды за вызов метода + выполнение. В 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)

Как видите, pointcut сопоставляет методы только с одним параметром List<String> и исключает, например, doSomethingElse(List<Integer>), , но также соответствует doSomethingGeneric(List), то есть метод с необработанным универсальным типом. Он даже совпадает с ним дважды, как при вызове с параметром List<String>, так и с параметром List<Integer>.

Теперь это, в основном, проблема не AspectJ, а ограничение универсального языка Java, называемое стиранием типов. Вы можете гуглить это, если хотите, было бы не по теме, чтобы объяснить это здесь подробно. В любом случае, обычно это означает, что во время выполнения вы можете добавить что-либо в общий список, JVM не знает, что вы можете добавлять строку в список целых чисел, что в точности и делает аспект в этом случае. Таким образом, когда позже в цикле for мы предполагаем, что все элементы списка являются целыми числами, мы получаем исключение, которое вы можете увидеть в журнале консоли выше.

Теперь давайте просто изменим последний цикл for следующим образом:

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

Затем исключение исчезает и выводится цикл for:

11
22
doSomethingGeneric

Теперь, что касается исходного вопроса, у нас нет проблем с дженериками, все гораздо проще. Pointcut будет выглядеть примерно так:

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

Это должно соответствовать обоим getArticles(..) методам в приведенном выше примере, но что тогда? Обратите внимание, что код, который вы хотите выделить, не полностью идентичен. Один раз у вас есть удостоверение личности, а один раз нет. Так что либо вы создаете два pointcuts + соответствующие советы (вы также можете встроить pointcut, вам не нужно указывать их отдельно, если вы не используете их повторно), либо вы делаете некрасивые вещи if-else и снова получаете второй, необязательный параметр через getArgs(). Я думаю, вам следует использовать два совета, потому что вы также вызываете два разных перегруженных клиентских метода Feign с разными сигнатурами (то есть разными списками параметров и разными типами возвращаемых данных).

1 голос
/ 07 июня 2019

Аспекты, как правило, довольно грязный бизнес с точки зрения безопасности.

Чтобы манипулировать, скажем, 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);
}

Это определенно можно сделать лучше, но это в значительной степени суть того, как вы можете реализовать такой аспект.

Может быть прочитайте эту статью , чтобы получить хотя бы основание.

0 голосов
/ 18 июля 2019

Вам не нужно использовать AOP для достижения этой цели. Feign имеет поддержку RequestInterceptors, которую можно применить до того, как запрос будет отправлен.

Вот пример из документации OpenFeign

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

В этом примере показан один способ изменить заголовок с помощью перехватчика.

...