拦截方法调用并添加/丰富参数
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
,可以在 发送请求之前 应用。
的示例
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 的方法。
我正在编写一个休息 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
,可以在 发送请求之前 应用。
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 的方法。