我正在编写一个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多种方法。
在吗?如果可以,怎么办?
答案 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
,可以在发送请求之前应用。
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));
}
}
此示例显示了一种使用拦截器修改标头的方法。